Skip to content

Latest commit

 

History

History
420 lines (322 loc) · 14.7 KB

File metadata and controls

420 lines (322 loc) · 14.7 KB

Getting Started

jinn is a sandboxed tool executor for AI coding agents. You pipe it a JSON request, it runs one tool, and it prints a JSON response. It ships as a single binary with no external runtime services.

Installation

Install with Go:

go install github.com/dotcommander/jinn/cmd/jinn@latest

Or build from source:

git clone https://github.com/dotcommander/jinn.git
cd jinn
go build -o jinn ./cmd/jinn/

Verify It Works

jinn --version

You'll see the version derived from your VCS tag or module path. To dump all tool definitions as JSON:

jinn --schema

This prints the full compact OpenAI function-calling schema for every tool jinn exposes. For in-protocol discovery, list_tools returns compact capability metadata by default and includes the schema only when requested.

Your First Tool Call

jinn reads exactly one JSON object of at most 16 MiB from stdin and writes one JSON object to stdout. Duplicate keys, trailing values, unknown fields, invalid types, and unknown tool arguments are rejected. Read a file:

echo '{"tool":"read_file","args":{"path":"go.mod"}}' | jinn

You'll get a response like:

{
  "ok": true,
  "result": "1\tmodule github.com/dotcommander/jinn\n2\t\n3\tgo 1.26.4\n"
}

If something goes wrong, ok is false and the response contains an error field:

{
  "ok": false,
  "error": "file not found: nonexistent.txt"
}

The Protocol

Every request follows this shape:

{
  "tool": "tool_name",
  "args": { }
}

At minimum, every response has one of these shapes:

{"ok": true, "result": "..."}
{"ok": false, "error": "..."}

Some tools add optional fields to the envelope:

Field When present Description
content Structured content responses Typed blocks such as detected images from read_file
meta Structured metadata responses Checksums, truncation details, diffs, stdout/stderr, and compression details
error_code Structured errors Stable error category for programmatic handling
suggestion On structured errors One-sentence next-step hint — read it before retrying
classification run_shell (always) Exit-code class: success, expected_nonzero, error, timeout, signal
risk run_shell (always) Pre-execution risk: safe, caution, dangerous
request_id When supplied by caller Echoes the top-level request ID

Example with extended fields:

{"ok": true, "result": "[exit: 0]\nok\n[classification: success — exit 0]", "risk": "safe", "classification": "success"}

The protocol is one-shot: one JSON request on stdin, one JSON response on stdout. jinn is not a daemon. It starts, handles one request, and exits.

If stdin is a terminal (you run jinn with no pipe), jinn prints a short help message and exits. You must pipe input or redirect from a file.

Integration Patterns

Shell Script

result=$(echo '{"tool":"read_file","args":{"path":"main.go"}}' | jinn)
ok=$(echo "$result" | jq -r '.ok')
if [ "$ok" = "true" ]; then
  echo "$result" | jq -r '.result'
else
  echo "failed: $(echo "$result" | jq -r '.error')"
fi

Python subprocess

import json, subprocess

def jinn(tool: str, args: dict) -> dict:
    req = json.dumps({"tool": tool, "args": args})
    proc = subprocess.run(
        ["jinn"],
        input=req,
        capture_output=True,
        text=True,
        timeout=60,
    )
    return json.loads(proc.stdout)

# Read a file
resp = jinn("read_file", {"path": "go.mod"})
if resp["ok"]:
    print(resp["result"])
else:
    print(f"error: {resp['error']}")

TypeScript (Bun)

async function jinn(tool: string, args: Record<string, unknown>) {
  const proc = Bun.spawn(["jinn"], {
    stdin: new TextEncoder().encode(JSON.stringify({ tool, args })),
    stdout: "pipe",
  });
  return JSON.parse(await new Response(proc.stdout).text());
}

const resp = await jinn("read_file", { path: "go.mod" });
console.log(resp.ok ? resp.result : `error: ${resp.error}`);

Go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os/exec"
)

func jinn(tool string, args map[string]any) (map[string]any, error) {
	req, _ := json.Marshal(map[string]any{"tool": tool, "args": args})
	cmd := exec.Command("jinn")
	cmd.Stdin = bytes.NewReader(req)
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	var resp map[string]any
	if err := json.Unmarshal(out, &resp); err != nil {
		return nil, err
	}
	return resp, nil
}

func main() {
	resp, err := jinn("read_file", map[string]any{"path": "go.mod"})
	if err != nil {
		panic(err)
	}
	fmt.Println(resp["result"])
}

PHP

<?php
function jinn(string $tool, array $args): array {
    $req = json_encode(['tool' => $tool, 'args' => $args]);
    $proc = proc_open('jinn', [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
    ], $pipes);
    fwrite($pipes[0], $req);
    fclose($pipes[0]);
    $out = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    proc_close($proc);
    return json_decode($out, true);
}

$resp = jinn('read_file', ['path' => 'go.mod']);
echo $resp['ok'] ? $resp['result'] : "error: {$resp['error']}";

Shell Loop (Sequential)

while IFS= read -r line; do
  echo "$line" | jinn
done < requests.jsonl

Each line in requests.jsonl is a complete JSON request object. jinn processes them one at a time.

Flags

Flag Description
--schema Print all tool definitions as JSON and exit
--inspect [addr] Start the local browser inspector UI. Defaults to 127.0.0.1:8787
--mcp Start the MCP 2026-07-28 stdio broker in the route-only discovery profile
--mcp-http [addr] Start the stateless MCP 2026-07-28 Streamable HTTP broker at /mcp. Defaults to 127.0.0.1:8788
--mcp-profile=discover|read-only|network Select the MCP profile. read-only adds guarded local inspection; network adds guarded web execution. Requires --mcp or --mcp-http
JINN_MCP_HTTP_TOKEN Optional loopback bearer token, required for non-loopback HTTP binds
JINN_MCP_HTTP_ORIGINS Exact comma-separated HTTP(S) origins, required for non-loopback HTTP binds
--version Print the version and exit
--help, -h Print usage information and exit

Fetch and search the public web

jinn web fetch --json https://example.com
jinn web fetch --json --max-lines=200 https://example.com/article
jinn web search --provider=exa --include-domain=go.dev "context cancellation"

These subcommands use stdlib flags with human output and flat --json envelopes. --max-bytes and --max-lines add an explicit human truncation marker or JSON byte/line totals and truncation fields. Reader/cache/render settings use JINN_WEB_*, including JINN_WEB_CHROME_PATH; provider credentials use JINA_API_KEY, BRAVE_API_KEY, or EXA_API_KEY. The default cache directory is $XDG_CACHE_HOME/jinn/web/urls, falling back to ~/.cache/jinn/web/urls.

web_fetch and web_search are available to normal one-shot discovery. Native web_fetch accepts format=markdown|headings|links, zero-based start_line, max_bytes, and max_lines; continuation and truncation details are returned in snake-case metadata. They are network-risk tools, excluded from read-only, and rejected from every run_plan phase. Use --mcp-profile=network only when the MCP host is allowed to make public network requests.

MCP Discovery Broker

jinn --mcp uses the official Go MCP SDK and newline-delimited JSON-RPC over stdin/stdout. The current protocol is 2026-07-28 and uses stateless requests, not an initialize handshake. Each request carries the protocol version and client capabilities under _meta.

When an MCP host accepts only an executable path, it may launch jinn without arguments. Jinn auto-detects MCP JSON-RPC on piped stdin and otherwise retains the one-shot {tool,args} protocol. Interactive jinn still prints help.

The default broker exposes one MCP tool, jinn_route. The tool is recommendation-only: it does not execute read_file, run_shell, or any other jinn tool. It maps a natural-language need to the most relevant existing jinn tools, risk/mutation notes, and optional lean schemas for only the matched tools. Keeping one tool in the default MCP surface avoids prompt bloat from listing all 21 executor schemas.

For agents that need bounded inspection without granting mutation or shell access, use the opt-in read-only profile:

{
  "mcpServers": {
    "jinn-read-only": {
      "command": "jinn",
      "args": ["--mcp-profile=read-only", "--mcp"]
    }
  }
}

This profile exposes jinn_route and jinn_call. jinn_call dispatches only the canonical read-only tool allowlist, while file/state mutation, shell execution, memory, and undo are rejected before dispatch. It also forces shell execution off. The profile uses the current stateless 2026-07-28 request metadata; older initialize-based clients remain on the route-only compatibility path.

The network profile retains only these two MCP tools and adds web_fetch and web_search through jinn_call. Those requests leave the machine and may consume provider quota.

Use the built-in explorer for local fakes, stdio subprocesses, or HTTP(S):

jinn mcp list ENDPOINT
jinn mcp inspect --command jinn --arg --mcp jinn_route
jinn mcp call ENDPOINT jinn_route -a need '"read Go files"'

The deadline defaults to 30s; bearer auth defaults from JINN_MCP_HTTP_TOKEN. Tool isError is a successful explorer response; transport/protocol failures use Jinn's normal nonzero error envelope.

MCP Streamable HTTP

Use the separate opt-in flag when the client connects to a URL instead of a stdio process:

jinn --mcp-http
# POST http://127.0.0.1:8788/mcp

The default HTTP profile exposes only jinn_route. Add --mcp-profile=read-only to expose the same guarded jinn_call profile described above. HTTP requests are stateless and use one JSON-RPC request per POST. They must include Accept: application/json, text/event-stream, Content-Type: application/json, MCP-Protocol-Version: 2026-07-28, and Mcp-Method; tools/call also needs Mcp-Name.

Loopback binds work without environment controls. Any non-loopback bind is rejected unless both JINN_MCP_HTTP_TOKEN and JINN_MCP_HTTP_ORIGINS are set. The token is never placed in CLI arguments or startup output, and the origin list uses exact HTTP(S) origins:

JINN_MCP_HTTP_TOKEN="$TOKEN" \
JINN_MCP_HTTP_ORIGINS="https://agent.example.com" \
jinn --mcp-profile=read-only --mcp-http 0.0.0.0:8788

Use a non-loopback bind only on a trusted network or behind a TLS-terminating proxy or tunnel: bearer tokens authenticate requests but do not encrypt them.

HTTP deliberately does not accept the stdio legacy initialize handshake. See mcp-smoke-test.md for a real subprocess check of the endpoint, headers, auth, origin, and shutdown contracts.

For migration safety, the default stdio profile detects a first older initialize request and serves the legacy compatibility path. The read-only profile deliberately leaves legacy traffic to the current SDK, which rejects it before any route or tool dispatch. Current clients should use the 2026-07-28 request shape shown in tool-reference.md.

Example client config:

{
  "mcpServers": {
    "jinn": {
      "command": "jinn",
      "args": ["--mcp"]
    }
  }
}

Routing is deterministic and local; weak matches are dropped rather than guessed, and a vague need returns a corrective note. See tool-reference.md for the input table, a captured response, and need phrasing guidance.

Persistent Memory

The memory tool stores key/value pairs across jinn invocations in a SQLite database at ~/Library/Application Support/jinn/memory.db on macOS (~/.config/jinn/memory.db on Linux; override base dir with JINN_CONFIG_DIR), scoped per project:

# Save a value
echo '{"tool":"memory","args":{"action":"save","key":"db_url","value":"postgres://localhost/myapp"}}' | jinn

# Recall it later
echo '{"tool":"memory","args":{"action":"recall","key":"db_url"}}' | jinn

# List all keys
echo '{"tool":"memory","args":{"action":"list"}}' | jinn

# Save to the cross-project "global" scope
echo '{"tool":"memory","args":{"action":"save","key":"editor","value":"nvim","scope":"global"}}' | jinn

By default, keys are scoped to the current project (auto-detected from the nearest .git ancestor). Pass scope: "global" for a cross-project bucket, scope: "project" with scope_id for an explicit project path, or scope: "task" / scope: "agent" with a caller-supplied scope_id.

Keys must match [a-zA-Z0-9_.-] (max 128 chars). Values are capped at 16 KiB. Saved entries can also carry kind: "fact" | "directive" | "lesson", pin: true, and expires_in durations such as "12h", "7d", or "2w".

# Include values and metadata when listing
echo '{"tool":"memory","args":{"action":"list","include_values":true}}' | jinn

# Save a task-scoped entry that expires
echo '{"tool":"memory","args":{"action":"save","key":"backfill.lesson","value":"retry failed rows after the second pass","kind":"lesson","scope":"task","scope_id":"backfill-2026-06","expires_in":"14d"}}' | jinn

# Remove expired, unpinned memories and old idempotency rows
echo '{"tool":"memory","args":{"action":"gc"}}' | jinn

Language Server Queries

The lsp_query tool connects to a running language server to answer semantic questions about source code. The server is auto-selected from the file extension:

# Jump to definition at line 12, character 5
echo '{"tool":"lsp_query","args":{"action":"definition","path":"main.go","line":12,"character":5}}' | jinn

# List all symbols in a file
echo '{"tool":"lsp_query","args":{"action":"symbols","path":"internal/jinn/engine.go"}}' | jinn

# Pull diagnostics for a file
echo '{"tool":"lsp_query","args":{"action":"diagnostics","path":"main.go"}}' | jinn

Supported actions: definition, references, hover, symbols, diagnostics, and rename preview. Supported extensions include Go, Rust, Python, TypeScript/JavaScript, C/C++, Java, Lua, and Zig. The server binary must be on PATH; if missing, the response includes a suggestion with the install command.

What's Next

  • Harness Integrations -- recipes for Claude Code, Codex CLI, and custom agent loops
  • Tool Reference -- every tool with full parameter tables and examples
  • Security -- rooted confinement, mutation preconditions, shell modes, and the risk classifier