Porting notes: maki (bash permission scopes, token reduction, subagent tiers)
Maps the maki reference to the harness build: tree-sitter scope extraction
and the 4-way rule matcher for the permission gate, index/tool_search token
reduction, weak/medium/strong subagent tiers with model clamping, and the
headless spawn model that the conductor loop follows. Gotchas preserved:
headless prompt==deny for complex bash, matcher suffix order, deny-exact vs
allow-broad asymmetry, symlink-aware path checks.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,180 @@
|
|||||||
|
# Porting notes: maki (reference/maki @ plugins/ + maki-agent/)
|
||||||
|
|
||||||
|
Scope for MOPAC harness: tree-sitter bash permission parsing, token-reduction
|
||||||
|
(index / MCP tool_search), subagent tiers, headless loop. Paths relative to
|
||||||
|
`reference/maki/`; line refs from study 2026-08-28, drift possible. Rust core
|
||||||
|
+ Lua tool plugins; Lua API mirrors tree-sitter via maki-lua.
|
||||||
|
|
||||||
|
## 1. Tree-sitter bash permission parsing
|
||||||
|
|
||||||
|
Two halves: Lua plugin extracts "scopes" from command text; Rust
|
||||||
|
PermissionManager matches them against allow/deny rules.
|
||||||
|
|
||||||
|
**Scope extraction** — `plugins/bash/init.lua:275-296` (`permission_scopes`):
|
||||||
|
- Parse `command` with tree-sitter-bash. Parse error (`root:has_error()`)
|
||||||
|
OR any node (recursive) of type `command_substitution |
|
||||||
|
process_substitution | subshell | arithmetic_expansion` (is_complex,
|
||||||
|
:176-193) -> `{scopes=[whole command], force_prompt=true}`. Never
|
||||||
|
decompose `$(...)`; force prompt instead. Else `collect_commands`
|
||||||
|
(:209-235): `program`/`list` -> recurse (flattens `&&`/`||`/`;`);
|
||||||
|
`pipeline` -> one scope per named child; leaf types `command,
|
||||||
|
redirected_statement, negated_command, subshell, compound_statement,
|
||||||
|
if/while/for/case, function_definition, c_style_for_statement` ->
|
||||||
|
trimmed node text. `git diff && rm -rf /` -> ["git diff","rm -rf /"];
|
||||||
|
`a | b` -> [a, b]. `cd X && cmd` -> workdir+cmd hint (:37-49); the
|
||||||
|
`workdir` input param is preferred (description enforces).
|
||||||
|
|
||||||
|
**Rule engine** — `maki-agent/src/permissions.rs`:
|
||||||
|
- `check_inner` (:308-417): per scope, iterate rules session -> config ->
|
||||||
|
builtin -> plugin. ANY matching deny => whole call Denied immediately.
|
||||||
|
Matching allow => covered. Uncovered => NeedsPrompt (or per-tool/global
|
||||||
|
default Deny|Allow|Prompt). `force_prompt` => prompt all scopes despite
|
||||||
|
allows. Yolo => Allowed but only AFTER deny scan (:356): deny beats yolo.
|
||||||
|
- `scope_matches` (:680-706), order critical: `pfx/**` => normalized path
|
||||||
|
prefix (absolutize + `incremental_canonicalize`, symlink-aware);
|
||||||
|
`cmd *` => equals `cmd` or starts `"cmd "` (word boundary: `pwd *`
|
||||||
|
matches `pwd`/`pwd -L`, not `pwdx`); `pfx*` => raw prefix; else exact.
|
||||||
|
Universal `*`,`/**`,`/*` short-circuit every scope.
|
||||||
|
- Builtin rules (:32-48): file-write tools allow `cwd/**`; `task` `*`;
|
||||||
|
nothing for bash. Answers (:113-183): allow once/session/always_local/
|
||||||
|
always_global, deny(+guidance)/always. `apply_decision` (:495-545)
|
||||||
|
GENERALIZES before persisting: bash -> first token + `" *"`
|
||||||
|
(`cargo test -p x` -> `cargo *`, :737-740); file writes -> parent
|
||||||
|
`/**`; MCP -> `*`. Deny persists EXACT scope (tight deny, broad
|
||||||
|
allow — deliberate).
|
||||||
|
- `enforce` (:548-633): NeedsPrompt => PermissionRequest event + await on
|
||||||
|
response channel; NO channel (headless) => deny (user_abort). Headless
|
||||||
|
needs rules/yolo covering everything it will do.
|
||||||
|
|
||||||
|
**Go port**: `github.com/tree-sitter/tree-sitter-bash`; node kinds are
|
||||||
|
identical — port COMPLEX_TYPES/LEAF_COMMAND_TYPES verbatim. Rules as
|
||||||
|
`{tool, scopePattern, effect}` + the 4-way suffix matcher. Keep
|
||||||
|
scopes=segments: it's what makes `allow bash cargo *` safe under
|
||||||
|
chaining. Skip: plugin rule store, OTel, plan-path auto-allow.
|
||||||
|
|
||||||
|
## 2. Token reduction
|
||||||
|
|
||||||
|
Rationale: `site/docs/content/token-economy/_index.md` — every result is
|
||||||
|
re-paid each turn; attack result size AND round-trip count.
|
||||||
|
|
||||||
|
**index tool** — `plugins/index/{init,indexer}.lua`, 30+ extractors in
|
||||||
|
`plugins/index/lang/*.lua`: sections are `imports: [lo-hi]` with paths as
|
||||||
|
a trie tree; module/type names as wrapped CSV; signatures one per line
|
||||||
|
suffixed ` [start-end]`; test lines separate; module doc range. ~70-90%
|
||||||
|
smaller than full read. Contract: index first, then `read offset/limit`
|
||||||
|
the ranges (hints init.lua:144-152 + description). 2MB cap. PORT: yes,
|
||||||
|
high value, pure tree-sitter. Start: go, rust, python, ts, markdown,
|
||||||
|
json/yaml fallback. Keep the `text [lo-hi]` suffix format.
|
||||||
|
|
||||||
|
**MCP tool_search** — `maki-agent/src/mcp/mod.rs`:
|
||||||
|
- Defer when server's non-`always_load` tools > `defer_tools` (dflt 10;
|
||||||
|
below that defs cost less than a catalog). Hidden tools become ONE
|
||||||
|
synthetic `tool_search` whose description embeds a name-only catalog
|
||||||
|
per server (:1199-1241). 117-tool server -> 1 def. `search_tools`
|
||||||
|
(:400-485): name hit=2, description/schema-prop hit=1; exact name
|
||||||
|
always wins; top 5; model-origin search inserts matches into session
|
||||||
|
`loaded` set -> full defs on NEXT request; overflow <=20 names.
|
||||||
|
Calling a deferred tool by name also loads it (:506).
|
||||||
|
- Session-private `loaded` (:306-344); resume seeds from history tool_use
|
||||||
|
names containing `__`. `extend_tools` recomputed EVERY request
|
||||||
|
(`agent/run.rs:287-299`) — caching freezes the catalog. PORT: yes, with
|
||||||
|
MCP client (crush notes §1); per-turn recompute, per-session loads,
|
||||||
|
resume-seeding are all cheap and load-bearing.
|
||||||
|
|
||||||
|
**code_execution** — `plugins/code_execution/init.lua` + `maki-interpreter`
|
||||||
|
(pydantic `monty` embedded Rust Python): tools as async fns, chained/
|
||||||
|
filtered calls stay in sandbox, only prints enter context; custom
|
||||||
|
`gather()` keeps sibling results when one call fails (:34-50); 30s budget
|
||||||
|
(tool-call await excluded), 50MB, no fs/network, fresh sandbox; audience-
|
||||||
|
gated so read-only subs can't reach edit/write through it (:171-184).
|
||||||
|
SKIP: violates 100%-Go HARD RULE. Substitute now = `batch` tool
|
||||||
|
(`plugins/batch/init.lua`: N independent calls, one turn/request); Go
|
||||||
|
filter DSL later; keep the "only compact output enters context" rule.
|
||||||
|
|
||||||
|
**Also portable cheaply**: global truncation (max lines/bytes per tool;
|
||||||
|
overlong grep lines skipped); partial-output preservation on Esc/deadline
|
||||||
|
(bash keeps streamed lines, task keeps half transcript) — for us: on turn
|
||||||
|
timeout return what the tool produced so far, tagged partial.
|
||||||
|
## 3. Subagent model -> TASK/dispatch
|
||||||
|
|
||||||
|
`plugins/task/init.lua` + audiences `maki-agent/src/tools/registry.rs:21-48`:- Types: `research` (default, read-only) vs `general` (full). Enforcement
|
||||||
|
= audience bitflags (MAIN, RESEARCH_SUB, GENERAL_SUB, INTERPRETER,
|
||||||
|
WORKFLOW). edit/write declare `{main, general_sub, interpreter}`
|
||||||
|
(edit:254+, write:33) so research lacks them; `research.md` prompt
|
||||||
|
doubles down. bash/read/grep default ALL — research CAN bash (git log)
|
||||||
|
but shares parent PermissionManager (Arc clone, api/agent.rs:589), so
|
||||||
|
write-ish bash still denies/prompts. Advertised == callable via one
|
||||||
|
shared filter fn. Tiers: weak|medium|strong (Haiku/Sonnet/Opus, strong
|
||||||
|
~5x medium). `resolve_model_from_ctx` (api/agent.rs:44-63) CLAMPS
|
||||||
|
`requested.min(ctx.tier)` — subagents only go cheaper, never escalate.
|
||||||
|
Tier->model: registry overrides -> discovered -> positional
|
||||||
|
(Strong=models[0], Medium=[1], Weak=[2]) (`maki-providers/src/
|
||||||
|
model_registry.rs:171-238`); pricing per provider in providers/*.
|
||||||
|
Concurrency: process-wide semaphore dflt 8; pcall so error can't leak
|
||||||
|
permit (:178-239).
|
||||||
|
- Structured output: optional JSON Schema -> session-local
|
||||||
|
`structured_output` tool; invalid input = inline error model fixes
|
||||||
|
same-run; <=2 nudges, <=3 schema errors. Isolation: fresh session,
|
||||||
|
prompt inlined, single result string (or validated JSON) back;
|
||||||
|
description demands "concise summaries with file:line refs".
|
||||||
|
|
||||||
|
**Map to MOPAC**: grinder dispatch = weak read-only research; P1/feature
|
||||||
|
= medium general; founder gate stays human. structured_output schema ==
|
||||||
|
REPORT file contract. Semaphore == class-aware LLM slot GATE. Skip the
|
||||||
|
Lua layer: task tool = Go fn spawning a nested bounded agent run with
|
||||||
|
filtered toolset + clamped model alias.
|
||||||
|
|
||||||
|
## 4. Headless / --print + session parallelism
|
||||||
|
- `src/print.rs:140-362`: prompt arg or stdin; output text|json|stream-
|
||||||
|
json. JSON = Claude-Code-style events: `system/init` (cwd, session_id,
|
||||||
|
tools, model), `assistant` per turn (content, usage, model,
|
||||||
|
parent_tool_use_id for subagent turns), `user` tool results,
|
||||||
|
`system/api_retry`, final `result` {subtype, duration_ms, num_turns,
|
||||||
|
result, stop_reason, session_id, total_cost_usd, usage}. Cost summed as
|
||||||
|
turns land (rates move mid-run).- `maki-agent/src/headless.rs`: `spawn` (:164-282) one-shot agent on smol
|
||||||
|
task: own PermissionManager, fresh FileReadTracker, event channel,
|
||||||
|
session persisted per turn. `question` excluded (:182). NO response
|
||||||
|
channel => uncovered scopes deny (§1). `spawn_interactive` (:318+)
|
||||||
|
adds input/answer/cancel/model channels (ACP/SDK, `src/sdk_mode.rs`).
|
||||||
|
SessionMailbox drained before each user turn (run.rs:266). Each spawn
|
||||||
|
self-contained => N concurrent sessions trivial (SDK mode does it);
|
||||||
|
subagent semaphore is the only global cap. Resumable by id from state dir.
|
||||||
|
|
||||||
|
**Port**: this IS our core loop — bounded headless run, JSON events to
|
||||||
|
log, session file per turn for resume/audit. Copy the result envelope
|
||||||
|
(cost/usage/stop_reason) so drip-chaining can budget-gate.
|
||||||
|
## 5. Re-read during build (top 10)
|
||||||
|
|
||||||
|
1. `plugins/bash/init.lua` — scopes, exec, cd-hint, partials.
|
||||||
|
2. `maki-agent/src/permissions.rs` — engine+matcher+generalization; tests :776+ are the spec.
|
||||||
|
3. `maki-agent/src/mcp/mod.rs:358-510,1199+` — tool_search catalog.
|
||||||
|
4. `plugins/index/indexer.lua` + `lang/go.lua` — skeleton format.
|
||||||
|
5. `plugins/task/init.lua` — subagent lifecycle, nudges, semaphore.
|
||||||
|
6. `maki-lua/src/api/agent.rs` — model clamp, session, tools fn.
|
||||||
|
7. `maki-agent/src/headless.rs` — spawn/spawn_interactive wiring.
|
||||||
|
8. `src/print.rs` — event wire format + result envelope.
|
||||||
|
9. `maki-agent/src/tools/registry.rs` — audiences + definitions filter.
|
||||||
|
10. `maki-agent/src/agent/run.rs` — loop, per-turn request_tools, mailbox.
|
||||||
|
|
||||||
|
## 6. Gotchas (top 5)
|
||||||
|
|
||||||
|
1. Headless prompt==deny: parse-error/complex bash forces prompt, and
|
||||||
|
headless has no channel => deny. Rules must be prefix scopes (`cargo
|
||||||
|
*`, `git *`) or commands stay trivial; test compound commands against
|
||||||
|
the decomposer, not just single words.
|
||||||
|
2. Matcher order: try `/**` and `" *"` suffixes BEFORE bare `*` or a
|
||||||
|
plain prefix swallows them; `*`/`/**`/`/*` are universal (deny `*`
|
||||||
|
blocks every tool). `pwd *` must not match `pwdx`.
|
||||||
|
3. Never cache tool definitions: MCP catalog + loads recomputed every
|
||||||
|
request; advertise==callable must hold for audiences too, or a
|
||||||
|
read-only sub gets edit schemas it can't call — or calls them.
|
||||||
|
4. Generalization asymmetry: allow-always broadens (bash first word,
|
||||||
|
writes to parent dir, MCP `*`), deny stays exact. Copy deliberately —
|
||||||
|
broad denies brick sessions, exact allows spam prompts.
|
||||||
|
5. Path scopes need symlink-aware canonicalization incrementally
|
||||||
|
left-to-right BEFORE `..` applies (`physical_boundary_check`,
|
||||||
|
permissions.rs:720-735); lexical prefix matching is smugglable via
|
||||||
|
`symlink/..`. Same care for "writes confined to declared roots".
|
||||||
|
Bonus: tier clamp silently downgrades (medium parent + strong -> medium)
|
||||||
|
— that's the cost control, don't "fix" it. tree-sitter-bash node names
|
||||||
|
shift across grammar majors (e.g. `command_name`); pin your test version.
|
||||||
Reference in New Issue
Block a user