返回 DeepSeek-Reasonix
SPEC.md
根目录 / docs / SPEC.md
1 # Reasonix Engineering Spec
2
3 > Reasonix is a coding agent: a thin harness driving multiple models, with **all
4 > capabilities supplied by configuration and plugins**. This document is the
5 > contract — code follows it. Change the contract first, then the code.
6
7 ## 1. Design Principles
8
9 1. **Config- and plugin-driven core.** The core knows only interfaces. Concrete
10 models and tools are resolved by name from registries, declared in config, or
11 injected by plugins. No hardcoded `switch model`.
12 2. **Single static binary.** `CGO_ENABLED=0`; cross-compile with one command;
13 CLI works out of the box.
14 3. **Lean dependencies.** Standard library by default. A third-party dependency
15 must be pure-Go, lightweight, and must not compromise the single-binary /
16 cross-platform / distribution story. TOML parsing is the one accepted dependency.
17 4. **Two extension tiers.** Compile-time built-ins (self-register via `init()`),
18 and runtime external plugins (stdio JSON-RPC subprocesses, MCP-compatible).
19 5. **Interface-first & registry-based.** `Provider` and `Tool` are interfaces.
20 6. **Evolve, don't over-engineer.**
21
22 Language: **English is the primary language for all code** — comments,
23 user-facing strings, tool descriptions, system prompts, and this spec. The
24 README is bilingual (`README.md` English + `README.zh-CN.md`).
25
26 ## 2. Layout
27
28 ```
29 reasonix/
30 ├── go.mod / go.sum # module reasonix; require BurntSushi/toml
31 ├── Makefile # build / cross / vet / fmt / test
32 ├── README.md / README.zh-CN.md
33 ├── reasonix.example.toml # sample config
34 ├── docs/SPEC.md # this file
35 ├── cmd/reasonix/main.go # entry; blank-imports built-in providers/tools
36 ├── cmd/reasonix-plugin-example/ # reference MCP stdio plugin (a runnable example)
37 └── internal/
38 ├── cli/ # subcommand routing, flags, assembly, exit codes
39 ├── config/ # TOML loading (flag > project > user > defaults)
40 ├── provider/ # Provider interface + types + kind→factory registry
41 │ └── openai/ # OpenAI-compatible impl; init() registers "openai"
42 ├── tool/ # Tool interface + Registry
43 │ └── builtin/ # read_file/write_file/edit_file/move_file/bash/ls/glob/grep
44 ├── permission/ # per-call Policy: allow/ask/deny rules → Decision
45 ├── command/ # custom slash commands loaded from .reasonix/commands/*.md
46 ├── plugin/ # stdio JSON-RPC (MCP) client; adapts remote tools
47 ├── remote/ # SSH transport for the Remote-SSH module
48 │ ├── forward/ # -L / -R port-forward lifecycle
49 │ ├── sftpfs/ # SFTP file layer (quarantines pkg/sftp)
50 │ └── bootstrap/ # detached `reasonix serve` bootstrap over SSH
51 └── agent/ # Session + harness loop
52 ```
53
54 Dependency direction (acyclic): `cli → {agent, plugin, config} → {tool, provider}`.
55 Built-in subpackages (`provider/openai`, `tool/builtin`) import their parent to
56 self-register; parents never import children. The Remote-SSH module layers
57 `cli → remote/bootstrap → remote → {remote/forward, remote/sftpfs, config,
58 netclient}`; `remote` and its subpackages never import `cli`, `agent`, or
59 `serve`, and all interactivity flows through callbacks (host-key / secret
60 prompts) so the desktop module consumes the same surface. See §Remote below.
61
62 ## 3. Core Abstractions
63
64 ### 3.1 Provider + registry (`internal/provider`)
65
66 ```go
67 type Provider interface {
68 Name() string
69 Stream(ctx context.Context, req Request) (<-chan Chunk, error)
70 }
71
72 // Factory builds a Provider from a resolved config instance.
73 type Factory func(cfg Config) (Provider, error)
74
75 // Register adds a factory under a kind (e.g. "openai"). Called from init().
76 func Register(kind string, f Factory)
77
78 // New instantiates the provider of the given kind.
79 func New(kind string, cfg Config) (Provider, error)
80
81 type Config struct {
82 Name string // instance name, e.g. "deepseek"
83 BaseURL string
84 Model string
85 APIKey string
86 Extra map[string]any // kind-specific options
87 }
88 ```
89
90 - The `openai` kind is an OpenAI-compatible `/chat/completions` implementation.
91 - **OpenAI-compatible vendors are config instances** of `kind = "openai"`,
92 differing only in `base_url` / `model` / `api_key_env`. Adding another OpenAI-
93 compatible model is a config edit, not a code change.
94 - **A provider is a vendor endpoint** (one `base_url` + `api_key_env`) that offers
95 one or more models. OpenAI-compatible chat normally posts to
96 `base_url + "/chat/completions"`; set `chat_url` only for gateways that require a
97 full request URL. An entry declares either a single `model = "..."` or a
98 `models = ["...", "..."]` list (with an optional `default`); the list form lets
99 one vendor expose several models without re-declaring the endpoint/key. A
100 **model reference** (`default_model`, the `--model` flag, the desktop switcher)
101 resolves via `Config.ResolveModel`, which accepts a provider name (→ its default
102 model), a bare model name, or an explicit `provider/model`. `context_window` is
103 the provider-wide fallback; `model_overrides.<model>.context_window` can replace
104 it for one model. Per-model `prices` use model IDs as keys.
105 - Streaming tool-call deltas are accumulated by index inside the provider; only
106 complete `ToolCall`s are emitted.
107
108 ### 3.2 Tool + registry (`internal/tool`)
109
110 ```go
111 type Tool interface {
112 Name() string
113 Description() string
114 Schema() json.RawMessage // JSON Schema for parameters
115 Execute(ctx context.Context, args json.RawMessage) (string, error)
116 }
117 ```
118
119 - Built-in tools self-register into a process-global builtin set via `init()`
120 (`tool.RegisterBuiltin(t)`); `tool.Builtins()` lists them.
121 - A runtime `*Registry` is assembled per run: enabled built-ins (filtered by
122 config) **plus** plugin-provided tools. The agent only sees the `*Registry`.
123 - Tool schemas are canonicalized on registry insertion. The built-in contract is
124 documented in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md) and backed by tests that
125 compare the documented surface against the same canonical schema path.
126 - `Execute` parses raw JSON args itself. Errors are returned, not fatal — the
127 agent feeds them back so the model can self-correct.
128
129 ### 3.3 Plugins (`internal/plugin`) — MCP client
130
131 An external plugin is an MCP server declared in config. The wire protocol is
132 **JSON-RPC 2.0** in every case; only the transport differs. A `transport`
133 interface (`call` / `notify` / `close`) abstracts that, so the MCP-level logic
134 (handshake, `tools/list`, `tools/call`, …) is written once.
135
136 - **Transports** (config `type`):
137 - `stdio` (default) — a local subprocess; one JSON message per line over the
138 child's stdin/stdout (the MCP stdio convention). Declared with
139 `command` / `args` / `env`; terminated on ctx cancel / shutdown.
140 - `http` (a.k.a. `streamable-http`) — a remote server at `url`. Each request
141 is an HTTP POST; the server replies with either `application/json` (one
142 response) or `text/event-stream` (an SSE stream carrying the response plus
143 any server notifications). The `Mcp-Session-Id` response header, once seen,
144 is echoed on subsequent requests. Static `headers` (e.g. a bearer token) are
145 sent on every request. OAuth is out of scope for now (see §9).
146 - `sse` — the legacy 2024-11-05 HTTP+SSE transport. A persistent GET stream
147 receives an announced relative POST endpoint, JSON-RPC responses, and server
148 messages. Cross-origin announced endpoints are rejected so static headers
149 cannot leak.
150 - `${VAR}` / `${VAR:-default}` are expanded in `command`, `args`, `env`, `url`,
151 and `headers` so secrets come from the environment, not the config file.
152 - Lifecycle: `initialize` → `notifications/initialized` → `tools/list`;
153 invocation via `tools/call {name, arguments}`.
154 - When a workspace root exists, initialize advertises `roots` and transports
155 answer `roots/list` with its file URI. `tools/call` includes a per-call
156 `_meta.progressToken`; matching `notifications/progress` messages stream into
157 the existing tool-progress event path.
158 - A stdio server uses one persistent transport for initialize, reads, and
159 writes, preserving state such as browser sessions across tool calls. The
160 process uses the server's process sandbox because process confinement cannot
161 change per RPC; read-only eligibility and destructive filtering remain local
162 workflow gates rather than separate process sandboxes.
163 - Configuration provenance is runtime metadata and determines persistence scope.
164 Desktop and CLI installs write the user-global `config.toml`; project
165 `reasonix.toml` and `.mcp.json` entries remain in their owning project file.
166 Every configured source is trusted without a separate launch-confirmation
167 step. Project entries override same-name global entries, and project
168 `reasonix.toml` overrides `.mcp.json`. Editing writes to the effective entry's
169 source; removing it reveals the next lower-priority declaration.
170 - Each remote tool is adapted to the `Tool` interface and injected into the run
171 registry, namespaced `mcp__<server>__<tool>` (spaces normalised to `_`) to
172 match Claude Code and avoid clashes.
173 - A tool's MCP `annotations.readOnlyHint` maps to `Tool.ReadOnly()`. It defaults
174 to false (a remote tool is opaque — we can't see its side effects), so a
175 plugin opts a tool into parallel-batch dispatch and the permission layer's
176 reader-default by declaring `readOnlyHint: true` in `tools/list`.
177 - Installation is the trust decision for tool metadata. Reasonix assumes an
178 installed server reports `readOnlyHint` and `destructiveHint` honestly;
179 planner/read-only filtering is a workflow boundary for trusted servers, not
180 containment against a malicious MCP server. Explicit deny rules and the
181 process sandbox remain host-controlled boundaries.
182 - `prompts/list` + `prompts/get` surface as `/mcp__<server>__<prompt>` slash
183 commands; `resources/list` + `resources/read` are referenced as
184 `@<server>:<uri>` in chat. `/mcp` shows connected servers and their counts.
185 - `cmd/reasonix-plugin-example` is a runnable reference stdio server (`echo`,
186 `wordcount`), driven by an end-to-end test that builds the real binary.
187
188 ### 3.4 Agent (`internal/agent`)
189
190 - `Session` holds `[]Message`.
191 - `Run(ctx, input)` loop: build `Request` (with tool schemas) → `provider.Stream`
192 → print text deltas live, collect complete tool calls → if none, done; else
193 execute each tool (built-in or plugin) and append results → repeat, bounded by
194 `maxSteps`. `ctx` threads throughout (Ctrl-C aborts in-flight requests).
195 - A `Runner` is anything with `Run(ctx, input) error`; both `Agent` and
196 `Coordinator` satisfy it, so the CLI is agnostic to single- vs two-model mode.
197
198 ### 3.5 Two-model collaboration (`Coordinator`)
199
200 When `agent.planner_model` names a provider different from the executor, a
201 `Coordinator` runs two models in **separate sessions** to keep each one's prompt
202 prefix cache-stable:
203
204 - The **planner** (low-frequency) runs in its own session with the same standing
205 memory context plus a filtered read-only research tool set, then produces a
206 concise plan. A deterministic host policy chooses executor-only, light
207 planning, full planning, plan-for-approval, or explicit plan-only from
208 pristine user text plus trusted turn metadata. It does not call a classifier
209 model and does not infer host state from controller-authored prompt blocks.
210 Explicit Plan Mode, synthetic turns, short contextual replies, atomic edits,
211 and bounded read-only actions avoid a second planner; cross-surface,
212 structured, ambiguous, and high-risk work uses the full contract. Active Goal
213 and Delivery turns upgrade non-atomic mutation work, while bounded read-only
214 actions remain executor-only. The privacy-safe
215 route/depth/reason decision is emitted in phase detail.
216 - Light plans use a small per-turn research-round budget and return a compact
217 objective, 1-4 ordered steps, likely touchpoints, and primary verification.
218 Full plans use a larger bounded budget and distinguish verified from candidate
219 touchpoints, with risks, acceptance criteria, command-level verification, and
220 rollback when relevant. The depth contract stays in one stable system prompt;
221 only a small host-authored `<planner-turn>` block changes per user turn. If
222 the planner still does not finalize after the bounded research and grace
223 round, plan-and-execute falls back to the executor with the pristine task;
224 plan-only and plan-for-approval remain fail-closed. The incomplete planner
225 turn is rolled back rather than exposed as a broken manual continuation.
226 - A bare plan-first route hands the completed plan directly to the executor.
227 Plan-for-approval is reserved for an explicit request to wait for
228 confirmation; the host enforces that boundary even if the planner omits its
229 marker, then hands the approved plan to the executor. A headless host persists
230 the plan so a later turn can continue. Explicit plan-only requests persist the
231 plan and end the current turn without execution. A planner failure on either
232 execution boundary cannot fall back to the executor. These directives may
233 appear after the task clause; quoted examples do not change the route.
234 - The plan is handed off as structured text to the **executor** — a full
235 tool-using `Agent` in its own session — which validates candidate assumptions
236 and carries it out.
237 - The sessions never mix, so neither model's prefix is disturbed by the other's
238 turns; both grow prepend-only and stay cache-friendly. This reconciles
239 "cache-first" with "two-model collaboration": switching models *inside one
240 shared conversation* would break the prefix and tank cache hits, so we don't.
241
242 ### 3.6 Context management (compaction)
243
244 Long tasks eventually fill the model's context window. Reasonix manages this with
245 **low-frequency compaction** that respects the cache-first design:
246
247 - Each provider declares its `context_window` (tokens). Context maintenance is
248 tiered: below `agent.tool_result_snip_ratio` (default `0.6`) the session is
249 left untouched apart from the soft notice; at the snip ratio, stale tool
250 results before the recent tail are archived and shortened with deterministic
251 head/tail markers; at `agent.compact_ratio` (default `0.8`) stale tool results
252 are archived and pruned to short placeholders before any summary call; only if
253 pruning still leaves the prompt above the threshold does summary compaction
254 run. At `agent.compact_force_ratio` (default `0.9`), the existing forced fold
255 may proceed even when the fold economics would normally skip it.
256 - Users can inspect or change the 65–85% automatic threshold with
257 `reasonix config compact-ratio [--local] [VALUE]`. The default is 80%; the
258 project-local value overrides the shared user config used by desktop and new
259 CLI sessions.
260 - A positive `model_overrides.<model>.context_window` replaces the provider-wide
261 value after model resolution. Missing or zero model overrides inherit the
262 provider value; provider-level `context_window = 0` disables compaction.
263 - `max_output_tokens` is a separate total-output budget, not a conversion from
264 the client reasoning byte guard. Zero selects the provider's safe default,
265 positive values set an explicit cap, and negative values omit optional wire
266 limits. `model_overrides.<model>.max_output_tokens` can specialize mixed
267 gateways; Anthropic still supplies a mandatory `max_tokens` fallback.
268 - Tool-result snip/prune never removes messages, so assistant `tool_calls` and
269 tool results stay paired. `KeepErrors` preserves error/blocked tool outputs,
270 and the recent tail is not rewritten. Snipped results can later be upgraded to
271 pruned placeholders; already-pruned results are left alone.
272 - When summary compaction runs, it folds only the assistant/tool work. Every
273 **user turn** small enough to be a brief and every **prior digest** is kept
274 verbatim; the foldable remainder is summarized — using the executor's own
275 provider, no tools — in place. The boundary is aligned backward off any tool
276 result so the recent tail never begins with an orphan tool message whose
277 `tool_calls` were summarized away.
278 - The dropped originals are archived under the user config dir
279 (`reasonix/archive/<timestamp>.jsonl`; see §5 for its per-OS location), one
280 message per line, so the full history stays traceable.
281 - The read-only `history` tool gives the agent on-demand BM25 retrieval over
282 saved session JSONL files. `scope="project"` searches the current controller's
283 session directory; `scope="global"` also searches the user-global session
284 directory and compacted-history archives. `operation="around"` can then read a
285 bounded transcript window around a returned hit. Search keeps the best hit and
286 trims trailing common-word-only noise with a relative score floor; a 0-result
287 response tells the agent how to retry with rarer terms or widen scope.
288 - The read-only `memory` tool gives the agent on-demand search/list/read access
289 to saved auto-memory files. It complements the writer tools: `memory` checks
290 what already exists, `remember` saves or updates a fact, and `forget` removes
291 a stale one from the active index while archiving the file for traceability.
292 Archived memory files are visible in local management surfaces (`/memory`,
293 TUI, desktop panel) but are excluded from active-memory retrieval. Memory
294 search uses the same relative BM25 floor and guides the agent to fall back to
295 history when exact original wording or tool output matters.
296 - Before each real user turn, bounded BM25 recall selects relevant active facts
297 from the raw user message and appends them as a low-authority user-turn suffix.
298 Generic turns are suppressed, project facts override equivalent global
299 fallbacks, stale facts are down-ranked, and recall is bounded by result/character
300 budgets. This never mutates the stable system prompt or tool schemas.
301 - The owning controller may auto-allow only a bounded, non-sensitive,
302 create-only project/reference `remember`, including in a top-level headless
303 run. Global facts, preferences, feedback,
304 updates, duplicates, sensitive/oversized content, and every `forget` require a
305 fresh human approval even under Auto or YOLO. Guardian/safety review cannot
306 answer these prompts on the user's behalf. Sub-agents and headless surfaces
307 without the owning scoped controller fail closed. The approval request includes a compact preview, while
308 external notification hooks only receive the tool name.
309 - Facts carry immutable IDs, monotonic revisions, timestamps, type, and scope.
310 Updates snapshot the previous revision; restore and archive recovery create a
311 higher revision and reject path escapes, symlinks, collisions, and overwrites.
312 User-initiated memory edits in the local UI are already explicit user actions.
313 See [`SESSION_MEMORY_RETRIEVAL.md`](SESSION_MEMORY_RETRIEVAL.md) for the
314 detailed implementation contract.
315
316 **What survives a fold.** A fact the user states in a normal-sized turn is kept
317 verbatim and is never summarized away — at any point in the session, across any
318 number of compactions. A digest, once written, is likewise kept verbatim rather
319 than re-summarized, so facts it captured are not lost to drift. The one
320 **best-effort** boundary: a fact buried inside a single oversized message (a
321 large paste, over the per-turn pin budget) folds with the rest, so its survival
322 depends on the summarizer catching it while compressing bulk. There is no
323 reliable way to auto-detect an arbitrary fact in bulk, so durable facts belong in
324 their own turn rather than buried in a large paste; the raw oversized content is
325 still archived and recoverable either way.
326
327 This is the **only** point where the prompt prefix changes — a deliberate, rare
328 "cache-reset point". Between compactions the session grows prepend-only and
329 stays cache-friendly, so cache hit rate (the key observability signal) stays
330 high. `context_window = 0` disables compaction for an instance.
331
332 ### 3.7 Permissions (`internal/permission`) — per-call gating
333
334 A coding agent runs shell commands and edits files autonomously. The permission
335 layer decides, **per tool call**, whether to allow it, deny it, or ask the user
336 first. It is independent of the model and of the CLI — the agent consults a
337 `Gate` interface at execute time; the gate is built from a static `Policy` plus
338 an optional interactive `Approver`.
339
340 ```go
341 type Decision int // permission package
342 const (Allow Decision = iota; Ask; Deny)
343
344 // Policy evaluates static rules against a tool call. Pure, no I/O.
345 type Policy struct { Mode Decision; Allow, Ask, Deny []Rule }
346 func (p Policy) Decide(toolName string, readOnly bool, args json.RawMessage) Decision
347 ```
348
349 - **Rule syntax.** A rule is `Tool` (matches any call in that tool family) or
350 `Tool(specifier)` (matches when the call's *subject* matches the specifier).
351 Bash and file mutation approvals use Claude Code-style families such as
352 `Bash(npm run build)`, `Bash(npm run test:*)`, and `Edit(docs/**)`. Built-in
353 file mutations include writes, edits, notebook edits, symbol/range deletes,
354 and `move_file` renames/moves. Legacy lowercase tool IDs still load for
355 compatibility. `Bash=<literal>` is the exact-command form: metacharacters in
356 the literal are ordinary characters and only the identical complete command
357 matches. The
358 `:*` suffix marks a Bash command-prefix approval; generated prefix rules also
359 reject later commands that introduce shell operators, so `Bash(go test:*)`
360 does not cover `go test ./... && rm -rf tmp`.
361 Legacy `Bash(go test *)` prefix rules still load, but new rules are saved as
362 `Bash(go test:*)`. The subject is extracted generically from the call's JSON
363 args by a small set of
364 known keys — `command` (bash), `path` / `file_path` (file tools), `pattern`
365 (grep/glob) — so tools need not change. A rule whose subject the args don't
366 expose only matches in its bare `Tool` form.
367 - **Dynamic Bash.** Parameter/arithmetic expansions, assignments, heredocs, unproved
368 redirects, and shell globs cannot reuse bare Bash, prefix, or glob allows;
369 remembered approvals are exact `Bash=<literal>` rules. They still follow the
370 normal posture fallback, so Auto and an approved-plan window may execute them
371 without prompting. Nested or indirect execution is stricter: command and
372 process substitution, a dynamic command name, parse failures, `eval`,
373 `source`, shell `-c`, PowerShell/cmd command strings, and runtime inline-code
374 flags require a human in interactive Ask/Auto. Guardian, allowing hooks, and
375 the approved-plan window cannot answer that decision; only an identical exact
376 grant or YOLO can bypass it by default. The advanced
377 `[permissions] allow_dynamic_bash = true` opt-in lets an Allow fallback,
378 including Auto, cover this class; explicit `ask` and `deny` rules retain
379 precedence.
380 - **Precedence.** `deny` > `ask` > `allow` > fallback. Fallback is `Allow` for
381 read-only tools and `Mode` (default `Ask`) for writers. `deny` always wins, so
382 a broad `allow = ["Bash"]` can still be carved by `deny = ["Bash(rm -rf*)"]`;
383 conversely `ask` overrides a broad `allow` to force a prompt on a risky subset.
384 - **Resolving `Ask`.** The interactive front-end (the chat TUI) prompts the user
385 — allow once / allow this approval scope for the session / always allow this
386 approval scope / deny — via an `Approver`. For Bash, the default scope is the
387 concrete command subject, and the user may choose a conservative command-prefix
388 scope when available (for example `Bash(go test:*)`) so similar invocations in
389 the same session or saved config do not prompt again. For file-mutation tools,
390 a session grant covers editing for the rest of the session while a persisted
391 grant is path-scoped when a path is available, stored as `Edit(<path>)` so all
392 built-in file-mutating tools share it. A
393 non-interactive run
394 (`reasonix run`, a sub-agent, anything with no TTY / no approver) cannot prompt.
395 Its explicit posture therefore resolves without blocking: Ask/manual fails
396 closed, Auto allows only ordinary writer fallback, and YOLO may bypass ordinary
397 Ask decisions. Nested or indirect Bash remains stricter: headless
398 Ask/Auto/DontAsk reject it unless an identical literal grant exists; YOLO or
399 `allow_dynamic_bash = true` with an Allow fallback may opt out. A `Deny` is a
400 hard block in *every* mode: the tool never executes and the model receives a
401 "blocked" result it can adapt to (the same shape as a plan-mode refusal).
402 - **MCP authorization.** Installing an MCP server authorizes all of its tools;
403 there is no second server, raw-tool, writer, or destructive approval policy.
404 Project configuration is trusted the same way and requires no separate launch
405 confirmation. Explicit global deny rules still win. `readOnlyHint` and
406 `destructiveHint` remain internal
407 facts for scheduling, Plan/read-only restrictions, and cached-to-live safety
408 reclassification. Strict read-only sub-agent registries expose only
409 authorized tools with `readOnlyHint: true` and no `destructiveHint`. The
410 two-model Planner uses the fixed `use_capability` proxy (never direct
411 `mcp__*` schemas) for authorized, non-destructive MCP without requiring
412 `readOnlyHint`; destructive tools are left for the Executor. In Balanced
413 two-model sessions the Executor owns an isolated frontend for the same proxy,
414 so Planner-discovered capability IDs remain executable after handoff. Schema-only
415 changes refresh the next-session cache without adding an execution approval
416 or retry. Immediately before dispatch, the proxy re-checks the current
417 controller's enablement, authorization, and complete runtime connection
418 identity; a same-name client on a shared Host is never sufficient authority.
419 - **Relationship to plan mode.** Plan mode (§3.4) is a plan-first collaboration
420 workflow, not an all-tools read-only mode. Before Permissions/Sandbox, the
421 host enforces explicit phase opt-outs (`complete_step` is read-only but
422 belongs to the post-approval execution phase, so it self-reports plan-unsafe
423 and is refused). The dedicated two-model Planner may call authorized,
424 non-destructive MCP even when `readOnlyHint` is absent; it hard-blocks
425 destructive targets and readers from unauthorized servers for the entire
426 planning phase. A single-model Plan without the dedicated Planner continues
427 to block MCP writer/destructive targets while Plan is active.
428 Ordinary built-in and Bash calls then use the same Ask/Auto/YOLO, explicit
429 `ask`/`deny`, and Sandbox path as Standard mode. A third-party MCP
430 `readOnlyHint` affects dispatch classification and strict-child eligibility,
431 but not the dedicated Planner's non-destructive trust path. Once the server is
432 installed or declared in project configuration, all non-destructive
433 capabilities enter the dedicated Planner proxy; only hinted readers enter
434 strict read-only sub-agent execution. `plan_mode_read_only_commands` is
435 retained for config/session round trips and does not grant or revoke calls in
436 the main Plan workflow. `read_only_task` and `read_only_skill` remain strict
437 read-only capabilities with their own tool registry and safe foreground Bash;
438 writer-capable `task` and skill execution remain permission-gated instead of
439 Plan-blocked, and their child turns inherit the Plan workflow marker and
440 explicit phase opt-outs.
441 - **User decisions are separate from tool approvals.** Runtime tool approval has
442 three user-facing postures: `ask` ("需要批准"), `auto` ("自动批准"), and
443 `yolo` ("Yolo批准"). `auto` lets the permission policy auto-approve the writer
444 fallback while preserving explicit ask/deny rules; `yolo` skips ordinary tool
445 permission prompts for approval-gated tools such as writers and Bash. Explicit
446 deny rules and forced fresh reviews still apply. Nested or indirect Bash
447 commands require a human in interactive Ask/Auto even during the approved-plan
448 window; ordinary expansions, assignments, redirects, and globs continue under
449 Auto fallback but cannot inherit reusable Bash rules. YOLO is the sole mode
450 bypass for the human-required class, while an identical exact literal remains
451 an ordinary explicit authorization.
452 Neither posture answers `ask` questions or approves `exit_plan_mode` plans.
453 Plan Mode is entered only through an explicit user choice and remains
454 independent of the active tool-approval posture. After a user approves a
455 plan, the controller opens a short `approvedPlanAutoApproveTools` execution
456 window so the model can perform the approved writes without re-prompting; that
457 transient window still does not auto-approve future plans. In headless `ask`
458 execution, any fallback answer is labelled as a model assumption, not as a
459 user decision.
460
461 - **Collaboration mode is separate from tool approval.** The desktop composer
462 presents collaboration as `normal` ("正常模式"), `plan` ("计划模式"), and
463 `goal` ("目标模式"). `/goal <objective>` starts an autonomous, session-scoped
464 active goal: the controller prepends goal context to user turns outside the
465 cache-stable system prompt and keeps issuing continuation turns until the
466 model reports completion, repeats the same blocked state three times, the user
467 stops it, or the safety continuation limit is reached. Blocked-state matching
468 is normalized for casing, whitespace, and punctuation so minor wording drift
469 does not reset the audit; restarting a goal begins a fresh blocked audit. A
470 goal is treated as a task contract: if the objective includes Context,
471 Request, Output format, Constraints, or Pause policy sections, those sections
472 define the autonomous work boundary. When they are absent, the model infers a
473 lightweight contract from the conversation and workspace. The injected goal
474 block tells the model to pause only for irreversible or externally visible
475 operations, scope changes, or information only the user can provide; ordinary
476 uncertainty should be handled with sensible defaults and reported as an
477 assumption. Completion requires the concrete request, output format,
478 constraints, and relevant verification expectations to be satisfied or
479 explicitly reported as unverified.
480 Goals that look like long-horizon research, debugging, optimization, or
481 implementation work automatically add an AutoResearch protocol to the same
482 transient active-goal user block. AutoResearch is a Goal strategy, not a
483 standalone global skill: it writes project-local state under
484 `.reasonix/autoresearch/YYYYMMDD-HHMMSS-slug/` and keeps dynamic run state out
485 of `REASONIX.md`, `AGENTS.md`, project memory, tool schemas, and the
486 cache-stable system prompt. `/goal --research <objective>` forces that
487 strategy; `/goal --simple <objective>` forces lightweight Goal. Outside goal
488 mode, ordinary prompts never change collaboration mode or create durable
489 AutoResearch state; the user must choose Goal or use `/goal` explicitly.
490 `/goal clear` removes the active goal. Switching into plan/normal mode clears
491 the active goal in the desktop UI so the collaboration mode remains one of
492 the three choices, while the underlying tool approval posture is preserved.
493
494 | Tool approval posture | Tool approvals | Plan approval | `ask` questions |
495 | --- | --- | --- | --- |
496 | Need approval / `ask` | Follow permission policy (`Ask` prompts interactively) | Waits for user | Waits for user |
497 | Auto approve / `auto` | Writer fallback auto-allowed; explicit ask/deny rules still apply | Waits for user | Waits for user |
498 | YOLO approval / `yolo` | Ordinary prompts auto-allowed; deny rules and fresh reviews remain | Waits for user | Waits for user |
499 | Approved-plan execution window | Approved plan's writer fallback is auto-allowed; explicit `ask` / `deny` rules remain | Future plans still wait | Waits for user |
500
501 Out of the box (`mode = "ask"`, no rules), interactive `reasonix` prompts before
502 each writer/bash call and `reasonix run` fails closed on those calls because it
503 has no approver. Use `reasonix run --auto ...` / `-y` to allow ordinary writer
504 fallback in unattended automation; `--permission-mode auto` is equivalent.
505 Explicit `ask` rules still fail closed under Auto, and `deny` rules harden every
506 posture.
507
508 ### 3.8 Slash commands (`internal/command`)
509
510 The chat TUI accepts `/command` input. Three kinds share one dispatch:
511
512 - **Built-in actions** (`/compact`, `/new`, `/clear`, `/effort`, `/mcp`, `/help`) manipulate session
513 state locally and never reach the model. `/new` starts a new session while
514 saving the previous transcript for resume/history. `/clear` requires
515 confirmation, then discards the current context without saving it; it does not
516 delete project memory.
517 - **Custom commands** are Markdown files under `.reasonix/commands/` (project) and
518 the user config dir, e.g. `~/.reasonix/commands/` on macOS/Linux; the project dir overrides the user dir on a
519 name clash. A file `review.md` becomes `/review`; a subdirectory namespaces it
520 (`git/commit.md` → `/git:commit`). Invoking one renders its body and sends the
521 result as the next user turn.
522 - **MCP prompts** (§3.3) appear as `/mcp__<server>__<prompt>`.
523
524 ```markdown
525 ---
526 description: Review the staged diff
527 argument-hint: [focus-area]
528 ---
529 Review the staged diff. Focus on $ARGUMENTS, list bugs with file:line.
530 ```
531
532 - Frontmatter is an optional `---`-fenced block of simple `key: value` lines;
533 `description` and `argument-hint` are recognised (no YAML dependency — Reasonix
534 stays lean). The remainder is the body template.
535 - Substitution in the body: `$ARGUMENTS` (all args, space-joined), `$1`…`$N`
536 (positional, empty when absent), `$$` (a literal `$`). Arguments are the
537 space-separated tokens after the command.
538 - Loading is pure (`command.Load(dirs...)`) and tested; a malformed file is
539 skipped, not fatal. Custom and MCP-prompt commands both resolve to text and
540 reuse the same "start a turn" path as a typed message.
541
542 #### CLI modal/composer ownership
543
544 The Bubble Tea chat TUI has one bottom composer. A slash-command overlay must
545 declare whether it owns keyboard input:
546
547 - **Modal overlays** own navigation/confirm/cancel keys and must hide the
548 composer while open. Examples: `/mcp`, `/resume`, `/rewind`, approval prompts,
549 and non-typing `ask` choice cards.
550 - **Input-owned overlays** are attached to the textarea and must keep the
551 composer visible. Examples: slash/@ autocomplete and `ask` free-text mode.
552
553 New CLI overlays must update `chat_tui.hideComposer()` and add/extend layout
554 tests so `bottomRows()` accounts for either `panel + status` or
555 `panel + composer + status`. This prevents inactive chat input boxes from being
556 rendered under modal panels.
557
558 ### 3.9 Chat references (`@`)
559
560 A chat message may embed `@` references; before the turn is sent, each is
561 resolved and prepended to the message as a tagged block the model can read.
562
563 - `@<server>:<uri>` where `<server>` is a connected MCP server → an MCP
564 resource (`resources/read`), wrapped `<resource ref="…">…</resource>`.
565 - `@<path>` otherwise → a **local file or directory**, but only when the path
566 actually exists on disk. This existence gate is the disambiguator: an ordinary
567 `@mention` or an email address resolves to no file and stays literal text. A
568 file is wrapped `<file path="…">…</file>` (size-capped, binary files noted not
569 dumped); a directory becomes a recursive listing (depth-first, skipping common
570 noise like `.git` and `node_modules`).
571 - Resolution is asynchronous (off the TUI event loop); a fetch failure surfaces
572 as a notice but doesn't block the turn. Reads are user-initiated and read-only
573 — they do **not** pass the permission gate (§3.7).
574 - Typing `/` or `@` opens an autocomplete menu above the input. The `@` menu
575 navigates **one directory level at a time** (`os.ReadDir`, never a recursive
576 walk — bounded for huge directories): a directory entry descends, a file
577 completes, and MCP resources appear alongside top-level entries. The
578 bottom-region menu changes height only on these discrete actions, never per
579 streamed token, so scrollback stays clean (§ rendering).
580
581 ### 3.10 Subagent profiles and explicit CLI execution
582
583 A subagent profile is a Skill with `runAs: subagent` and, for profiles managed
584 by the desktop or CLI editors, `invocation: manual`. Profiles reuse the existing
585 project/global Skill files; they do not introduce another state format or
586 database. Manual invocation excludes a profile from the pinned Skill index so
587 the model cannot discover it implicitly, while explicit `/<name> <task>`
588 invocation remains available.
589
590 Interactive slash invocation and `Controller.RunSubagentProfile` both execute
591 the profile with the Boot-wired Skill runners. Each run gets an isolated child
592 session and returns only its final answer to the caller. The headless contract is
593 explicit:
594
595 - `reasonix subagent try <name> ... <task>` uses the read-only Skill runner;
596 - `reasonix subagent run <name> ... <task>` uses the normal permission and
597 sandbox path; and
598 - ordinary `Controller.Run` / `reasonix run` remains unchanged and does not
599 reinterpret slash-prefixed input as a subagent command.
600
601 Desktop and CLI profile mutations share
602 `skill.ValidateEditableSubagentProfile`. Only simple manual project/global
603 profiles can be rewritten or deleted. Custom-scope Skills, unmanaged
604 frontmatter, and Skill directories containing `references/` or `scripts/` are
605 refused so an editor cannot silently flatten or discard rich Skill content.
606 Built-in profiles support configuration overrides but have no writable file.
607
608 Effective model and effort precedence is: per-profile
609 `agent.subagent_models` / `agent.subagent_efforts`, this call's `model` /
610 `effort` on `task`/`fleet`, profile frontmatter, `agent.subagent_model` /
611 `agent.subagent_effort`, then executor/default model configuration.
612
613 `task` accepts optional `profile` and `write_paths`. `fleet` dispatches 2–64
614 profile-aware tasks under a session scheduler
615 (`agent.max_subagent_concurrency`, default 6; `agent.max_parallel_writers`,
616 default 3). Profile names are resolved at runtime from the Skill store and
617 must never enter tool schemas or the parent system prompt. Custom and named
618 built-in profile bodies are the full child system prompt (no implicit
619 concise default). `parallel_tasks` remains the compatible read-only batch
620 API on the same scheduler. In a persisted parent session, parallel/fleet
621 children save independent transcripts; the aggregate carries bounded previews
622 and stable refs, and `read_subagent_result` pages a referenced final answer by
623 UTF-8 byte offset under the current conversation-lineage/workspace boundary.
624 Headless runs remain ephemeral and return fair bounded previews without refs.
625 See [Subagent profiles](./SUBAGENT_PROFILES.md)
626 for the user-facing command and file-format contract.
627
628 ## 4. Data Types (`internal/provider`)
629
630 ```go
631 type Role string
632 const (RoleSystem Role = "system"; RoleUser Role = "user"
633 RoleAssistant Role = "assistant"; RoleTool Role = "tool")
634
635 type Message struct {
636 Role Role `json:"role"`
637 Content string `json:"content,omitempty"`
638 ToolCalls []ToolCall `json:"tool_calls,omitempty"`
639 ToolCallID string `json:"tool_call_id,omitempty"`
640 Name string `json:"name,omitempty"`
641 }
642
643 type ToolCall struct { ID, Name, Arguments string } // Arguments: raw JSON
644 type ToolSchema struct { Name, Description string; Parameters json.RawMessage }
645 type Request struct { Messages []Message; Tools []ToolSchema; Temperature float64; MaxTokens int }
646
647 type ChunkType int
648 const (ChunkText ChunkType = iota; ChunkToolCall; ChunkDone; ChunkError)
649
650 type Chunk struct {
651 Type ChunkType
652 Text string // ChunkText
653 ToolCall *ToolCall // ChunkToolCall
654 Err error // ChunkError
655 }
656 ```
657
658 ## 5. Configuration (TOML)
659
660 Resolution order: **flag > project `./reasonix.toml` > the user config file
661 > built-in defaults**. Starting with **Reasonix v1.8.1**, the user config lives
662 at `~/.reasonix/config.toml` on macOS/Linux and
663 `%AppData%\reasonix\config.toml` on Windows. See
664 [Configuration paths](./CONFIG_PATHS.md) for migration and related data paths.
665 Fields marked user/global only are not overridden by project `reasonix.toml`.
666 Provider entries name secrets with `api_key_env`; saved key values live in
667 Reasonix's global `<Reasonix home>/.env`, shared by CLI and desktop. Project
668 `.env`, home `.env`, inherited shell environment variables, legacy credentials,
669 and the OS keyring are not provider-key runtime fallbacks. Project `.env` still
670 feeds workspace-scoped, non-provider `${VAR}` expansion for MCP/plugin settings
671 without importing provider keys or Reasonix control variables.
672
673 ```toml
674 default_model = "deepseek" # provider name (→ its default model) or "provider/model"
675 # language = "zh" # ui language tag; empty = auto-detect from $LANG / $REASONIX_LANG
676
677 [ui]
678 # shortcut_layout = "desktop" # classic|desktop; compatibility setting
679 # cursor_shape = "bar" # CLI/TUI textarea cursor: underline|block|bar
680 show_turn_usage = false # hide per-request token/cost receipts in the TUI; default true
681
682 [agent]
683 system_prompt = "You are Reasonix, a coding agent..." # or system_prompt_file = "..."
684 temperature = 0.0
685 reasoning_language = "auto" # visible reasoning text: auto|zh|en
686 # plan_mode_read_only_commands = ["gh issue view"] # legacy compatibility only; Plan bash uses Permissions
687 # planner_model = "deepseek-pro" # optional: two-model collaboration (low-frequency planner)
688 # subagent_model = "deepseek-pro" # optional default for runAs=subagent skills
689 # subagent_effort = "high" # optional default reasoning effort for subagents
690 # subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" }
691 # subagent_efforts = { review = "max", security_review = "high" }
692
693 # A vendor endpoint exposing several models under one base_url/key.
694 [[providers]]
695 name = "deepseek"
696 kind = "openai"
697 base_url = "https://api.deepseek.com"
698 # chat_url = "https://proxy.example.com/v1/chat/completions" # optional full chat request URL
699 # models_url = "https://proxy.example.com/v1/models" # optional model discovery URL
700 models = ["deepseek-v4-flash", "deepseek-v4-pro"]
701 default = "deepseek-v4-flash" # optional; defaults to models[0]
702 api_key_env = "DEEPSEEK_API_KEY"
703 context_window = 1000000 # tokens; harness compacts older history near this limit (0 disables)
704 max_output_tokens = 32768 # total visible + reasoning + tool-call output; 0 = provider default
705 # model_overrides = { "deepseek-v4-flash" = { context_window = 1000000, max_output_tokens = 32768 } }
706
707 # A single-model entry still works for custom OpenAI-compatible endpoints.
708
709 [environment]
710 enabled = true # inject a stable startup summary of OS, shell, and common tool versions
711
712 # Optional trusted executable paths shown to the model when PATH probing is not enough.
713 # Workspace-local paths are listed but not auto-executed during startup probing.
714 # [environment.tools]
715 # go = "/opt/homebrew/bin/go"
716
717 [tools]
718 enabled = [] # omit/empty = all built-ins
719 bash_timeout_seconds = 120 # foreground safety cap; set 0 for no tool-local cap
720 mcp_startup_timeout_seconds = 30 # background initialize + tools/list safety cap
721 mcp_call_timeout_seconds = 300 # default MCP call safety cap; plugin/tool overrides may raise it
722
723 [tools.shell]
724 prefer = "auto" # auto (default) | bash | powershell | pwsh — force the shell tool's interpreter
725 # path = "C:\\Program Files\\PowerShell\\7\\pwsh.exe" # explicit executable for the chosen shell
726
727 [skills]
728 # paths = ["~/my-skills", "../shared/skills"] # extra custom skill roots
729 # excluded_paths = ["~/.agents/skills"] # hide convention roots without deleting folders
730 # disabled_skills = ["review"] # hidden from prompt, slash invocation, and skill tools
731
732 [permissions]
733 mode = "ask" # writer fallback when no rule matches: ask|allow|deny
734 deny = ["Bash(rm -rf*)", "Bash(git push*)"] # hard-blocked in every mode
735 allow = ["Bash(go test:*)", "Bash(git status:*)"] # never prompted
736 ask = [] # force a prompt even if otherwise allowed
737
738 [sandbox]
739 # workspace_root = "" # file-writers confined here; empty = cwd
740 # allow_write = ["/tmp"] # extra dirs write_file/edit_file/multi_edit/move_file may modify
741 # forbid_read = ["${HOME}/.ssh"] # paths read/list/search tools and sandboxed bash may not inspect
742
743 [serve]
744 auth_mode = "none" # none|token|password; use auth before binding beyond localhost
745 # token = "" # optional fixed token; empty token mode generates one at startup
746 # password_hash = "" # bcrypt hash generated with reasonix serve --hash-password --password '...'
747 # behind_proxy = false # trust X-Forwarded-* only behind a trusted reverse proxy
748
749 [[plugins]]
750 name = "example" # type defaults to "stdio"
751 command = "reasonix-plugin-example"
752 args = []
753 # env = { FOO = "bar" }
754 # startup_timeout_seconds = 60 # initialize + tools/list cap; 0 = global/default cap
755 # call_timeout_seconds = 600 # per-server MCP call timeout; 0 = global/default cap
756 # tool_timeout_seconds = { "generate_video" = 1800 } # raw MCP tool names
757 # [[plugins]] # a remote MCP server over Streamable HTTP
758 # name = "stripe"
759 # type = "http" # "stdio" (default) | "http" | "sse"
760 # url = "https://mcp.stripe.com"
761 # headers = { Authorization = "Bearer ${STRIPE_KEY}" } # ${VAR} / ${VAR:-default} expanded
762 ```
763
764 The native CLI updater always installs the latest strict `vX.Y.Z` official
765 release. Legacy channel configuration and arguments remain parseable during
766 1.x, resolve to the official release, and are omitted on subsequent writes.
767
768 The executor tracks an adaptive progress lease while a todo is active. A new
769 completion, unique successful read, command, or mutation renews the lease;
770 exact repeats do not. After 8 no-progress tool-call rounds the host appends a
771 one-shot reassessment nudge. After 16 it pauses and preserves work for a later
772 user turn. The serial contract is level-aware while preserving the
773 single-in_progress rule: in a two-level list the active level-1 sub-step is
774 the only `in_progress` item and its level-0 phase stays `pending`; sub-steps
775 complete in order, and the phase becomes `in_progress` — and signs off — only
776 after all of its sub-steps have completed. A level-1 item with no phase above
777 it is rejected. Retired `[agent].max_steps` and `planner_max_steps` keys remain
778 parseable for upgrade compatibility, but are ignored and removed by a one-time
779 migration. The CLI `--max-steps` flag and `[bot].max_steps` remain separate,
780 explicit controls for one-off and unattended execution.
781
782 `reasonix setup` writes this default config so the CLI is usable out of the box.
783
784 `[ui].cursor_shape` is normalized to `underline`, `block`, or `bar`; empty or
785 unknown values fall back to `bar`. It applies to the Bubble Tea CLI/TUI
786 textarea only, while desktop and browser inputs keep their platform-native
787 cursor behavior.
788
789 `[serve]` controls the HTTP browser frontend used by `reasonix serve`. The
790 default `auth_mode = "none"` is intended for the loopback default
791 `127.0.0.1:8787`; deployments reachable from another machine must use `token` or
792 `password`. Password mode requires either a startup `--password` or a stored
793 bcrypt `password_hash`. `behind_proxy` must stay false unless the server is
794 behind a trusted proxy that owns the `X-Forwarded-For` and `X-Forwarded-Proto`
795 headers.
796
797 MCP servers may also be declared in a project-root `.mcp.json` using Claude
798 Code's exact `mcpServers` schema (`command`/`args`/`env`, `type`/`url`/`headers`,
799 `${VAR}` expansion). It is read after the TOML files and merged into
800 `[[plugins]]`; on a name collision `reasonix.toml` wins (it is the more explicit,
801 Reasonix-specific source). This lets a server already configured for Claude work in
802 Reasonix unchanged.
803
804 MCP startup has a separate lifecycle from an individual tool call. A caller
805 waits briefly for cold startup, while the shared launch/authorization/
806 `initialize`/`tools/list` sequence may continue in the background up to
807 `mcp_startup_timeout_seconds` (default `30`). A per-server
808 `startup_timeout_seconds` overrides that cap. MCP call timeouts begin only after
809 the connection is ready.
810
811 ```json
812 { "mcpServers": {
813 "stripe": { "type": "http", "url": "https://mcp.stripe.com",
814 "headers": { "Authorization": "Bearer ${STRIPE_KEY}" } }
815 } }
816 ```
817
818 `[sandbox]` is the *enforcement* layer beneath permissions (which are *policy*).
819 Phase 0 confines the file-writing built-ins (`write_file`, `edit_file`,
820 `multi_edit`, `move_file`) to `workspace_root` (default cwd), the Reasonix user
821 config dir, plus `allow_write`: a write whose target — resolved to an absolute,
822 symlink-free path so a symlinked dir or `..` cannot tunnel out — falls outside
823 every root is refused, and the error is fed back to the model. Confinement is on
824 by default (root = cwd), so edits stay in the project while the agent can still
825 update its own global config. `forbid_read` lists files or directories the agent should
826 not read, list, or search; entries support `${VAR}` / `${VAR:-default}` expansion
827 and should be absolute, or use `${HOME}` for home-relative secrets such as
828 `${HOME}/.ssh`. `bash` is itself jailed by default when an OS sandbox is
829 available (`[sandbox] bash = "enforce"`: Seatbelt on macOS and bubblewrap on
830 Linux): each command is allowed to write only
831 the same roots plus platform-specific command temp/cache roots, denied reads
832 under `forbid_read`, and allowed to reach the network only when
833 `network = true`.
834 **Windows status:** Reasonix does not ship an OS-level Bash sandbox on Windows.
835 The effective mode is fixed to `off`; an older config containing
836 `bash = "enforce"` remains readable but resolves to `off`, `reasonix doctor`
837 reports the ignored value, and the desktop control is read-only. Bash therefore
838 runs unconfined on Windows. The in-process file tools continue to enforce
839 `workspace_root`, `allow_write`, and `forbid_read`.
840 When no OS sandbox is available, `bash = "enforce"` refuses bash execution
841 instead of running unconfined. Install the platform sandbox backend
842 (bubblewrap/`bwrap` on Linux, `sandbox-exec` on macOS) or set
843 `[sandbox] bash = "off"` to explicitly restore the pre-1.16 unconfined shell
844 behavior. The escape-prompt and broader OS support are Phase 1's remainder (§9).
845
846 ## 6. Error Handling
847
848 - Library code wraps with `fmt.Errorf("...: %w", err)` and returns; it never
849 prints or calls `os.Exit`.
850 - Only `cli` / `main` decide exit codes and user-facing messages.
851 - Tool execution errors are fed back to the model, not fatal.
852 - Network layer should apply bounded exponential backoff on 429 / 5xx
853 (interface reserved; implementation may follow).
854
855 ## 7. Code Style
856
857 - `gofmt` + `go vet` must be clean; package names lowercase; exported
858 identifiers documented; comments explain *why*, not *what*.
859 - No premature generalization. Prefer clear and direct.
860
861 ## 8. Distribution
862
863 - Build: `CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$(VERSION)" -o reasonix ./cmd/reasonix`
864 - Cross matrix: `darwin|linux|windows` × `amd64|arm64`.
865 - Version injected via ldflags (`git describe --tags --always`).
866 - Install: prebuilt binary / `go install` / future `brew tap`.
867
868 ## 9. Roadmap (not in current scope)
869
870 - Sandbox Phase 1: an OS-level jail for `bash` so commands — not just the
871 file-writer built-ins (Phase 0) — are confined to the workspace. **Seatbelt on
872 macOS and bubblewrap on Linux ship, on by default when available** (see §5).
873 Remaining: the escape-prompt — detect sandbox-unavailable or sandbox-denied failures and
874 offer an explicit, permission-gated unconfined rerun (in `reasonix run`, the
875 command just fails and the model adapts), which completes the "allow inside the
876 box, prompt at its edge" model. With this in place, "always allow" rule
877 persistence becomes optional rather than load-bearing.
878 - MCP long tail (deferred deliberately — no consumer / no foundation yet): OAuth
879 2.0 + `headersHelper` auth for remote servers; the remaining `.mcp.json` scopes
880 (local / user — project scope shipped, see §5); tool-search deferral;
881 `list_changed` live updates; channels / elicitation / roots; plugins that
882 provide *providers*, not just tools.
883 - An Anthropic-native provider `kind` (native prompt-cache control), proving the
884 registry generalises beyond one wire format.
885 - "Always allow" persistence writing learned rules back to project config; a
886 per-session permission override flag for `reasonix run`.
887
887 lines MARKDOWN