-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent.toml.example
More file actions
752 lines (680 loc) · 39.9 KB
/
Copy pathagent.toml.example
File metadata and controls
752 lines (680 loc) · 39.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
# agent.toml.example — copy to agent.toml and uncomment what you need.
#
# Model resolution order:
# 1. [models] role assignments (default / summarizer / embeddings)
# 2. Built-in defaults (localhost:8080/v1)
#
# CLI overrides:
# agent chat --model <name-or-id>
# agent commit --model <name-or-id> # primary model
# agent commit --summarizer-model <entry> # chunked-diff summarizer only
#
# Env overrides:
# AGENT_MODEL_ROLE_DEFAULT=<entry-name>
# AGENT_MODEL_ROLE_SUMMARIZER=<entry-name>
# AGENT_MODEL_ROLE_EMBEDDINGS=<entry-name>
# AGENT_LLM_BASE_URL / AGENT_LLM_API_KEY / AGENT_LLM_MODEL
# AGENT_EMBEDDINGS_BASE_URL / AGENT_EMBEDDINGS_MODEL
# ── Role assignments ──────────────────────────────────────────────────────────
# Point each role at a named [models.<entry>] below.
[models]
default = "local-coder" # primary coding model (chat, run, commit final msg)
summarizer = "deepseek-r1" # used for commit chunked summaries, Q/A summaries
embeddings = "nomic-local" # used for RAG
# ── Per-purpose model matrix (optional) ──────────────────────────────────────
# Pin any purpose to a named entry. Unset roles fall back to `default` (or to the
# free-cloud background picker for background/namer/compaction). Switch live with
# /model <role>=<entry> e.g. /model review=cerebras
# and view the full matrix with `/model` (no arg). Known roles:
# background = "cerebras" # idle work: session naming, summaries, compaction
# # (unset → auto free-cloud when model_mode allows)
# namer = "groq" # session auto-naming only
# compaction = "groq" # context compaction only
# review = "cerebras" # /security review deep-read audit
# triage = "groq" # /security triage ranking
# verify = "local-coder" # /security verify PoC generation
# evolve = "cerebras" # security KB self-evolution
# commit = "mistral-codestral" # `agent commit` final message model
[models.local-coder]
base_url = "http://localhost:8080/v1"
api_key = "local"
model = "qwen3-coder-30b"
ctx_window = 32768
max_output_tokens = 8192
temperature = 0.7
tags = ["coding"]
# intelligence_index = 52.0 # optional: from OpenRouter rankings (0 = not rated)
# coding_index = 68.0
# agentic_index = 41.0
[models.local-fast]
base_url = "http://localhost:8080/v1"
api_key = "local"
model = "qwen3-8b"
ctx_window = 16384
max_output_tokens = 4096
temperature = 0.7
tags = ["fast", "summarizer"]
[models.nomic-local]
base_url = "http://localhost:8080/v1"
api_key = "local"
model = "nomic-embed-text"
dimensions = 768
tags = ["embeddings"]
# ── DeepSeek (online API) ─────────────────────────────────────────────────────
# Sign up at https://platform.deepseek.com — get api_key from the dashboard.
[models.deepseek-r1]
base_url = "https://api.deepseek.com/v1"
api_key = "sk-..." # replace with your DeepSeek API key
model = "deepseek-r1"
ctx_window = 65536
max_output_tokens = 8192
temperature = 0.6
tags = ["reasoning", "summarizer"]
tier = "paid"
intelligence_index = 85.0 # from OpenRouter rankings; 0 = not rated
coding_index = 80.0
agentic_index = 61.0
[models.deepseek-v4-preview]
base_url = "https://api.deepseek.com/v1"
api_key = "sk-..." # replace with your DeepSeek API key
model = "deepseek-v4-preview"
ctx_window = 65536
max_output_tokens = 8192
temperature = 0.7
tags = ["coding", "reasoning"]
tier = "paid"
# ── SmartAIPI ─────────────────────────────────────────────────────────────────
# Fill in base_url and api_key from your SmartAIPI account.
[models.smartaipi]
base_url = "" # e.g. "https://api.smartaipi.com/v1"
api_key = ""
model = "" # model id from provider
ctx_window = 32768
max_output_tokens = 4096
temperature = 0.7
tags = []
# ── OpenAI-compatible catch-all ───────────────────────────────────────────────
[models.openai-gpt4o]
base_url = "https://api.openai.com/v1"
api_key = "sk-..."
model = "gpt-4o"
ctx_window = 128000
max_output_tokens = 4096
temperature = 0.7
tags = ["coding"]
tier = "paid" # priced provider — set cost_*_per_1k or tier="paid"
# so model_mode classifies it correctly.
# ── Subscription-bundled (OpenAI-compatible) ──────────────────────────────────
# A cloud model covered by a flat subscription (e.g. Claude Max, Copilot): billed
# but NOT metered per token. Mark it tier = "bundled" so it shows as its own
# bucket in the per-round model-call breakdown and `/modelcalls` (and isn't
# mis-counted as free or paid). "bundled" is never auto-derived — set it here.
[models.bundled-sub]
base_url = "https://api.example-sub.com/v1"
api_key = "..."
model = "sub-model"
ctx_window = 200000
max_output_tokens = 4096
temperature = 0.7
tags = ["coding"]
tier = "bundled"
# ── Models the endpoint serves but does not advertise ─────────────────────────
# Availability is judged by an endpoint's /models catalog: an entry whose id is
# missing from it is treated as offline and drops out of the tier ladder and
# failover. Some endpoints serve ids they never publish — dated preview aliases
# (DeepSeek's "…-expires-on-MMDD"), private deployments, gateways with a curated
# catalog. Set assume_available on those entries: the endpoint must still answer
# and not be rate-limited, only the catalog match is waived.
[models.unlisted-preview]
base_url = "https://api.deepseek.com"
api_key = "sk-..."
model = "deepseek-v4.1-flash-expires-on-0910"
assume_available = true
tier = "paid"
# ── Free cloud providers (OpenAI-compatible) ──────────────────────────────────
# Daily/monthly free tiers. Get keys from each provider's dashboard. With
# model_mode = "free-hybrid" (default for offloading), idle/background work and
# spawn_agents auto-route to these, sparing the local model for the main thread.
# All free-tier (no per-token cost) → tier auto-classifies as "free".
# Cerebras — 1M tokens/day, 30 RPM, 14.4K RPD. Best overall free tier.
# https://cloud.cerebras.ai
[models.cerebras]
base_url = "https://api.cerebras.ai/v1"
api_key = "csk-..." # replace with your Cerebras key
model = "llama-3.3-70b" # or "qwen-3-235b-a22b"
ctx_window = 65536
max_output_tokens = 8192
temperature = 0.7
tags = ["coding", "reasoning", "background", "summarizer"]
intelligence_index = 74.0
coding_index = 70.0
# Groq — 6K tokens/min, 30 RPM, 14.4K RPD. Fastest free tier.
# https://console.groq.com
[models.groq]
base_url = "https://api.groq.com/openai/v1"
api_key = "gsk_..." # replace with your Groq key
model = "llama-3.3-70b-versatile" # or "gemma2-9b-it"
ctx_window = 32768
max_output_tokens = 8192
temperature = 0.7
tokens_per_sec = 750.0 # very fast; good for background sweeps
tags = ["fast", "background", "summarizer"]
# Mistral AI — ~1B tokens/month (~33M/day), 2 RPM. Best free for code (Codestral).
# https://console.mistral.ai
[models.mistral-codestral]
base_url = "https://api.mistral.ai/v1"
api_key = "..." # replace with your Mistral key
model = "codestral-latest"
ctx_window = 32768
max_output_tokens = 8192
temperature = 0.4
tags = ["coding"]
coding_index = 72.0
# OpenRouter free — 50 RPD. Too restrictive alone; useful as a fallback.
# https://openrouter.ai/keys (models with a ":free" suffix are free-tier)
[models.openrouter-free]
base_url = "https://openrouter.ai/api/v1"
api_key = "sk-or-..." # replace with your OpenRouter key
model = "deepseek/deepseek-r1:free"
ctx_window = 65536
max_output_tokens = 8192
temperature = 0.7
tags = ["reasoning", "background"]
# ── Per-turn model tiering (effort ladder) ───────────────────────────────────
# Rank chat entries weakest→strongest by "power" and pick per turn. Power comes
# from declared benchmark indices on each [models.*] entry (intelligence_index /
# coding_index / agentic_index, OpenRouter-style 0–100) or, if unset, from
# params_b (model size in billions). Entries with neither sort weakest — declare
# at least params_b for sane ordering. Only live endpoints are considered
# (per-endpoint /models probe, cached ~60s). Mid-turn failure signals
# (confidence guard, loop guard, failed [verify]) climb one rung; a fresh turn
# re-decides, so escalation never sticks.
#
# Picking a model by hand with `/model <entry>` pins it: auto-tier then stands
# down — no per-turn pick, no mid-turn escalation — until `/model auto` or an
# `/effort` change hands control back. Without that, a hand-picked model lasted
# exactly one turn. A `default = "<entry>"` pin written here is not a pin in
# that sense: it is the starting point auto-tier refines.
#
# [auto_tier]
# enabled = true
# ladder = true # power-ladder mode (false = legacy fast/strong pair)
# effort = "smart" # quick | smart | deep — switch live with /effort or
# # the header chip in the textual UI:
# # quick — weakest live model (fast simple answers)
# # smart — predict per turn from the prompt (default)
# # deep — strongest live model (complex work)
# min_prompt_chars = 600 # smart: prompt at/above this length counts as deep
# escalate_on_code = true # smart: fenced code block counts as deep
# ── Agent runtime behavior ────────────────────────────────────────────────────
[agent]
mode = "fast" # "fast" | "ultrasecure"
# fast: web_search/web_fetch callable directly.
# ultrasecure: internet stripped from the main
# agent; it reaches the net only via the
# ask_internet broker, which spawns a disposable
# subagent (assumed compromised) holding the web
# tools and returns sanitized, LLM-re-emitted
# {answer,sources,quotes}. Requires
# web_search.enabled = true. CLI: --ultrasecure.
# Optional: map a separate model for the broker
# subagent via [models] internet = "<entry>".
model_mode = "any" # which cost tiers auto-selection may use:
# local-only — only local models (no egress)
# lan-only — only own LAN box(es): entries on a
# private-IP host (192.168.x, 10.x,
# 172.16-31.x). Matches on endpoint
# LOCATION, not cost. Embeddings are
# exempt — a localhost embed server is
# still used when it answers.
# free-cloud — only free cloud providers
# free-hybrid — local + free cloud (offload idle
# work to free cloud, keep local
# model for the main thread)
# paid-cloud — only paid cloud (force quality)
# any — local + free + paid
# Affects AUTOMATIC picks only (startup pool walk,
# idle/background, failover, spawn_agents): allowed
# entries are tried first, disallowed ones stay as a
# last-resort fallback. A pinned [models] default (or
# --model) always wins. Switch live with /mode — that
# re-pins the default role onto a live allowed
# endpoint. Env: AGENT_MODEL_MODE.
max_iterations = 10 # max tool-call rounds per user turn
compaction_threshold = 0.75 # compact context when usage exceeds this fraction
compaction_message_threshold = 15 # also compact after this many messages
narration_fallback = true # emit narration text if model skips tool-call
auto_detect_ctx = true # query server for actual context size on startup
think_level = "normal" # "off" | "low" | "normal" | "medium" | "high" | "max"
stream_stall_seconds = 90 # mid-stream gap (after first token) before declaring a wedge (0 = off)
stream_ttft_adaptive = true # derive the first-token fuse from this endpoint's own measured
# prefill history, scaled to the prompt actually being sent
# (metrics/ttft_expect.py: robust fit of ttft against prompt size,
# fuse at 2× the p90). Needs ~8 samples at a comparable prompt
# size; until then, and for prompts far larger than anything
# measured, stream_ttft_seconds below is used unchanged.
# The heartbeat line then reads "40s of 190s" so the wait is
# judged against what THIS model normally needs.
stream_ttft_seconds = 600 # wait for FIRST token before declaring a wedge; must exceed worst-case
# prefill on a big prompt, which emits no chunks (0 = off)
stream_heartbeat_seconds = 20 # emit a progress heartbeat this often while a stream is quiet (0 = off)
stream_stall_retries = 1 # retries after a stall before giving up
transport_retries = 1 # retries on a dropped socket / request timeout before the endpoint is
# written off (cooldown + failover). A transport error is not evidence
# the endpoint is down: the client may have given up mid-generation
# and the server keep going. The SDK's own retries are off, so this
# replaces them. 0 = mark the endpoint down on the first error.
show_output_cap = false # append the active model's output-token cap to the
# END of the system prompt (so toggling it does not
# invalidate the cacheable prefix). Helps models that
# would otherwise emit an oversized single tool call
# and hit the cap mid-JSON. Off by default.
# ── RAG ───────────────────────────────────────────────────────────────────────
[rag]
db_path = ".agent/index.db"
archive_db_path = ".agent/index-archive.db"
archive_ttl_days = 30
chunk_min_tokens = 20
chunk_max_tokens = 400
top_k = 8
hybrid = true
index_text_files = true # also index generic text files: .md, .toml, .example,
# .template, Makefile, unknown extensions that sniff as
# text. false = code extensions only (old behavior)
text_max_bytes = 262144 # per-file cap for generic text files (code files exempt)
# ── Tools ─────────────────────────────────────────────────────────────────────
[tools]
allow_shell = true
shell_timeout = 30
working_dir = "."
agent_dir = ".agent"
preamble_path = ".agent/agent.preamble"
search_parents = true
# Unbounded read_file serves a file whole when it is within BOTH limits:
read_full_max_lines = 200
read_full_max_chars = 12000
# ...or, for bigger files, while estimated tokens <= headroom * fraction
# (context mostly empty), capped at hard_max_chars. 0 disables.
read_full_headroom_fraction = 0.25
read_full_hard_max_chars = 100000
# Ranged reads (start_line/end_line) are capped by characters too.
read_range_max_chars = 16000
# ── UI ────────────────────────────────────────────────────────────────────────
[ui]
mode = "textual" # "textual" | "simple"
syntax_highlight = true
show_token_count = true
q_summaries = true # one-line Q/A summaries (uses summarizer model)
chat_wrap = "last used" # "wrap" | "nowrap" | "last used"
round_summary = true # show gray Q/A summary line after each turn
reasoning_fold = "end_of_round" # "immediate" | "end_of_round" | "never"
show_active_models = "auto" # status-bar live model breakdown: "auto" (when >1 model or subagents run) | "always" | "off"
# allowed_hosts = ["192.168.31.42"] # extra Origin/Host names the HTTP UI accepts
# # beyond loopback (LAN access); also --allow-host
[ui.changeset]
enabled = true
inline_max_files = 3
inline_max_lines = 80
list_max_files = 50
max_diff_bytes = 262144
max_total_bytes = 4194304
context_lines = 3
fold_journal = "on_next_round" # "on_next_round" | "immediately" | "never"
session_rollup = true
prose_summary = "off" # "off" | "background" | "always"
[ui.theme]
bg = "#0C0C0C"
panel_bg = "#141414"
panel_bg_dark = "#0E0E0E"
chat_bg = "#111111"
chat_bg_focus = "#191919"
input_bg = "#111111"
input_bg_focus = "#191919"
border = "#2E7D32"
active = "#E8801A"
scrollbar_bg = "#1C1C1C"
scrollbar_thumb = "#3A3A3A"
text = "#C0C0C0"
text_dim = "#505050"
user_color = "#388E3C"
agent_color = "#E8801A"
tool_color = "#505050"
cmd_color = "#388E3C"
prompt = "#388E3C"
success = "#388E3C"
warning = "#F9A825"
error = "#C62828"
thinking_color = "#404060"
# ── Token limits ──────────────────────────────────────────────────────────────
# Raise these if a reasoning model burns its budget on hidden <think> tokens.
[token_limits]
asm_splitter = 256
asm_describer = 512
commit_message = 4096
commit_message_max_tokens = 16384
commit_message_reserved = 512
commit_chunk_chars = 12000
commit_summary_tokens = 1024
prompt_compile_min = 2048
compactor_analyze_min = 2048
compactor_synthesize_initial = 2048
compactor_synthesize_retry = 4096
# ── Logs ──────────────────────────────────────────────────────────────────────
[logs]
level = "DEBUG" # file log level
stderr_level = "WARNING" # terminal log level
max_bytes = 20971520 # 20 MB per file
backup_count = 5
dedupe_preamble = true
[logs.sources]
"httpcore" = "WARNING"
"httpcore.http11" = "WARNING"
"httpcore.connection" = "WARNING"
"httpx" = "WARNING"
"openai._base_client" = "INFO"
"asyncio" = "INFO"
# ── Loop guard ────────────────────────────────────────────────────────────────
[loop_guard]
enabled = true
window = 10
repeat_threshold = 3
per_tool_threshold = { list_files = 6, read_file = 5, search_code = 5 }
# ── ASM analysis ──────────────────────────────────────────────────────────────
[asm_analysis]
enabled = false
splitter_ctx_tokens = 8192
splitter_overlap_lines = 20
describer_model = "" # empty = inherit default model
describer_ctx_tokens = 8192
group_size = 8
max_levels = 6
batch_size = 4
# ── Compiled prompts ──────────────────────────────────────────────────────────
[compile_prompts]
enabled = true
auto_recompile = true
max_recompile_attempts = 3
error_rate_threshold = 0.2
min_samples = 5
min_savings_ratio = 0.10
cache_dir = ".agent/compiled_prompts"
exclude = []
# ── Tool compaction ───────────────────────────────────────────────────────────
[tool_compaction]
enabled = false
base_url = "" # optional smaller endpoint; empty = default model endpoint
api_key = ""
model = "" # empty = inherit default model
max_output_tokens = 512
timeout_seconds = 30.0
min_length_to_compact = 500
concurrency_limit = 2
skip_on_error = true
skip_on_truncated = true
skip_tools = ["read_file"]
prompt_path = ""
# ── Tool Discovery (progressive disclosure) ──────────────────────────────────
# With ~70 tools, sending every schema each turn burns context and drowns a
# weak local model in choices. When enabled, only a small CORE set ships every
# turn; the rest are advertised as a compact grouped catalog in the system
# prompt, and their full schemas load on demand when the model calls
# find_tools("<keywords>"). Off by default (full schema list, unchanged).
[tool_discovery]
enabled = false
extra_core = [] # tool names to always expose with full schema (added to core)
max_results = 8 # max tools returned/activated per find_tools call
# ── Web Search ───────────────────────────────────────────────────────────────
# Five-layer safety architecture. All web content is treated as hostile.
# Off by default — you must opt in.
[web_search]
enabled = false # off by default — set true to enable
backend = "auto" # "auto" | "duckduckgo" | "brave" | "mojeek" | "searxng" | "marginalia"
# auto = fallback chain: searxng (if url set) → duckduckgo →
# brave (if key) → mojeek → marginalia; anti-bot blocks skip
# to the next backend instead of reporting "no results".
searxng_url = "" # self-hosted SearXNG instance (most robust, no anti-bot);
# instance must enable JSON output. e.g. "http://192.168.31.42:8888"
# web_search tool also takes source="social" (HackerNews/Lemmy/Reddit), "code"
# (GitHub/StackOverflow), "wiki" (Wikipedia) — keyless JSON APIs, no anti-bot.
max_results_per_search = 10
max_search_calls_per_turn = 3
max_fetch_calls_per_turn = 5
execution_mode = "sandboxed" # "sandboxed" (HTTP under bwrap/firejail) | "direct"
timeout_connect_s = 10
timeout_total_s = 30
user_agent = "owncoder-agent/1.0"
# ── Planning ──────────────────────────────────────────────────────────────────
[planning]
enabled = true
auto_commit_on_step_complete = false
max_steps = 50
# ── Recovery ──────────────────────────────────────────────────────────────────
[recovery]
enabled = true
prompt_mode = "ask" # "ask" | "auto_recover" | "auto_skip"
keep_resolved_days = 14
# ── Security / sandbox ────────────────────────────────────────────────────────
[credpool]
# Credential pool: authenticated, well-behaved internet access. Hold one real
# (free) account per service so blocked-by-default sites see a logged-in user.
# Credentials live in an encrypted vault below the LLM and are attached at the
# HTTP transport layer, so a prompt-injectable internet subagent never sees them.
# Add accounts via /credpool add <service> <domain> <username> <password>
# (never in this file — secrets stay out of config). See docs/credential-pool.md.
enabled = false
cooldown_seconds = 3600 # rest an account this long after a soft block (401/403/429)
[security]
sandbox_backend = "auto" # "auto" | "bwrap" | "firejail" | "none"
# auto tries bwrap then firejail; explicit value must
# be the binary name. bwrap gets the curated seccomp
# filter; firejail uses its own generic one.
require_sandbox = false # default true: refuse host exec when no backend found
network = "off" # "off" | "on" — allow sandboxed commands to opt into
# network access (run_shell/run_argv network=true)
cpu_seconds = 20
wall_seconds = 30
rss_mb = 512
nproc = 64
fsize_mb = 256
nofile = 256
follow_symlinks = false
project_venv_on_path = true # prepend <project>/.venv/bin (or venv/bin) to PATH for sandboxed
# commands, so `python3`/`pip` are the project's. The sandbox mounts
# only /usr and the project root, so a venv kept anywhere else is
# invisible inside it and bare `python3` is the system interpreter
# without the project's dependencies — which reads to a model as
# "the machine needs pip install" instead of "use the venv".
mask_scan_timeout_s = 10.0 # before every sandboxed command the tree is walked once to find
# secret files to mask and policy files to bind read-only; past
# this budget the command is refused. File count is not limited.
# /sandbox shows what the last scan cost.
mask_scan_max_matches = 2000 # masks + read-only binds per command (bwrap ceiling: 2900)
mask_scan_fail_open = false # true: run anyway on an incomplete scan (leaves a protection gap)
# ── Pre-approved path ceiling — USER CONFIG ONLY ────────────────────
# Lives in ~/.config/agent/agent.{toml,yaml}, NOT in a project config: a repo
# that ships its own ceiling would be approving its own access, so a project
# layer setting this key is ignored (with a warning).
#
# With entries here, every path grant added at runtime — the Access panel, the
# agent's request_path_access tool, `paths add`, a stored path_grants.json, a
# resumed session's snapshot — must lie under one of them, and a rw grant needs
# a rw entry. An ro entry is therefore a permanent read-only decision that the
# agent cannot undo. No entries (the default) = grants are unrestricted.
#
# [[security.grant_ceiling]] # read-only reference material
# path = "/srv/specs"
# mode = "ro"
#
# [[security.grant_ceiling]] # a tree the agent may also write
# path = "/home/me/src"
# mode = "rw"
# ── Adaptive Emotional Intelligence ──────────────────────────────────────────
# Controls how the agent adapts its tone and communication style.
#
# mode = "adaptive" (default) — silently assesses each user message on
# sentiment, certainty, and style; adjusts verbosity,
# confirmation frequency, and tone accordingly.
# mode = "analytical" — always direct, terse, critical; no hedging or
# emotional padding. For power users who know what they want.
# mode = "supportive" — always warm, explanatory, patient. For beginners.
[aei]
mode = "adaptive" # "adaptive" | "analytical" | "supportive"
# ── Parallel agents ───────────────────────────────────────────────────────────
# Gives the agent a `spawn_agents` tool to fan-out independent subtasks across
# multiple model endpoints simultaneously.
#
# Each task runs in a fresh worker agent with its own LLM client. Workers share
# the RAG store (read-only) but have isolated message history. Workers cannot
# call spawn_agents themselves (recursion guard).
#
# Example: research a topic on GPU + CPU + DeepSeek in parallel:
# spawn_agents(tasks=[
# {"task": "...", "model": "local-coder"},
# {"task": "...", "model": "local-fast"},
# {"task": "...", "model": "deepseek-r1"},
# ])
[parallel]
enabled = false
workers = ["local-coder"] # flat list used round-robin when task.model omitted; overridden by groups
global_max_concurrent = 4 # cap for models not in any group
worker_tools = "readonly" # "readonly" = read_file/search_code/list_files/grep/recall only
# "all" = full tool set minus spawn_agents
# "internet" = web_search/web_fetch only
worker_timeout_seconds = 120
# ── Model groups (optional) ───────────────────────────────────────────────────
# Each group has its own concurrency limit so GPU workers don't contend with
# cloud workers. A model that belongs to a group uses that group's semaphore
# rather than global_max_concurrent.
# [parallel.groups.gpu]
# models = ["local-coder"]
# max_concurrent = 1 # only one GPU job at a time
# [parallel.groups.cpu]
# models = ["local-fast"]
# max_concurrent = 2
# [parallel.groups.cloud]
# models = ["deepseek-r1", "deepseek-v4-preview"]
# max_concurrent = 5 # API rate-limit headroom
# ── Remote → local failover ───────────────────────────────────────────────────
# ── Action classifier ─────────────────────────────────────────────────────────
# Optional. Small local/LAN model scores risky tool calls before they run
# (safe / needs_review / destructive / exfiltration + probability). Only ever
# escalates: advisory = note, enforce = ask/deny. Loopback/private-IP endpoints
# only unless allow_remote. Server setup: docs/classify.md. Off by default.
# [classify]
# mode = "advisory" # off | advisory | enforce
# endpoint = "http://192.168.31.42:8084/v1"
# model = "classifier"
# timeout_s = 1.5
# ask_at = { needs_review = 0.85, destructive = 0.6, exfiltration = 0.5 }
# deny_at = { exfiltration = 0.9 }
# hide_unconfigured_notice = false
# review_below_confidence = 0.0 # unsure verdict → review (0 = off)
# turn_health = "off" # off | advisory | act — stuck/looping/fake-call turns
# answer_check = "off" # off | advisory | act — reply that fakes calls / echoes a template
#
# Cloud variant — TypeSafe Jev SaaS (same interface; redacted + identity-scrubbed
# args leave the machine, file contents never; refused under air-gap/private):
# backend = "jev"
# allow_remote = true
# api_key = "file:~/.config/agent/typesafe.key" # or "env:VAR"; default $TYPESAFE_API_KEY
# timeout_s = 3.0
# When the active (remote) endpoint is unreachable / times out / 5xx — or serves
# no model under the configured id — retry the turn on another live entry so the
# agent keeps working offline. Failover only ever routes toward local (or to a
# cheaper cloud peer), so it never weakens privacy. Off by default.
[failover]
enabled = false
local_entry = "" # fallback entry name ("" = first local-tier model entry)
max_retries = 1 # at most this many remote→local switches per turn
# What an automatic switch may do when the active entry was hand-picked this
# session (/model <entry>). A pin is a deliberate choice, so drifting off it —
# and especially onto a PAID endpoint — is a policy decision, not a repair:
# "fallback" = switch to any live entry, paid included if that is what is left
# "free-only" = local/LAN/free entries only; stop and ask if only paid are live
# "ask" = never switch automatically; stop and ask the user
# Ignored when the entry was NOT pinned — an auto-tier turn keeps failing over
# freely within its model-mode tiers.
# pinned_policy = "fallback"
# ── Per-turn privacy routing ──────────────────────────────────────────────────
# When the active endpoint is REMOTE and the outbound payload contains a
# secret/credential shape (reuses the security.redaction patterns), apply a
# strategy. Local endpoints are always exempt — nothing leaves the machine.
# "redact" mask the secrets in the wire copy only, then send remote
# (history untouched). Keeps remote power on non-sensitive work.
# "force-local" switch this + later turns to a local model.
# "block" refuse the turn with a clear message.
[privacy]
enabled = false
strategy = "redact" # "redact" | "force-local" | "block"
local_entry = "" # entry used by force-local ("" = first local-tier entry)
# ── Per-edit diagnostics ──────────────────────────────────────────────────────
# Runs a fast, single-file checker right after each successful edit and folds
# its output into the tool result the model is about to read, so breakage is
# fixed on the next step. Different from [verify], which runs one project-wide
# command at the end of a turn. Leave `checkers` empty to auto-select whichever
# supported checkers are installed (ruff / bash -n / gofmt / eslint, with
# py_compile as the Python fallback). Commands run as argv — never a shell — and
# the file is confined to the working directory.
[diagnostics]
enabled = false
timeout_s = 5.0 # per checker, per file — keep it sub-second in practice
max_findings = 10 # cap on lines injected into context
# [[diagnostics.checkers]]
# suffixes = [".py"]
# command = ["ruff", "check", "--output-format", "concise", "{file}"]
# ── Tool permissions (policy layer) ───────────────────────────────────────────
# allow / ask / deny per tool, with optional matching on the tool's primary
# argument. This sits ABOVE the enforcement layers (sandbox, fs gate, deny
# globs, air-gap): it can only narrow what those permit, never widen it.
# Rules are evaluated in order and the FIRST MATCH WINS.
# match — glob by default; prefix "re:" for a regex (refused on path args).
# argv lists are joined with spaces, so "git push*" matches
# run_argv(argv=["git", "push", "origin"]).
# An `ask` verdict with no interactive UI (headless run) resolves to deny.
# Durable rules added at runtime live in .agent/permissions.json, which is
# write-denied to the agent's own file tools. Manage with /permissions.
[permissions]
default = "allow" # allow | ask | deny — "allow" keeps current behavior
ask_timeout_s = 300.0 # no answer in this long = deny (fail closed)
# A built-in `ask` baseline runs at the LOWEST precedence, so your own rules
# below always win. It covers only actions that cannot be undone or that leave
# this machine: force-push, remote branch deletion, history rewrites, publish to
# a registry, sudo, raw egress (curl/ssh/nc), scheduled work. Ordinary pushes,
# installs, builds and `git reset --hard` are NOT in it — a baseline that fires
# on routine work just teaches you to approve without reading.
# See `agent permissions` for the resolved list.
builtin_rules = true
# Note: with no interactive UI (`agent run`, CI) an `ask` denies, because nobody
# is there to approve. `agent run` warns about that before it starts.
# [[permissions.rules]]
# tool = "run_argv"
# match = "git push*"
# verdict = "ask"
# reason = "pushing publishes work"
# [[permissions.rules]]
# tool = "write_file"
# match = "deploy/**"
# verdict = "deny"
# reason = "deploy config is changed by humans only"
# ── Prompt-cache breakpoints ──────────────────────────────────────────────────
# Most endpoints (OpenAI, DeepSeek, vLLM, llama.cpp) cache prompt prefixes
# automatically and need nothing here — leave it "off". Anthropic-compatible
# endpoints only cache what the request marks explicitly; set "anthropic" to
# emit cache_control blocks at the end of the system preamble and at the end of
# the conversation. Sending markers to an endpoint that validates its request
# schema breaks the request, which is why this is opt-in per model entry:
# [models.my-claude]
# cache_breakpoints = "anthropic"
# The cached share of each request is reported in the post-turn usage line when
# the endpoint tells us (many do not). Default is "off" for every entry.
# ── Checkpoint persistence ────────────────────────────────────────────────────
# /checkpoint takes a session-wide rollback point before a risky multi-file
# change. The journal holds a pre-image copy of every edited file, mirrored to
# .agent/checkpoints/ so the rollback point survives a crash or restart — which
# is exactly when it is wanted. Pre-images are content-addressed, so repeated
# edits to one file do not store repeated copies, and the journal is pruned by
# age at session start. .agent/checkpoints/ is write-denied to the agent's own
# file tools. Set persist = false for the old memory-only behavior.
[checkpoints]
persist = true
max_age_days = 7.0 # drop journal entries older than this (0 = keep forever)