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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ cmd/odek/
file_tool.go Built-in file tools (read_file, write_file, search_files, patch, batch_read, glob, file_info)
external_ref.go --external-ref flag parsing (run + continue) → session.ExternalRef
perf_tools.go Performance/parallelism tools (batch_patch, parallel_shell, http_batch, math_eval, diff,
count_lines, multi_grep, json_query, tree, checksum, sort, head_tail, base64, tr, word_count)
multi_grep, json_query, tree, checksum, head_tail, base64)
mcp.go MCP server implementation (stdio transport)
mcp_approval.go Per-tool MCP server approval UI and persistence (key hashes limits/artifact_roots)
project_sandbox_approval.go Project-level sandbox config approval gate
Expand Down Expand Up @@ -129,7 +129,7 @@ ReAct cycle: observe → think → act → repeat.
- **Execution budgets** — `limits` config section + `--max-runtime/--max-tool-calls/--max-input-tokens/--max-output-tokens/--max-cost-usd` on `run`; typed `budget.Error` → CLI exit code 4; session persisted before return. Per-model prices via `limits.model_prices` with flat-pair fallback; cost enforcement only when cap + prices configured. `odek init --global` scaffolds the section (zeros = off). `GET /api/limits` on serve exposes limits + effective prices for cost rendering.

### Tools
All built-in tools with zero subprocess forks: batch_read, batch_patch, parallel_shell, http_batch, math_eval, diff, count_lines, multi_grep, json_query, tree, checksum, sort, head_tail, base64, tr, word_count, transcribe, browser, read_file, write_file, search_files, patch, shell, delegate_tasks, session_search, config_view, list_tools.
All built-in tools with zero subprocess forks: batch_read, batch_patch, parallel_shell, http_batch, math_eval, diff, multi_grep, json_query, tree, checksum, head_tail, base64, transcribe, browser, read_file, write_file, search_files, patch, shell, delegate_tasks, session_search, config_view, list_tools.

### Terminal Rendering (`internal/render/`)
Vertical space compression is baked into the render paths; blank lines removed from Iteration/FinalAnswer/Summary. Raw-mode cursor uses `\r\n` for cross-platform compatibility.
Expand Down
15 changes: 3 additions & 12 deletions cmd/odek/bg_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,18 +332,9 @@ type bgStartTool struct {
func (t *bgStartTool) Name() string { return "bg_start" }

func (t *bgStartTool) Description() string {
return `Start a shell command in the background and return immediately.
Use for long-running work: builds, full test suites, dev servers, watchers, fuzz runs, batch jobs.
The command runs detached from the conversation: you keep working while it
runs. Completion is delivered automatically: the exit notice is injected into
a later iteration of a running turn, and when the turn has ended the client
wakes the session on job completion (or the notice is delivered on the next
turn). Never call sleep or otherwise pause to wait for a job — it blocks the
loop for nothing. Poll with bg_status / bg_output only if notices are
disabled or your current turn depends on the result.
timeout_seconds: optional kill timer; 0 or absent = run until session end
(operator cap may clamp explicit values). Jobs are killed when the session
or the process ends. Output is capped; retrieve it with bg_output.`
return `Start a shell command in the background and return immediately — for long-running work (builds, full test suites, dev servers, watchers).
Completion is delivered automatically (notice injected into a running turn; the session wakes on job completion when the turn has ended) — never call sleep to wait for a job. Poll with bg_status / bg_output only if your current turn depends on the result.
timeout_seconds: optional kill timer; 0/absent = run until session end. Output is capped; retrieve with bg_output.`
}

func (t *bgStartTool) Schema() any {
Expand Down
11 changes: 2 additions & 9 deletions cmd/odek/browser_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,8 @@ func (t *browserTool) checkRedirect(req *http.Request, via []*http.Request) erro
func (t *browserTool) Name() string { return "browser" }

func (t *browserTool) Description() string {
return `Navigate and interact with web pages. Four actions:

navigate — Fetch a URL and extract page content + interactive elements
snapshot — Return the current page's text view with ref IDs for elements
click — Follow a link or interact with an element by ref ID
back — Return to the previous page in navigation history

Typical flow: navigate(url), then snapshot() to get element ref IDs (e.g. @e1), then click(ref).
Note: regex-based HTML parsing with NO JavaScript execution. Best for server-rendered HTML pages; SPAs and JS-heavy sites may return limited content.`
return `Navigate and interact with web pages: navigate (fetch URL + extract content/elements), snapshot (text view with element ref IDs), click(ref), back. Typical flow: navigate(url) → snapshot() → click(@ref).
Regex-based HTML parsing, NO JavaScript execution — best for server-rendered pages; SPAs may return limited content.`
}

// browserArgs holds all possible parameters for the browser tool.
Expand Down
30 changes: 3 additions & 27 deletions cmd/odek/bughunt_v3_fixes_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package main

// RED-first tests for the bughunt-v3 perf/file tool fixes:
// 1. tr string transform unbounded expansion
// 2. base64 decode unwrapped output
// 3. search/multi_grep silently skipping unopenable files (fd pressure)
// 4. unwrapped FS-derived match paths (glob, searchFiles, multiGrep)
// 1. base64 decode unwrapped output
// 2. search/multi_grep silently skipping unopenable files (fd pressure)
// 3. unwrapped FS-derived match paths (glob, searchFiles, multiGrep)

import (
"fmt"
Expand All @@ -14,29 +13,6 @@ import (
"testing"
)

// 1. tr: a small input plus a large replacement must be rejected, not expanded.
func TestTr_RejectsUnboundedStringExpansion(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("permission-based test unreliable as root")
}
big := strings.Repeat("x", 3<<20) // 3 MiB replacement, 4 occurrences → ~12 MiB
tool := &trTool{}
args := fmt.Sprintf(`{"content":"aaaa","transformations":[{"type":"string","from":"a","to":%q}]}`, big)
result := callJSON(t, tool, args)

var r struct {
Result string `json:"result"`
Error string `json:"error"`
}
mustUnmarshal(t, result, &r)
if r.Error == "" {
t.Fatalf("expected expansion-cap error, got result of %d bytes", len(r.Result))
}
if len(r.Result) > 1<<20 {
t.Errorf("result should not contain the expanded output")
}
}

// 2. base64: decoded strings cross the trust boundary like every other
// tool output and must be wrapped.
func TestBase64_WrapsDecodedString(t *testing.T) {
Expand Down
10 changes: 1 addition & 9 deletions cmd/odek/file_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -1445,15 +1445,7 @@ func (t *batchReadTool) CallContext(ctx context.Context, args string) (string, e
}

func (t *batchReadTool) Description() string {
return `Read multiple files in a single call. Files are read in parallel and results are returned as an array.
Each file entry supports offset and limit for pagination (same as read_file).
Use this when you need to read several files at once — it's faster than N sequential read_file calls.

Returns an array of results, one per file, each with:
path — the file path requested
content — file content with line numbers (or truncated by offset/limit)
total_lines — total lines in the file
error — error message if the file couldn't be read (file not found, binary, etc.)`
return `Read up to 10 files in one parallel call — faster than N sequential read_file calls. Each entry supports offset/limit pagination (same as read_file) and returns {path, content, total_lines, error}.`
}

type batchReadFileArg struct {
Expand Down
4 changes: 0 additions & 4 deletions cmd/odek/file_tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2614,13 +2614,9 @@ func TestRED_SetupSandbox_ConfinesHostReadTools(t *testing.T) {
&multiGrepTool{},
&jsonQueryTool{},
&diffTool{},
&countLinesTool{},
&treeTool{},
&checksumTool{},
&sortTool{},
&wordCountTool{},
&base64Tool{},
&trTool{},
&visionTool{},
&transcribeTool{},
}
Expand Down
22 changes: 2 additions & 20 deletions cmd/odek/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ Think of the best Chief of Staff a founder could have, fused with a Principal-gr
· "write_file" NOT "echo", "tee", "cat heredoc"
· "patch" NOT "sed", "awk"

The perf tools (batch_read, batch_patch, parallel_shell, http_batch, math_eval, diff, multi_grep, json_query, tree, checksum, head_tail, base64) are pure-Go, zero-subprocess implementations of their shell equivalents — prefer them over shell for file inspection and transformation; they run without forks, approval friction, or output-pipeline risk.

One wrong name wastes an entire iteration. Be precise.

## Search performance — cost scales with file count
Expand Down Expand Up @@ -2390,8 +2392,6 @@ func applySandboxToolBindings(tools []odek.Tool, containerName string) {
tool.restrictToCWD = true
case *diffTool:
tool.restrictToCWD = true
case *countLinesTool:
tool.restrictToCWD = true
case *multiGrepTool:
tool.restrictToCWD = true
case *jsonQueryTool:
Expand All @@ -2400,16 +2400,10 @@ func applySandboxToolBindings(tools []odek.Tool, containerName string) {
tool.restrictToCWD = true
case *checksumTool:
tool.restrictToCWD = true
case *sortTool:
tool.restrictToCWD = true
case *headTailTool:
tool.restrictToCWD = true
case *base64Tool:
tool.restrictToCWD = true
case *trTool:
tool.restrictToCWD = true
case *wordCountTool:
tool.restrictToCWD = true
case *visionTool:
tool.restrictToCWD = true
case *transcribeTool:
Expand Down Expand Up @@ -2438,8 +2432,6 @@ func toolRestrictsToCWD(t odek.Tool) bool {
return tool.restrictToCWD
case *diffTool:
return tool.restrictToCWD
case *countLinesTool:
return tool.restrictToCWD
case *multiGrepTool:
return tool.restrictToCWD
case *jsonQueryTool:
Expand All @@ -2448,16 +2440,10 @@ func toolRestrictsToCWD(t odek.Tool) bool {
return tool.restrictToCWD
case *checksumTool:
return tool.restrictToCWD
case *sortTool:
return tool.restrictToCWD
case *headTailTool:
return tool.restrictToCWD
case *base64Tool:
return tool.restrictToCWD
case *trTool:
return tool.restrictToCWD
case *wordCountTool:
return tool.restrictToCWD
case *visionTool:
return tool.restrictToCWD
case *transcribeTool:
Expand Down Expand Up @@ -2617,16 +2603,12 @@ func builtinTools(dc danger.DangerousConfig, sm *skills.SkillManager, approver d
newHTTPBatchTool(dc),
&mathEvalTool{},
&diffTool{dangerousConfig: dc},
&countLinesTool{dangerousConfig: dc},
&multiGrepTool{dangerousConfig: dc},
&jsonQueryTool{dangerousConfig: dc},
&treeTool{dangerousConfig: dc},
&checksumTool{dangerousConfig: dc},
&sortTool{dangerousConfig: dc},
&headTailTool{dangerousConfig: dc},
&base64Tool{dangerousConfig: dc},
&trTool{dangerousConfig: dc},
&wordCountTool{dangerousConfig: dc},
newTranscribeTool(dc, tcfg.Transcription),
newVisionTool(dc, tcfg.Vision),
// session_search returns content from arbitrary past sessions —
Expand Down
37 changes: 0 additions & 37 deletions cmd/odek/native_outcomes.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ func (r diffResult) nativeError() string { return r.Error }
func (r jsonQueryResult) nativeError() string { return r.Error }
func (r treeResult) nativeError() string { return r.Error }
func (r base64Result) nativeError() string { return r.Error }
func (r trResult) nativeError() string { return r.Error }
func (r batchReadResult) nativeError() string {
failed := 0
for _, entry := range r.Results {
Expand Down Expand Up @@ -66,18 +65,6 @@ func (r httpBatchResult) nativeError() string {
}
return fmt.Sprintf("%d of %d operations failed", failed, len(r.Results))
}
func (r countLinesResult) nativeError() string {
failed := 0
for _, entry := range r.Results {
if entry.Error != "" {
failed++
}
}
if failed == 0 {
return ""
}
return fmt.Sprintf("%d of %d operations failed", failed, len(r.Results))
}
func (r multiGrepResult) nativeError() string {
failed := 0
for _, entry := range r.Results {
Expand All @@ -102,18 +89,6 @@ func (r checksumResult) nativeError() string {
}
return fmt.Sprintf("%d of %d operations failed", failed, len(r.Results))
}
func (r sortResult) nativeError() string {
failed := 0
for _, entry := range r.Results {
if entry.Error != "" {
failed++
}
}
if failed == 0 {
return ""
}
return fmt.Sprintf("%d of %d operations failed", failed, len(r.Results))
}
func (r headTailResult) nativeError() string {
failed := 0
for _, entry := range r.Results {
Expand All @@ -126,18 +101,6 @@ func (r headTailResult) nativeError() string {
}
return fmt.Sprintf("%d of %d operations failed", failed, len(r.Results))
}
func (r wordCountResult) nativeError() string {
failed := 0
for _, entry := range r.Results {
if entry.Error != "" {
failed++
}
}
if failed == 0 {
return ""
}
return fmt.Sprintf("%d of %d operations failed", failed, len(r.Results))
}

func (r visionResult) nativeError() string { return r.Error }
func (r webSearchOutput) nativeError() string { return r.Error }
Expand Down
32 changes: 0 additions & 32 deletions cmd/odek/next_security_vulnerabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,38 +496,6 @@ func TestDiff_WrapsContent(t *testing.T) {
}
}

func TestSort_WrapsContent(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
os.WriteFile(path, []byte("zebra\napple\n"), 0644)

tool := &sortTool{dangerousConfig: danger.DangerousConfig{}}
result := callJSON(t, tool, fmt.Sprintf(`{"path":%q}`, path))
var r struct {
Output string `json:"output"`
}
mustUnmarshal(t, result, &r)
if !strings.HasPrefix(r.Output, "<untrusted_content_") {
t.Fatalf("sort output should be wrapped in untrusted_content, got: %q", r.Output)
}
}

func TestTr_WrapsContent(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
os.WriteFile(path, []byte("hello\n"), 0644)

tool := &trTool{dangerousConfig: danger.DangerousConfig{}}
result := callJSON(t, tool, fmt.Sprintf(`{"path":%q,"transformations":[{"type":"upper"}]}`, path))
var r struct {
Result string `json:"result"`
}
mustUnmarshal(t, result, &r)
if !strings.HasPrefix(r.Result, "<untrusted_content_") {
t.Fatalf("tr result should be wrapped in untrusted_content, got: %q", r.Result)
}
}

func TestJsonQuery_WrapsStringValue(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.json")
Expand Down
Loading
Loading