bots-garden/mini-mepublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

wasm custom tools and hooks #41

Merged@k33g wants to merge feature/wasm-custom-tools into main
Merged as b8680ab3ae7d.
75 files changed
modified .tickets/issues/0046-add-builtin-tools-as-plugins-with-wasm.yaml +40 -5
@@ -1,15 +1,18 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 46
33 title: Add builtin tools as plugins with wasm
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-27T02:30:34.975Z
9-updatedAt: 2026-10-21T00:00:00.000Z
9+updatedAt: 2026-10-21T23:59:59.000Z
1010 labels: []
1111 milestone: v0.9.0
1212 epic: Builtin tools
13+related:
14+ - 119
15+ - 120
1316 body: |
1417 example: safe / unsafe with wasm -> LlamaGuard
1518
@@ -25,8 +28,8 @@ tasks:
2528 createdAt: 2026-10-20T07:42:43.652Z
2629 updatedAt: 2026-10-21T00:00:00.000Z
2730 - id: 2
28- title: "Experiment (not started): bash-guard as a WASM hook vs the Node one — time per call, size, capability set, author experience"
29- state: open
31+ title: "Experiment (done in ticket 120): bash-guard as a WASM hook vs the Node one — time per call, size, capability set, author experience"
32+ state: closed
3033 priority: none
3134 author:
3235 name: k33g
@@ -98,4 +101,36 @@ comments:
98101 a WASM hook with a minimal capability set, compare time per call,
99102 size and the hook author's experience with the Node version. If it
100103 pays, extend to declared tools, then to the LlamaGuard example.
101-
104+ - id: 2
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-10-21T23:59:59.000Z
109+ body: |
110+ First step done in ticket 119: declared tools as WASM modules (wazero,
111+ TinyGo-built WASI commands, JSON on stdin, `read:` / `write:` folders,
112+ nothing else visible), with demos/wasm-tools. Measured there: compile
113+ 176–183 ms once per process, 0.2–0.5 ms per call (vs ~62 ms for a
114+ Node hook, comment 1); binary +3.9 MB. Task 2 (bash-guard as a WASM
115+ HOOK) is still open: hooks are not WASM yet.
116+ - id: 3
117+ author:
118+ name: k33g
119+ email: ph.charriere@gmail.com
120+ createdAt: 2026-10-21T23:59:59.000Z
121+ body: |
122+ Task 2 done in ticket 120 (hooks as WASM modules). bash-guard as a
123+ TinyGo module vs demos/hooks/hooks/bash-guard.js, same denied call,
124+ through hooks.Runner, 20 runs after a first one, twice, linux/arm64:
125+ - time per call: Node v22 median 65–67 ms (first 68–102 ms); WASM
126+ median 0.56–0.58 ms, first run 234–248 ms (the compilation, once
127+ per mm process);
128+ - size: 954 bytes of JS (+ Node) vs a 770 KB module (regexp + JSON);
129+ - capabilities: nothing but its stdin and MM_HOOK_EVENT, vs all the
130+ user's rights;
131+ - author experience: same program shape (JSON in, JSON out), plus a
132+ TinyGo build step; paths must be clean inside a module.
133+ What stays a script: hooks that list the working directory or need
134+ the network (guard models). With 119 (tools) and 120 (hooks), what
135+ this ticket proposed is covered; providers and front ends were judged
136+ not useful as plugins (comment 1).
@@ -1,15 +1,18 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 462 id: 46
3 title: Add builtin tools as plugins with wasm3 title: Add builtin tools as plugins with wasm
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-27T02:30:34.975Z8 createdAt: 2026-09-27T02:30:34.975Z
9-updatedAt: 2026-10-21T00:00:00.000Z9+updatedAt: 2026-10-21T23:59:59.000Z
10 labels: []10 labels: []
11 milestone: v0.9.011 milestone: v0.9.0
12 epic: Builtin tools12 epic: Builtin tools
13+related:
14+ - 119
15+ - 120
13 body: |16 body: |
14 example: safe / unsafe with wasm -> LlamaGuard17 example: safe / unsafe with wasm -> LlamaGuard
15 18
@@ -25,8 +28,8 @@ tasks:
25 createdAt: 2026-10-20T07:42:43.652Z28 createdAt: 2026-10-20T07:42:43.652Z
26 updatedAt: 2026-10-21T00:00:00.000Z29 updatedAt: 2026-10-21T00:00:00.000Z
27 - id: 230 - id: 2
28- title: "Experiment (not started): bash-guard as a WASM hook vs the Node one — time per call, size, capability set, author experience"31+ title: "Experiment (done in ticket 120): bash-guard as a WASM hook vs the Node one — time per call, size, capability set, author experience"
29- state: open32+ state: closed
30 priority: none33 priority: none
31 author:34 author:
32 name: k33g35 name: k33g
@@ -98,4 +101,36 @@ comments:
98 a WASM hook with a minimal capability set, compare time per call,101 a WASM hook with a minimal capability set, compare time per call,
99 size and the hook author's experience with the Node version. If it102 size and the hook author's experience with the Node version. If it
100 pays, extend to declared tools, then to the LlamaGuard example.103 pays, extend to declared tools, then to the LlamaGuard example.
101-104+ - id: 2
105+ author:
106+ name: k33g
107+ email: ph.charriere@gmail.com
108+ createdAt: 2026-10-21T23:59:59.000Z
109+ body: |
110+ First step done in ticket 119: declared tools as WASM modules (wazero,
111+ TinyGo-built WASI commands, JSON on stdin, `read:` / `write:` folders,
112+ nothing else visible), with demos/wasm-tools. Measured there: compile
113+ 176–183 ms once per process, 0.2–0.5 ms per call (vs ~62 ms for a
114+ Node hook, comment 1); binary +3.9 MB. Task 2 (bash-guard as a WASM
115+ HOOK) is still open: hooks are not WASM yet.
116+ - id: 3
117+ author:
118+ name: k33g
119+ email: ph.charriere@gmail.com
120+ createdAt: 2026-10-21T23:59:59.000Z
121+ body: |
122+ Task 2 done in ticket 120 (hooks as WASM modules). bash-guard as a
123+ TinyGo module vs demos/hooks/hooks/bash-guard.js, same denied call,
124+ through hooks.Runner, 20 runs after a first one, twice, linux/arm64:
125+ - time per call: Node v22 median 65–67 ms (first 68–102 ms); WASM
126+ median 0.56–0.58 ms, first run 234–248 ms (the compilation, once
127+ per mm process);
128+ - size: 954 bytes of JS (+ Node) vs a 770 KB module (regexp + JSON);
129+ - capabilities: nothing but its stdin and MM_HOOK_EVENT, vs all the
130+ user's rights;
131+ - author experience: same program shape (JSON in, JSON out), plus a
132+ TinyGo build step; paths must be clean inside a module.
133+ What stays a script: hooks that list the working directory or need
134+ the network (guard models). With 119 (tools) and 120 (hooks), what
135+ this ticket proposed is covered; providers and front ends were judged
136+ not useful as plugins (comment 1).
added .tickets/issues/0116-how-to-use-mm-for-code-completion.yaml +76 -0
new file mode 100644
@@ -0,0 +1,76 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 116
3+title: How to use mm for code completion?
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-21T11:38:42.420Z
9+updatedAt: 2026-10-21T23:59:59.000Z
10+labels:
11+ - wontfix
12+body: |
13+ Inline code completion in the IDEs (VS Code, Zed, …): ghost text while
14+ typing, Copilot-style. Can mm provide it?
15+tasks: []
16+comments:
17+ - id: 1
18+ author:
19+ name: k33g
20+ email: ph.charriere@gmail.com
21+ createdAt: 2026-10-21T23:59:59.000Z
22+ body: |
23+ Answer (2026-10-21): wontfix — inline completion is not mm's job, and
24+ ACP could not carry it anyway. The IDE's completion feature should
25+ talk to a model server directly; mm stays in the agent panel. Taken
26+ out of v0.9.0, labelled wontfix (user's decision).
27+
28+ ## Why not mm
29+ - The protocol. ACP (how mm talks to editors) is sessions and
30+ prompts: no method or capability for inline completion was found;
31+ the only "autocomplete" in ACP clients is slash-command completion
32+ in the chat panel. Inline completion lives in the editor's own
33+ completion feature (or LSP).
34+ https://codecompanion.olimorris.dev/usage/acp-protocol
35+ https://mintlify.com/agentclientprotocol/typescript-sdk/concepts/protocol-overview
36+ - Latency and shape. A completion fires on every typing pause and
37+ must answer in a few hundred ms, with a "prefix + suffix"
38+ (fill-in-the-middle, FIM) prompt. An mm turn is a system prompt,
39+ tool schemas, a history, sometimes several round trips: seconds.
40+
41+ ## What the editors do today (web research, 2026-10-21, not tried here)
42+ - Zed: since February 2026 "edit predictions" accept several
43+ providers, Ollama among them; the official docs say it is set up
44+ separately from the agent — so mm in the agent panel and a local
45+ completion model coexist.
46+ https://zed.dev/blog/edit-prediction-providers
47+ https://zed.dev/docs/ai/use-a-local-model
48+ https://www.alternativeto.net/news/2026/2/zed-editor-adds-multiple-edit-prediction-providers-and-pluggable-architecture/
49+ - VS Code: Continue's tab autocomplete with Ollama and a small code
50+ model (e.g. qwen2.5-coder:1.5b) is the best documented path. One
51+ single source claims Continue was acquired and its repo frozen in
52+ June 2026 — it contradicts the July 2026 docs: to check. Other
53+ extensions exist (Ollama Copilot).
54+ https://gitea.varghacsongor.hu/GithubMirror/continue/src/commit/4fd276fbb7c8baad3feb0351df1acccfd21fb8d4/docs/customize/deep-dives/autocomplete.mdx
55+ https://tech-insider.org/fr/ollama-vscode-assistant-ia-local-2026/
56+ https://awesome.ecosyste.ms/projects/github.com%2Fgnana997%2Follama-copilot
57+
58+ ## Measured on llmman (this sandbox, 2026-10-21, Qwen3.5 0.8B)
59+ | endpoint | result |
60+ | /v1/completions (raw) | works: 0.11–0.17 s for 12 tokens, warm |
61+ | /infill (llama.cpp FIM, used by llama.vim / llama.vscode) | 404, not exposed |
62+ | /api/generate + "suffix" (Ollama FIM) | 400: llmman does not support the "suffix" field of /api/generate |
63+ So today an editor's completion needs a real Ollama or llama-server
64+ beside llmman; llmman is fine for the agent. Possible request to
65+ llmman: accept "suffix" or expose /infill.
66+
67+ ## If anything is done later
68+ - Docs only (EN+FR): an explanation (why not mm; how mm and a
69+ completion model share one editor) and a how-to (Zed edit
70+ predictions / VS Code Continue on Ollama or llama-server with a
71+ small FIM-trained model, mm in the agent panel). It needs a real
72+ try in an editor — not possible in this sandbox.
73+ - A separate idea, not this ticket: mm's possible contribution would
74+ be project context (neighbouring files, LSP symbols, search_docs)
75+ injected into a FIM prompt — what makes Copilot more than a bare
76+ model. A project of its own.
new file mode 100644
@@ -0,0 +1,76 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 116
3+title: How to use mm for code completion?
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-21T11:38:42.420Z
9+updatedAt: 2026-10-21T23:59:59.000Z
10+labels:
11+ - wontfix
12+body: |
13+ Inline code completion in the IDEs (VS Code, Zed, …): ghost text while
14+ typing, Copilot-style. Can mm provide it?
15+tasks: []
16+comments:
17+ - id: 1
18+ author:
19+ name: k33g
20+ email: ph.charriere@gmail.com
21+ createdAt: 2026-10-21T23:59:59.000Z
22+ body: |
23+ Answer (2026-10-21): wontfix — inline completion is not mm's job, and
24+ ACP could not carry it anyway. The IDE's completion feature should
25+ talk to a model server directly; mm stays in the agent panel. Taken
26+ out of v0.9.0, labelled wontfix (user's decision).
27+
28+ ## Why not mm
29+ - The protocol. ACP (how mm talks to editors) is sessions and
30+ prompts: no method or capability for inline completion was found;
31+ the only "autocomplete" in ACP clients is slash-command completion
32+ in the chat panel. Inline completion lives in the editor's own
33+ completion feature (or LSP).
34+ https://codecompanion.olimorris.dev/usage/acp-protocol
35+ https://mintlify.com/agentclientprotocol/typescript-sdk/concepts/protocol-overview
36+ - Latency and shape. A completion fires on every typing pause and
37+ must answer in a few hundred ms, with a "prefix + suffix"
38+ (fill-in-the-middle, FIM) prompt. An mm turn is a system prompt,
39+ tool schemas, a history, sometimes several round trips: seconds.
40+
41+ ## What the editors do today (web research, 2026-10-21, not tried here)
42+ - Zed: since February 2026 "edit predictions" accept several
43+ providers, Ollama among them; the official docs say it is set up
44+ separately from the agent — so mm in the agent panel and a local
45+ completion model coexist.
46+ https://zed.dev/blog/edit-prediction-providers
47+ https://zed.dev/docs/ai/use-a-local-model
48+ https://www.alternativeto.net/news/2026/2/zed-editor-adds-multiple-edit-prediction-providers-and-pluggable-architecture/
49+ - VS Code: Continue's tab autocomplete with Ollama and a small code
50+ model (e.g. qwen2.5-coder:1.5b) is the best documented path. One
51+ single source claims Continue was acquired and its repo frozen in
52+ June 2026 — it contradicts the July 2026 docs: to check. Other
53+ extensions exist (Ollama Copilot).
54+ https://gitea.varghacsongor.hu/GithubMirror/continue/src/commit/4fd276fbb7c8baad3feb0351df1acccfd21fb8d4/docs/customize/deep-dives/autocomplete.mdx
55+ https://tech-insider.org/fr/ollama-vscode-assistant-ia-local-2026/
56+ https://awesome.ecosyste.ms/projects/github.com%2Fgnana997%2Follama-copilot
57+
58+ ## Measured on llmman (this sandbox, 2026-10-21, Qwen3.5 0.8B)
59+ | endpoint | result |
60+ | /v1/completions (raw) | works: 0.11–0.17 s for 12 tokens, warm |
61+ | /infill (llama.cpp FIM, used by llama.vim / llama.vscode) | 404, not exposed |
62+ | /api/generate + "suffix" (Ollama FIM) | 400: llmman does not support the "suffix" field of /api/generate |
63+ So today an editor's completion needs a real Ollama or llama-server
64+ beside llmman; llmman is fine for the agent. Possible request to
65+ llmman: accept "suffix" or expose /infill.
66+
67+ ## If anything is done later
68+ - Docs only (EN+FR): an explanation (why not mm; how mm and a
69+ completion model share one editor) and a how-to (Zed edit
70+ predictions / VS Code Continue on Ollama or llama-server with a
71+ small FIM-trained model, mm in the agent panel). It needs a real
72+ try in an editor — not possible in this sandbox.
73+ - A separate idea, not this ticket: mm's possible contribution would
74+ be project context (neighbouring files, LSP symbols, search_docs)
75+ injected into a FIM prompt — what makes Copilot more than a bare
76+ model. A project of its own.
deleted .tickets/issues/0116-how-to-use-mm-for-completion.yaml +0 -14
deleted file mode 100644
@@ -1,14 +0,0 @@
1-# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 116
3-title: How to use mm for completion?
4-state: open
5-author:
6- name: k33g
7- email: ph.charriere@gmail.com
8-createdAt: 2026-10-21T11:38:42.420Z
9-updatedAt: 2026-10-21T11:44:57.748Z
10-labels: []
11-milestone: v0.9.0
12-body: ""
13-tasks: []
14-comments: []
deleted file mode 100644
@@ -1,14 +0,0 @@
1-# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 116
3-title: How to use mm for completion?
4-state: open
5-author:
6- name: k33g
7- email: ph.charriere@gmail.com
8-createdAt: 2026-10-21T11:38:42.420Z
9-updatedAt: 2026-10-21T11:44:57.748Z
10-labels: []
11-milestone: v0.9.0
12-body: ""
13-tasks: []
14-comments: []
added .tickets/issues/0119-custom-tools-as-wasm-modules.yaml +123 -0
new file mode 100644
@@ -0,0 +1,123 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 119
3+title: Custom tools as WASM modules (wazero)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-21T23:59:59.000Z
9+updatedAt: 2026-10-21T23:59:59.000Z
10+labels: []
11+milestone: v0.9.0
12+epic: Builtin tools
13+related:
14+ - 46
15+ - 120
16+body: |
17+ A declared tool (ticket 88, docs/en/how-to/declare-a-custom-tool.md) runs
18+ a program with ALL the user's rights. This ticket adds a second kind of
19+ declared tool: a WebAssembly module run inside mm by wazero (pure Go, so
20+ mm stays a static, CGO-free binary) — the first measured step of ticket
21+ 46's "sandbox by capability".
22+
23+ User's decisions (2026-10-21):
24+ - The module is an ordinary WASI command (TinyGo `-target=wasip1`, a
25+ `main`). The call's arguments — checked, typed, defaults applied, as
26+ for a `command` tool — arrive as ONE JSON object on stdin; stdout and
27+ stderr are the result; a non-zero exit is a failure. Same shape as the
28+ hooks. No argv template.
29+ - Capabilities: nothing by default — no file, no environment variable,
30+ no network. An entry may open project folders: `read: [notes]`
31+ (read-only) and `write: [out]`.
32+ - A demo in demos/, modules built with TinyGo.
33+
34+ ```yaml
35+ tools:
36+ - name: word_stats
37+ description: Count the words, lines and characters of a text.
38+ parameters:
39+ text: { type: string }
40+ wasm: ./tools/word_stats.wasm
41+ ```
42+tasks:
43+ - id: 1
44+ title: "config: `wasm`, `read`, `write` on a tools entry, validated (wasm XOR command)"
45+ state: closed
46+ priority: none
47+ author:
48+ name: k33g
49+ email: ph.charriere@gmail.com
50+ createdAt: 2026-10-21T23:59:59.000Z
51+ updatedAt: 2026-10-21T23:59:59.000Z
52+ - id: 2
53+ title: "runtime: wazero runner (compile once, JSON on stdin, timeout, mounted folders), wired into the declared tool in every front end"
54+ state: closed
55+ priority: none
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-21T23:59:59.000Z
60+ updatedAt: 2026-10-21T23:59:59.000Z
61+ - id: 3
62+ title: "demos/wasm-tools: TinyGo modules, build script, agent.yaml, live run"
63+ state: closed
64+ priority: none
65+ author:
66+ name: k33g
67+ email: ph.charriere@gmail.com
68+ createdAt: 2026-10-21T23:59:59.000Z
69+ updatedAt: 2026-10-21T23:59:59.000Z
70+ - id: 4
71+ title: "docs EN+FR (how-to, reference, explanation), diagram, ticket 46"
72+ state: closed
73+ priority: none
74+ author:
75+ name: k33g
76+ email: ph.charriere@gmail.com
77+ createdAt: 2026-10-21T23:59:59.000Z
78+ updatedAt: 2026-10-21T23:59:59.000Z
79+comments:
80+ - id: 1
81+ author:
82+ name: k33g
83+ email: ph.charriere@gmail.com
84+ createdAt: 2026-10-21T23:59:59.000Z
85+ body: |
86+ Implemented on feature/wasm-custom-tools (uncommitted).
87+
88+ - config (tools.go): `wasm` (relative to the agent file, resolved at
89+ load, must exist), `read` / `write` (relative to the working
90+ directory; refused: absolute, `.`, above it, twice; only with
91+ wasm); command XOR wasm.
92+ - internal/wasmtool (new): one wazero runtime per process, each module
93+ compiled once on its first call, a fresh instance per call (several
94+ may overlap). Sandbox: stdin = the JSON, args = the module name, no
95+ env, real clock + crypto rand, folders mounted at /<name> (read-only
96+ or not), 256 MiB memory cap, output kept up to 1 MiB, timeout and
97+ cancellation stop the module. Errors as text, in runArgv's shape.
98+ - tools/declared.go: a `job` (shown text + run) built by prepare();
99+ the line and evented paths are unchanged otherwise (consent, loop
100+ detector, display). callValues now returns typed values: argv gets
101+ them as text, wasm as JSON.
102+ - Spike measurements (linux/arm64 sandbox): compile of a ~550 KB
103+ TinyGo module 176–183 ms once; a call 0.2–0.5 ms. mm binary 38.97
104+ → 42.87 MB. CGO_ENABLED=0 cross-builds of the 5 release targets ok.
105+ - Trap: TinyGo's WASI layer refuses "./notes/a.md" and ".." — modules
106+ must clean paths (path.Join); documented.
107+ - Tests: wasmtool (10, on testdata/probe.wasm, 145 KB, built from
108+ testdata/probe/main.go without encoding/json), config (resolution,
109+ refusals), tools (typed JSON, folders in the session directory,
110+ consent and invalid calls).
111+ - Demo demos/wasm-tools: Qwen3.5 4B on llmman, word_stats /
112+ search_notes / read_note (read notes/) / save_note (write out/);
113+ one live `mm -p` run each, all right tool first call; read_note
114+ with ../agent.yaml refused by the sandbox. Prebuilt modules
115+ committed (550–593 KB each), ./build.sh rebuilds them.
116+ - Docs EN+FR: new how-to write-a-wasm-tool, reference configuration
117+ (keys, errors, example) and tools (Wasm tools table), explanation
118+ declared-tools (Wasm tools section; ticket 46 alternative
119+ rewritten), declare-a-custom-tool variant, indexes, README demo
120+ row, packages.drawio (wasmtool, wazero).
121+ - Not done: the window and ACP paths seen live (unit-tested through
122+ the evented path only); quality gate (qlty absent); modules from
123+ other languages.
new file mode 100644
@@ -0,0 +1,123 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 119
3+title: Custom tools as WASM modules (wazero)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-21T23:59:59.000Z
9+updatedAt: 2026-10-21T23:59:59.000Z
10+labels: []
11+milestone: v0.9.0
12+epic: Builtin tools
13+related:
14+ - 46
15+ - 120
16+body: |
17+ A declared tool (ticket 88, docs/en/how-to/declare-a-custom-tool.md) runs
18+ a program with ALL the user's rights. This ticket adds a second kind of
19+ declared tool: a WebAssembly module run inside mm by wazero (pure Go, so
20+ mm stays a static, CGO-free binary) — the first measured step of ticket
21+ 46's "sandbox by capability".
22+
23+ User's decisions (2026-10-21):
24+ - The module is an ordinary WASI command (TinyGo `-target=wasip1`, a
25+ `main`). The call's arguments — checked, typed, defaults applied, as
26+ for a `command` tool — arrive as ONE JSON object on stdin; stdout and
27+ stderr are the result; a non-zero exit is a failure. Same shape as the
28+ hooks. No argv template.
29+ - Capabilities: nothing by default — no file, no environment variable,
30+ no network. An entry may open project folders: `read: [notes]`
31+ (read-only) and `write: [out]`.
32+ - A demo in demos/, modules built with TinyGo.
33+
34+ ```yaml
35+ tools:
36+ - name: word_stats
37+ description: Count the words, lines and characters of a text.
38+ parameters:
39+ text: { type: string }
40+ wasm: ./tools/word_stats.wasm
41+ ```
42+tasks:
43+ - id: 1
44+ title: "config: `wasm`, `read`, `write` on a tools entry, validated (wasm XOR command)"
45+ state: closed
46+ priority: none
47+ author:
48+ name: k33g
49+ email: ph.charriere@gmail.com
50+ createdAt: 2026-10-21T23:59:59.000Z
51+ updatedAt: 2026-10-21T23:59:59.000Z
52+ - id: 2
53+ title: "runtime: wazero runner (compile once, JSON on stdin, timeout, mounted folders), wired into the declared tool in every front end"
54+ state: closed
55+ priority: none
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-21T23:59:59.000Z
60+ updatedAt: 2026-10-21T23:59:59.000Z
61+ - id: 3
62+ title: "demos/wasm-tools: TinyGo modules, build script, agent.yaml, live run"
63+ state: closed
64+ priority: none
65+ author:
66+ name: k33g
67+ email: ph.charriere@gmail.com
68+ createdAt: 2026-10-21T23:59:59.000Z
69+ updatedAt: 2026-10-21T23:59:59.000Z
70+ - id: 4
71+ title: "docs EN+FR (how-to, reference, explanation), diagram, ticket 46"
72+ state: closed
73+ priority: none
74+ author:
75+ name: k33g
76+ email: ph.charriere@gmail.com
77+ createdAt: 2026-10-21T23:59:59.000Z
78+ updatedAt: 2026-10-21T23:59:59.000Z
79+comments:
80+ - id: 1
81+ author:
82+ name: k33g
83+ email: ph.charriere@gmail.com
84+ createdAt: 2026-10-21T23:59:59.000Z
85+ body: |
86+ Implemented on feature/wasm-custom-tools (uncommitted).
87+
88+ - config (tools.go): `wasm` (relative to the agent file, resolved at
89+ load, must exist), `read` / `write` (relative to the working
90+ directory; refused: absolute, `.`, above it, twice; only with
91+ wasm); command XOR wasm.
92+ - internal/wasmtool (new): one wazero runtime per process, each module
93+ compiled once on its first call, a fresh instance per call (several
94+ may overlap). Sandbox: stdin = the JSON, args = the module name, no
95+ env, real clock + crypto rand, folders mounted at /<name> (read-only
96+ or not), 256 MiB memory cap, output kept up to 1 MiB, timeout and
97+ cancellation stop the module. Errors as text, in runArgv's shape.
98+ - tools/declared.go: a `job` (shown text + run) built by prepare();
99+ the line and evented paths are unchanged otherwise (consent, loop
100+ detector, display). callValues now returns typed values: argv gets
101+ them as text, wasm as JSON.
102+ - Spike measurements (linux/arm64 sandbox): compile of a ~550 KB
103+ TinyGo module 176–183 ms once; a call 0.2–0.5 ms. mm binary 38.97
104+ → 42.87 MB. CGO_ENABLED=0 cross-builds of the 5 release targets ok.
105+ - Trap: TinyGo's WASI layer refuses "./notes/a.md" and ".." — modules
106+ must clean paths (path.Join); documented.
107+ - Tests: wasmtool (10, on testdata/probe.wasm, 145 KB, built from
108+ testdata/probe/main.go without encoding/json), config (resolution,
109+ refusals), tools (typed JSON, folders in the session directory,
110+ consent and invalid calls).
111+ - Demo demos/wasm-tools: Qwen3.5 4B on llmman, word_stats /
112+ search_notes / read_note (read notes/) / save_note (write out/);
113+ one live `mm -p` run each, all right tool first call; read_note
114+ with ../agent.yaml refused by the sandbox. Prebuilt modules
115+ committed (550–593 KB each), ./build.sh rebuilds them.
116+ - Docs EN+FR: new how-to write-a-wasm-tool, reference configuration
117+ (keys, errors, example) and tools (Wasm tools table), explanation
118+ declared-tools (Wasm tools section; ticket 46 alternative
119+ rewritten), declare-a-custom-tool variant, indexes, README demo
120+ row, packages.drawio (wasmtool, wazero).
121+ - Not done: the window and ACP paths seen live (unit-tested through
122+ the evented path only); quality gate (qlty absent); modules from
123+ other languages.
added .tickets/issues/0120-hooks-as-wasm-modules.yaml +123 -0
new file mode 100644
@@ -0,0 +1,123 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 120
3+title: Hooks as WASM modules (wazero)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-21T23:59:59.000Z
9+updatedAt: 2026-10-21T23:59:59.000Z
10+labels: []
11+milestone: v0.9.0
12+epic: Builtin tools
13+related:
14+ - 46
15+ - 119
16+body: |
17+ The same thing as ticket 119, for hooks: a hook entry may name a
18+ WebAssembly module (`wasm:`) instead of a shell command (`command:`).
19+ mm runs it itself with wazero, in the sandbox of ticket 119.
20+
21+ A hook already has the right contract: one JSON object on stdin, an
22+ optional JSON reply on stdout ({"action": "deny", "reason": …}). So the
23+ module is an ordinary WASI command (TinyGo `-target=wasip1`), reading the
24+ same input and printing the same reply as a script hook.
25+
26+ - Nothing visible by default; `read:` / `write:` folders of the working
27+ directory, same rules as tools.
28+ - Environment: MM_HOOK_EVENT only (one module can serve several events,
29+ as with scripts). Not MM_CONFIG_DIR: a host path means nothing inside
30+ the sandbox.
31+ - timeout, onError, matcher: unchanged.
32+ - This is ticket 46's task 2: bash-guard as a WASM hook vs the Node one —
33+ time per call, size, capabilities, author experience.
34+
35+ ```yaml
36+ hooks:
37+ preToolCall:
38+ - matcher: ^bash$
39+ wasm: ./hooks/bash_guard.wasm
40+ onError: fail
41+ ```
42+tasks:
43+ - id: 1
44+ title: "config: `wasm`, `read`, `write` on a hook entry (wasm XOR command), validation shared with tools"
45+ state: closed
46+ priority: none
47+ author:
48+ name: k33g
49+ email: ph.charriere@gmail.com
50+ createdAt: 2026-10-21T23:59:59.000Z
51+ updatedAt: 2026-10-21T23:59:59.000Z
52+ - id: 2
53+ title: "runtime: wasmtool Exec (stdout and stderr apart, env), the hooks runner dispatches command / wasm"
54+ state: closed
55+ priority: none
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-21T23:59:59.000Z
60+ updatedAt: 2026-10-21T23:59:59.000Z
61+ - id: 3
62+ title: "demos/wasm-hooks: no_secrets, bash_guard, audit in TinyGo; measured against the Node hooks of demos/hooks"
63+ state: closed
64+ priority: none
65+ author:
66+ name: k33g
67+ email: ph.charriere@gmail.com
68+ createdAt: 2026-10-21T23:59:59.000Z
69+ updatedAt: 2026-10-21T23:59:59.000Z
70+ - id: 4
71+ title: "docs EN+FR, diagram, tickets 46 and 119"
72+ state: closed
73+ priority: none
74+ author:
75+ name: k33g
76+ email: ph.charriere@gmail.com
77+ createdAt: 2026-10-21T23:59:59.000Z
78+ updatedAt: 2026-10-21T23:59:59.000Z
79+comments:
80+ - id: 1
81+ author:
82+ name: k33g
83+ email: ph.charriere@gmail.com
84+ createdAt: 2026-10-21T23:59:59.000Z
85+ body: |
86+ Implemented on feature/wasm-custom-tools (same branch as 119,
87+ uncommitted).
88+
89+ - config: Hook gets `wasm` (resolved against the agent file's
90+ directory in resolveHooks), `read`, `write`; command XOR wasm;
91+ IsWasm, Folders, Label (command or module file name — used by the
92+ failure line and displayHooks). Folder and module checks are now
93+ ONE function shared with tools (validateSandbox); its messages lost
94+ their prefix, so the tool one reads `tools[N] (name): wasm: no
95+ module at …` (docs updated).
96+ - wasmtool: Exec(ctx, call, stdout, stderr) with typed ends
97+ (TimeoutError, ErrCancelled, ExitError); Run (tools) is built on
98+ it; Call.Env; `Default` runner shared by tools and hooks (a module
99+ is compiled once per process).
100+ - hooks: runHook dispatches to runCommand or runWasm; runWasm keeps
101+ runCommand's caps (reply 1 MiB, stderr 300 bytes) and error texts
102+ (`timed out after`, `cancelled`), a non-zero exit reads `exit code
103+ <n>: <stderr>`. Env given to the module: MM_HOOK_EVENT only.
104+ - Tests: hooks/wasm_test.go (reply honoured, env = the event variable
105+ alone, folders, onError warn/fail + timeout), wasmtool Exec test,
106+ config tests; the probe module got three hook replies (deny,
107+ deny-env, deny-read).
108+ - Demo demos/wasm-hooks: no_secrets, bash_guard, audit (write: [logs])
109+ in TinyGo, Gemma 4 E4B on llmman; live `mm -p`: password stopped,
110+ rm -rf denied (old-drafts/ intact), audit lines written;
111+ displayHooks lists the module names. add-context not ported: it
112+ lists the working directory, which a module cannot open.
113+ - Measured (ticket 46 task 2), bash guard through hooks.Runner, 20
114+ runs after a first, twice, linux/arm64: Node v22 first 68–102 ms,
115+ median 65–67 ms; WASM first 234–248 ms (compile), median 0.56–0.58
116+ ms. Size: 954-byte .js vs 770 KB .wasm.
117+ - Docs EN+FR: new how-to write-a-wasm-hook; reference hooks (entry
118+ keys, Wasm hooks table, failures, report and display, validation
119+ errors, example); explanation hooks (new section); guard how-to
120+ variant; configuration (hooks row, tool error text); indexes;
121+ README demo row; packages.drawio (hooks → wasmtool).
122+ - Not done: hooks over ACP / in the window seen live (same runner,
123+ unit-tested); quality gate (qlty absent).
new file mode 100644
@@ -0,0 +1,123 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 120
3+title: Hooks as WASM modules (wazero)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-21T23:59:59.000Z
9+updatedAt: 2026-10-21T23:59:59.000Z
10+labels: []
11+milestone: v0.9.0
12+epic: Builtin tools
13+related:
14+ - 46
15+ - 119
16+body: |
17+ The same thing as ticket 119, for hooks: a hook entry may name a
18+ WebAssembly module (`wasm:`) instead of a shell command (`command:`).
19+ mm runs it itself with wazero, in the sandbox of ticket 119.
20+
21+ A hook already has the right contract: one JSON object on stdin, an
22+ optional JSON reply on stdout ({"action": "deny", "reason": …}). So the
23+ module is an ordinary WASI command (TinyGo `-target=wasip1`), reading the
24+ same input and printing the same reply as a script hook.
25+
26+ - Nothing visible by default; `read:` / `write:` folders of the working
27+ directory, same rules as tools.
28+ - Environment: MM_HOOK_EVENT only (one module can serve several events,
29+ as with scripts). Not MM_CONFIG_DIR: a host path means nothing inside
30+ the sandbox.
31+ - timeout, onError, matcher: unchanged.
32+ - This is ticket 46's task 2: bash-guard as a WASM hook vs the Node one —
33+ time per call, size, capabilities, author experience.
34+
35+ ```yaml
36+ hooks:
37+ preToolCall:
38+ - matcher: ^bash$
39+ wasm: ./hooks/bash_guard.wasm
40+ onError: fail
41+ ```
42+tasks:
43+ - id: 1
44+ title: "config: `wasm`, `read`, `write` on a hook entry (wasm XOR command), validation shared with tools"
45+ state: closed
46+ priority: none
47+ author:
48+ name: k33g
49+ email: ph.charriere@gmail.com
50+ createdAt: 2026-10-21T23:59:59.000Z
51+ updatedAt: 2026-10-21T23:59:59.000Z
52+ - id: 2
53+ title: "runtime: wasmtool Exec (stdout and stderr apart, env), the hooks runner dispatches command / wasm"
54+ state: closed
55+ priority: none
56+ author:
57+ name: k33g
58+ email: ph.charriere@gmail.com
59+ createdAt: 2026-10-21T23:59:59.000Z
60+ updatedAt: 2026-10-21T23:59:59.000Z
61+ - id: 3
62+ title: "demos/wasm-hooks: no_secrets, bash_guard, audit in TinyGo; measured against the Node hooks of demos/hooks"
63+ state: closed
64+ priority: none
65+ author:
66+ name: k33g
67+ email: ph.charriere@gmail.com
68+ createdAt: 2026-10-21T23:59:59.000Z
69+ updatedAt: 2026-10-21T23:59:59.000Z
70+ - id: 4
71+ title: "docs EN+FR, diagram, tickets 46 and 119"
72+ state: closed
73+ priority: none
74+ author:
75+ name: k33g
76+ email: ph.charriere@gmail.com
77+ createdAt: 2026-10-21T23:59:59.000Z
78+ updatedAt: 2026-10-21T23:59:59.000Z
79+comments:
80+ - id: 1
81+ author:
82+ name: k33g
83+ email: ph.charriere@gmail.com
84+ createdAt: 2026-10-21T23:59:59.000Z
85+ body: |
86+ Implemented on feature/wasm-custom-tools (same branch as 119,
87+ uncommitted).
88+
89+ - config: Hook gets `wasm` (resolved against the agent file's
90+ directory in resolveHooks), `read`, `write`; command XOR wasm;
91+ IsWasm, Folders, Label (command or module file name — used by the
92+ failure line and displayHooks). Folder and module checks are now
93+ ONE function shared with tools (validateSandbox); its messages lost
94+ their prefix, so the tool one reads `tools[N] (name): wasm: no
95+ module at …` (docs updated).
96+ - wasmtool: Exec(ctx, call, stdout, stderr) with typed ends
97+ (TimeoutError, ErrCancelled, ExitError); Run (tools) is built on
98+ it; Call.Env; `Default` runner shared by tools and hooks (a module
99+ is compiled once per process).
100+ - hooks: runHook dispatches to runCommand or runWasm; runWasm keeps
101+ runCommand's caps (reply 1 MiB, stderr 300 bytes) and error texts
102+ (`timed out after`, `cancelled`), a non-zero exit reads `exit code
103+ <n>: <stderr>`. Env given to the module: MM_HOOK_EVENT only.
104+ - Tests: hooks/wasm_test.go (reply honoured, env = the event variable
105+ alone, folders, onError warn/fail + timeout), wasmtool Exec test,
106+ config tests; the probe module got three hook replies (deny,
107+ deny-env, deny-read).
108+ - Demo demos/wasm-hooks: no_secrets, bash_guard, audit (write: [logs])
109+ in TinyGo, Gemma 4 E4B on llmman; live `mm -p`: password stopped,
110+ rm -rf denied (old-drafts/ intact), audit lines written;
111+ displayHooks lists the module names. add-context not ported: it
112+ lists the working directory, which a module cannot open.
113+ - Measured (ticket 46 task 2), bash guard through hooks.Runner, 20
114+ runs after a first, twice, linux/arm64: Node v22 first 68–102 ms,
115+ median 65–67 ms; WASM first 234–248 ms (compile), median 0.56–0.58
116+ ms. Size: 954-byte .js vs 770 KB .wasm.
117+ - Docs EN+FR: new how-to write-a-wasm-hook; reference hooks (entry
118+ keys, Wasm hooks table, failures, report and display, validation
119+ errors, example); explanation hooks (new section); guard how-to
120+ variant; configuration (hooks row, tool error text); indexes;
121+ README demo row; packages.drawio (hooks → wasmtool).
122+ - Not done: hooks over ACP / in the window seen live (same runner,
123+ unit-tested); quality gate (qlty absent).
modified README.md +2 -0
@@ -161,6 +161,8 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h
161161 | `demos/docs-search/` | an assistant answering from a documentation folder: nine Ratatui tutorials (Rust terminal UIs, each program checked against ratatui 0.30 by `check-tutorials.sh`), `/docs <words>` for you and the `search_docs` tool for the model; Qwen3.5 4B on llmman, measured results |
162162 | `demos/memory-search/` | an assistant that remembers a project's decisions: four earlier sessions (port, storage, logs in French, deployment as a `/compact` summary), `/memory <words>` for you and the `search_memory` tool for the model; Qwen3.5 4B on llmman, measured results — including what a word search cannot find |
163163 | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results |
164+| `demos/wasm-tools/` | a notes desk acting only through four WebAssembly tools built with TinyGo and run by mm in a sandbox: one pure computation, two that may only read `notes/`, one that may only write `out/`; Qwen3.5 4B on llmman |
165+| `demos/wasm-hooks/` | the hooks of `demos/hooks` (secret filter, `bash` guard, audit log) rewritten as WebAssembly modules built with TinyGo and run by mm in a sandbox — nothing to install; Gemma 4 E4B on llmman, measured against the Node versions |
164166
165167 ## Learn more
166168
@@ -161,6 +161,8 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h
161 | `demos/docs-search/` | an assistant answering from a documentation folder: nine Ratatui tutorials (Rust terminal UIs, each program checked against ratatui 0.30 by `check-tutorials.sh`), `/docs <words>` for you and the `search_docs` tool for the model; Qwen3.5 4B on llmman, measured results |161 | `demos/docs-search/` | an assistant answering from a documentation folder: nine Ratatui tutorials (Rust terminal UIs, each program checked against ratatui 0.30 by `check-tutorials.sh`), `/docs <words>` for you and the `search_docs` tool for the model; Qwen3.5 4B on llmman, measured results |
162 | `demos/memory-search/` | an assistant that remembers a project's decisions: four earlier sessions (port, storage, logs in French, deployment as a `/compact` summary), `/memory <words>` for you and the `search_memory` tool for the model; Qwen3.5 4B on llmman, measured results — including what a word search cannot find |162 | `demos/memory-search/` | an assistant that remembers a project's decisions: four earlier sessions (port, storage, logs in French, deployment as a `/compact` summary), `/memory <words>` for you and the `search_memory` tool for the model; Qwen3.5 4B on llmman, measured results — including what a word search cannot find |
163 | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results |163 | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results |
164+| `demos/wasm-tools/` | a notes desk acting only through four WebAssembly tools built with TinyGo and run by mm in a sandbox: one pure computation, two that may only read `notes/`, one that may only write `out/`; Qwen3.5 4B on llmman |
165+| `demos/wasm-hooks/` | the hooks of `demos/hooks` (secret filter, `bash` guard, audit log) rewritten as WebAssembly modules built with TinyGo and run by mm in a sandbox — nothing to install; Gemma 4 E4B on llmman, measured against the Node versions |
164 166
165 ## Learn more167 ## Learn more
166 168
added demos/wasm-hooks/README.en.md +69 -0
new file mode 100644
@@ -0,0 +1,69 @@
1+# WASM hooks — the hooks demo, as WebAssembly modules
2+
3+*[Version française](README.fr.md)*
4+
5+The agent of [`demos/hooks`](../hooks/README.en.md) — a small shell assistant — with three of its hooks rewritten in Go and built with TinyGo as **WebAssembly modules**. mm runs them itself (wazero), in a sandbox: no file, no network, of the environment only `MM_HOOK_EVENT` — except the folders an entry opens. **Nothing to install beside mm**: no Node.js.
6+
7+| Hook | Event | What it may see | What it shows |
8+|---|---|---|---|
9+| `hooks/no_secrets.wasm` | `preGeneration` | nothing but the question | **stop** a turn: a question holding a password or an API key never reaches the model |
10+| `hooks/bash_guard.wasm` | `preToolCall` (`matcher: ^bash$`) | nothing but the call | **deny** a tool call: `rm -r`, `sudo`, `curl … \| sh` never run, and the model reads why |
11+| `hooks/audit.wasm` | `postToolCall` + `postGeneration` | `logs/`, read-write | **observe**: one JSON line per tool call and per turn in `logs/hooks-audit.jsonl` |
12+
13+`add-context.js` is not ported: it lists the working directory, and a module can open folders inside it, not the directory itself.
14+
15+Model: **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) on **llmman**.
16+
17+## Run
18+
19+```bash
20+cd demos/wasm-hooks/playground
21+mm ../agent.yaml
22+```
23+
24+The modules are relative to `agent.yaml`; `logs/` is taken from the working directory, `playground/`. The `.wasm` files in `hooks/` are ready to use; to rebuild them after changing `hooks-src/`, with [TinyGo](https://tinygo.org) installed: `./build.sh`.
25+
26+## Seen on 2026-10-21
27+
28+`mm -p "<question>" ../agent.yaml` from `playground/`:
29+
30+```
31+> My password: hunter2 — can you store it in a file?
32+🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again.
33+
34+> Delete the old-drafts directory with rm -rf.
35+🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed]
36+🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook.
37+
38+> How many lines are in todo.txt?
39+🛠️ bash: wc -l todo.txt
40+ │ 2 todo.txt
41+🤖 There are 2 lines in todo.txt.
42+```
43+
44+`old-drafts/` was still there, and `logs/hooks-audit.jsonl` held one line per turn and one for the `wc` call. With `displayHooks: true`, the second question lists:
45+
46+```
47+ 🪝 1. preGeneration · no_secrets.wasm → continue
48+ 🪝 2. preToolCall bash · bash_guard.wasm → deny
49+ 🪝 3. postGeneration · audit.wasm → continue
50+```
51+
52+## Against the Node.js hooks
53+
54+The `bash` guard, the same three rules, run through mm's hooks runner on the same denied call, 20 runs after a first one, twice (linux/arm64 sandbox):
55+
56+| | First run | Median of 20 | Size |
57+|---|---|---|---|
58+| `demos/hooks/hooks/bash-guard.js` (Node v22) | 68–102 ms | 65–67 ms | 954 bytes, plus Node.js |
59+| `hooks/bash_guard.wasm` | 234–248 ms (the compilation, once per mm process) | 0.56–0.58 ms | 770 KB |
60+
61+## Files
62+
63+- `agent.yaml` — the agent and its `hooks:` (`wasm:`, `write:`)
64+- `hooks-src/` — the Go sources of the hooks (their own Go module, built by TinyGo)
65+- `hooks/` — the built modules
66+- `playground/` — where the agent works; `logs/` is written by the audit hook (git-ignored)
67+- `build.sh` — rebuilds `hooks/` from `hooks-src/`
68+
69+See [How to write a hook as a WebAssembly module](../../docs/en/how-to/write-a-wasm-hook.md).
new file mode 100644
@@ -0,0 +1,69 @@
1+# WASM hooks — the hooks demo, as WebAssembly modules
2+
3+*[Version française](README.fr.md)*
4+
5+The agent of [`demos/hooks`](../hooks/README.en.md) — a small shell assistant — with three of its hooks rewritten in Go and built with TinyGo as **WebAssembly modules**. mm runs them itself (wazero), in a sandbox: no file, no network, of the environment only `MM_HOOK_EVENT` — except the folders an entry opens. **Nothing to install beside mm**: no Node.js.
6+
7+| Hook | Event | What it may see | What it shows |
8+|---|---|---|---|
9+| `hooks/no_secrets.wasm` | `preGeneration` | nothing but the question | **stop** a turn: a question holding a password or an API key never reaches the model |
10+| `hooks/bash_guard.wasm` | `preToolCall` (`matcher: ^bash$`) | nothing but the call | **deny** a tool call: `rm -r`, `sudo`, `curl … \| sh` never run, and the model reads why |
11+| `hooks/audit.wasm` | `postToolCall` + `postGeneration` | `logs/`, read-write | **observe**: one JSON line per tool call and per turn in `logs/hooks-audit.jsonl` |
12+
13+`add-context.js` is not ported: it lists the working directory, and a module can open folders inside it, not the directory itself.
14+
15+Model: **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) on **llmman**.
16+
17+## Run
18+
19+```bash
20+cd demos/wasm-hooks/playground
21+mm ../agent.yaml
22+```
23+
24+The modules are relative to `agent.yaml`; `logs/` is taken from the working directory, `playground/`. The `.wasm` files in `hooks/` are ready to use; to rebuild them after changing `hooks-src/`, with [TinyGo](https://tinygo.org) installed: `./build.sh`.
25+
26+## Seen on 2026-10-21
27+
28+`mm -p "<question>" ../agent.yaml` from `playground/`:
29+
30+```
31+> My password: hunter2 — can you store it in a file?
32+🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again.
33+
34+> Delete the old-drafts directory with rm -rf.
35+🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed]
36+🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook.
37+
38+> How many lines are in todo.txt?
39+🛠️ bash: wc -l todo.txt
40+ │ 2 todo.txt
41+🤖 There are 2 lines in todo.txt.
42+```
43+
44+`old-drafts/` was still there, and `logs/hooks-audit.jsonl` held one line per turn and one for the `wc` call. With `displayHooks: true`, the second question lists:
45+
46+```
47+ 🪝 1. preGeneration · no_secrets.wasm → continue
48+ 🪝 2. preToolCall bash · bash_guard.wasm → deny
49+ 🪝 3. postGeneration · audit.wasm → continue
50+```
51+
52+## Against the Node.js hooks
53+
54+The `bash` guard, the same three rules, run through mm's hooks runner on the same denied call, 20 runs after a first one, twice (linux/arm64 sandbox):
55+
56+| | First run | Median of 20 | Size |
57+|---|---|---|---|
58+| `demos/hooks/hooks/bash-guard.js` (Node v22) | 68–102 ms | 65–67 ms | 954 bytes, plus Node.js |
59+| `hooks/bash_guard.wasm` | 234–248 ms (the compilation, once per mm process) | 0.56–0.58 ms | 770 KB |
60+
61+## Files
62+
63+- `agent.yaml` — the agent and its `hooks:` (`wasm:`, `write:`)
64+- `hooks-src/` — the Go sources of the hooks (their own Go module, built by TinyGo)
65+- `hooks/` — the built modules
66+- `playground/` — where the agent works; `logs/` is written by the audit hook (git-ignored)
67+- `build.sh` — rebuilds `hooks/` from `hooks-src/`
68+
69+See [How to write a hook as a WebAssembly module](../../docs/en/how-to/write-a-wasm-hook.md).
added demos/wasm-hooks/README.fr.md +69 -0
new file mode 100644
@@ -0,0 +1,69 @@
1+# Hooks WASM — la démo des hooks, en modules WebAssembly
2+
3+*[English version](README.en.md)*
4+
5+L'agent de [`demos/hooks`](../hooks/README.fr.md) — un petit assistant shell — avec trois de ses hooks réécrits en Go et construits avec TinyGo en **modules WebAssembly**. mm les exécute lui-même (wazero), dans un bac à sable : aucun fichier, pas de réseau, de l'environnement seulement `MM_HOOK_EVENT` — sauf les dossiers qu'une entrée ouvre. **Rien à installer à côté de mm** : pas de Node.js.
6+
7+| Hook | Événement | Ce qu'il peut voir | Ce qu'il montre |
8+|---|---|---|---|
9+| `hooks/no_secrets.wasm` | `preGeneration` | rien d'autre que la question | **arrêter** un tour : une question qui contient un mot de passe ou une clé d'API n'atteint jamais le modèle |
10+| `hooks/bash_guard.wasm` | `preToolCall` (`matcher: ^bash$`) | rien d'autre que l'appel | **refuser** un appel d'outil : `rm -r`, `sudo`, `curl … \| sh` ne s'exécutent jamais, et le modèle lit pourquoi |
11+| `hooks/audit.wasm` | `postToolCall` + `postGeneration` | `logs/`, en lecture-écriture | **observer** : une ligne JSON par appel d'outil et par tour dans `logs/hooks-audit.jsonl` |
12+
13+`add-context.js` n'est pas porté : il liste le répertoire de travail, et un module peut ouvrir des dossiers à l'intérieur, pas le répertoire lui-même.
14+
15+Modèle : **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) sur **llmman**.
16+
17+## Lancer
18+
19+```bash
20+cd demos/wasm-hooks/playground
21+mm ../agent.yaml
22+```
23+
24+Les modules sont relatifs à `agent.yaml` ; `logs/` est pris dans le répertoire de travail, `playground/`. Les fichiers `.wasm` de `hooks/` sont prêts à l'emploi ; pour les reconstruire après avoir modifié `hooks-src/`, avec [TinyGo](https://tinygo.org) installé : `./build.sh`.
25+
26+## Vu le 2026-10-21
27+
28+`mm -p "<question>" ../agent.yaml` depuis `playground/` :
29+
30+```
31+> My password: hunter2 — can you store it in a file?
32+🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again.
33+
34+> Delete the old-drafts directory with rm -rf.
35+🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed]
36+🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook.
37+
38+> How many lines are in todo.txt?
39+🛠️ bash: wc -l todo.txt
40+ │ 2 todo.txt
41+🤖 There are 2 lines in todo.txt.
42+```
43+
44+`old-drafts/` était toujours là, et `logs/hooks-audit.jsonl` contenait une ligne par tour et une pour l'appel `wc`. Avec `displayHooks: true`, la deuxième question liste :
45+
46+```
47+ 🪝 1. preGeneration · no_secrets.wasm → continue
48+ 🪝 2. preToolCall bash · bash_guard.wasm → deny
49+ 🪝 3. postGeneration · audit.wasm → continue
50+```
51+
52+## Face aux hooks Node.js
53+
54+La garde `bash`, les trois mêmes règles, exécutée par le lanceur de hooks de mm sur le même appel refusé, 20 exécutions après une première, deux fois (sandbox linux/arm64) :
55+
56+| | Première exécution | Médiane de 20 | Taille |
57+|---|---|---|---|
58+| `demos/hooks/hooks/bash-guard.js` (Node v22) | 68 à 102 ms | 65 à 67 ms | 954 octets, plus Node.js |
59+| `hooks/bash_guard.wasm` | 234 à 248 ms (la compilation, une fois par processus mm) | 0,56 à 0,58 ms | 770 Ko |
60+
61+## Fichiers
62+
63+- `agent.yaml` — l'agent et ses `hooks:` (`wasm:`, `write:`)
64+- `hooks-src/` — les sources Go des hooks (leur propre module Go, construit par TinyGo)
65+- `hooks/` — les modules construits
66+- `playground/` — là où l'agent travaille ; `logs/` est écrit par le hook d'audit (ignoré par git)
67+- `build.sh` — reconstruit `hooks/` à partir de `hooks-src/`
68+
69+Voir [Écrire un hook sous forme de module WebAssembly](../../docs/fr/how-to/write-a-wasm-hook.md).
new file mode 100644
@@ -0,0 +1,69 @@
1+# Hooks WASM — la démo des hooks, en modules WebAssembly
2+
3+*[English version](README.en.md)*
4+
5+L'agent de [`demos/hooks`](../hooks/README.fr.md) — un petit assistant shell — avec trois de ses hooks réécrits en Go et construits avec TinyGo en **modules WebAssembly**. mm les exécute lui-même (wazero), dans un bac à sable : aucun fichier, pas de réseau, de l'environnement seulement `MM_HOOK_EVENT` — sauf les dossiers qu'une entrée ouvre. **Rien à installer à côté de mm** : pas de Node.js.
6+
7+| Hook | Événement | Ce qu'il peut voir | Ce qu'il montre |
8+|---|---|---|---|
9+| `hooks/no_secrets.wasm` | `preGeneration` | rien d'autre que la question | **arrêter** un tour : une question qui contient un mot de passe ou une clé d'API n'atteint jamais le modèle |
10+| `hooks/bash_guard.wasm` | `preToolCall` (`matcher: ^bash$`) | rien d'autre que l'appel | **refuser** un appel d'outil : `rm -r`, `sudo`, `curl … \| sh` ne s'exécutent jamais, et le modèle lit pourquoi |
11+| `hooks/audit.wasm` | `postToolCall` + `postGeneration` | `logs/`, en lecture-écriture | **observer** : une ligne JSON par appel d'outil et par tour dans `logs/hooks-audit.jsonl` |
12+
13+`add-context.js` n'est pas porté : il liste le répertoire de travail, et un module peut ouvrir des dossiers à l'intérieur, pas le répertoire lui-même.
14+
15+Modèle : **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) sur **llmman**.
16+
17+## Lancer
18+
19+```bash
20+cd demos/wasm-hooks/playground
21+mm ../agent.yaml
22+```
23+
24+Les modules sont relatifs à `agent.yaml` ; `logs/` est pris dans le répertoire de travail, `playground/`. Les fichiers `.wasm` de `hooks/` sont prêts à l'emploi ; pour les reconstruire après avoir modifié `hooks-src/`, avec [TinyGo](https://tinygo.org) installé : `./build.sh`.
25+
26+## Vu le 2026-10-21
27+
28+`mm -p "<question>" ../agent.yaml` depuis `playground/` :
29+
30+```
31+> My password: hunter2 — can you store it in a file?
32+🤖 🔐 Your message looks like it contains a password. It was not sent to the model. Remove it and ask again.
33+
34+> Delete the old-drafts directory with rm -rf.
35+🪝 bash [denied by a hook: recursive deletion (rm -r) is not allowed]
36+🤖 The command was refused because recursive deletion (`rm -r`) is not allowed by a hook.
37+
38+> How many lines are in todo.txt?
39+🛠️ bash: wc -l todo.txt
40+ │ 2 todo.txt
41+🤖 There are 2 lines in todo.txt.
42+```
43+
44+`old-drafts/` était toujours là, et `logs/hooks-audit.jsonl` contenait une ligne par tour et une pour l'appel `wc`. Avec `displayHooks: true`, la deuxième question liste :
45+
46+```
47+ 🪝 1. preGeneration · no_secrets.wasm → continue
48+ 🪝 2. preToolCall bash · bash_guard.wasm → deny
49+ 🪝 3. postGeneration · audit.wasm → continue
50+```
51+
52+## Face aux hooks Node.js
53+
54+La garde `bash`, les trois mêmes règles, exécutée par le lanceur de hooks de mm sur le même appel refusé, 20 exécutions après une première, deux fois (sandbox linux/arm64) :
55+
56+| | Première exécution | Médiane de 20 | Taille |
57+|---|---|---|---|
58+| `demos/hooks/hooks/bash-guard.js` (Node v22) | 68 à 102 ms | 65 à 67 ms | 954 octets, plus Node.js |
59+| `hooks/bash_guard.wasm` | 234 à 248 ms (la compilation, une fois par processus mm) | 0,56 à 0,58 ms | 770 Ko |
60+
61+## Fichiers
62+
63+- `agent.yaml` — l'agent et ses `hooks:` (`wasm:`, `write:`)
64+- `hooks-src/` — les sources Go des hooks (leur propre module Go, construit par TinyGo)
65+- `hooks/` — les modules construits
66+- `playground/` — là où l'agent travaille ; `logs/` est écrit par le hook d'audit (ignoré par git)
67+- `build.sh` — reconstruit `hooks/` à partir de `hooks-src/`
68+
69+Voir [Écrire un hook sous forme de module WebAssembly](../../docs/fr/how-to/write-a-wasm-hook.md).
added demos/wasm-hooks/agent.yaml +64 -0
new file mode 100644
@@ -0,0 +1,64 @@
1+# Agent settings — the hooks demo again (demos/hooks), with its hooks written
2+# as WebAssembly modules instead of Node.js scripts (ticket 120). Each hook is
3+# built with TinyGo (hooks-src/ → hooks/, ./build.sh) and run by mm itself, in
4+# a sandbox: no file, no environment variable but MM_HOOK_EVENT, no network —
5+# except the folders its entry opens. Nothing to install beside mm.
6+#
7+# cd demos/wasm-hooks/playground
8+# mm ../agent.yaml
9+provider: llmman
10+model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
11+
12+sampling:
13+ temperature: 0.2
14+ parallel_tool_calls: false
15+
16+firstTokenTimeout: 3m
17+watchdogTimeout: 3m
18+maxTurns: 8
19+
20+bashTool: true
21+editTools: false
22+planTool: false
23+fetchContentTool: false
24+askUserTool: false
25+skillsDir: ""
26+
27+yolo: true
28+
29+agentsMd:
30+ enabled: false
31+
32+acp:
33+ name: wasm-hooks-demo
34+ title: 🪝 WASM hooks demo
35+
36+hooks:
37+ preGeneration:
38+ # onError: fail — a guard that cannot run must not let the question through.
39+ # No folder: it sees the question, nothing else.
40+ - wasm: ./hooks/no_secrets.wasm
41+ timeout: 5s
42+ onError: fail
43+ preToolCall:
44+ - matcher: ^bash$
45+ wasm: ./hooks/bash_guard.wasm
46+ timeout: 5s
47+ onError: fail
48+ postToolCall:
49+ # Writes into logs/ only (relative to the working directory: playground/).
50+ # onError: ignore — losing an audit line must not disturb the session.
51+ - wasm: ./hooks/audit.wasm
52+ write: [logs]
53+ onError: ignore
54+ postGeneration:
55+ - wasm: ./hooks/audit.wasm
56+ write: [logs]
57+ onError: ignore
58+
59+system: |
60+ You are a shell assistant working in the current directory.
61+ Use the bash tool to look at files and to run commands; never guess what a
62+ file contains. Answer in one or two short sentences, in the user's language.
63+ If a tool result says "[denied by a hook: …]", the command was refused:
64+ tell the user why, and do not try to get around the refusal.
new file mode 100644
@@ -0,0 +1,64 @@
1+# Agent settings — the hooks demo again (demos/hooks), with its hooks written
2+# as WebAssembly modules instead of Node.js scripts (ticket 120). Each hook is
3+# built with TinyGo (hooks-src/ → hooks/, ./build.sh) and run by mm itself, in
4+# a sandbox: no file, no environment variable but MM_HOOK_EVENT, no network —
5+# except the folders its entry opens. Nothing to install beside mm.
6+#
7+# cd demos/wasm-hooks/playground
8+# mm ../agent.yaml
9+provider: llmman
10+model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
11+
12+sampling:
13+ temperature: 0.2
14+ parallel_tool_calls: false
15+
16+firstTokenTimeout: 3m
17+watchdogTimeout: 3m
18+maxTurns: 8
19+
20+bashTool: true
21+editTools: false
22+planTool: false
23+fetchContentTool: false
24+askUserTool: false
25+skillsDir: ""
26+
27+yolo: true
28+
29+agentsMd:
30+ enabled: false
31+
32+acp:
33+ name: wasm-hooks-demo
34+ title: 🪝 WASM hooks demo
35+
36+hooks:
37+ preGeneration:
38+ # onError: fail — a guard that cannot run must not let the question through.
39+ # No folder: it sees the question, nothing else.
40+ - wasm: ./hooks/no_secrets.wasm
41+ timeout: 5s
42+ onError: fail
43+ preToolCall:
44+ - matcher: ^bash$
45+ wasm: ./hooks/bash_guard.wasm
46+ timeout: 5s
47+ onError: fail
48+ postToolCall:
49+ # Writes into logs/ only (relative to the working directory: playground/).
50+ # onError: ignore — losing an audit line must not disturb the session.
51+ - wasm: ./hooks/audit.wasm
52+ write: [logs]
53+ onError: ignore
54+ postGeneration:
55+ - wasm: ./hooks/audit.wasm
56+ write: [logs]
57+ onError: ignore
58+
59+system: |
60+ You are a shell assistant working in the current directory.
61+ Use the bash tool to look at files and to run commands; never guess what a
62+ file contains. Answer in one or two short sentences, in the user's language.
63+ If a tool result says "[denied by a hook: …]", the command was refused:
64+ tell the user why, and do not try to get around the refusal.
added demos/wasm-hooks/build.sh +10 -0
new file mode 100755
@@ -0,0 +1,10 @@
1+#!/bin/sh
2+# Builds the demo's WASM hooks with TinyGo: hooks-src/<name>/ → hooks/<name>.wasm.
3+# cd demos/wasm-hooks && ./build.sh
4+set -eu
5+cd "$(dirname "$0")/hooks-src"
6+for dir in */; do
7+ name=${dir%/}
8+ tinygo build -o "../hooks/$name.wasm" -target=wasip1 -no-debug "./$name"
9+ echo "built hooks/$name.wasm ($(wc -c < "../hooks/$name.wasm") bytes)"
10+done
new file mode 100755
@@ -0,0 +1,10 @@
1+#!/bin/sh
2+# Builds the demo's WASM hooks with TinyGo: hooks-src/<name>/ → hooks/<name>.wasm.
3+# cd demos/wasm-hooks && ./build.sh
4+set -eu
5+cd "$(dirname "$0")/hooks-src"
6+for dir in */; do
7+ name=${dir%/}
8+ tinygo build -o "../hooks/$name.wasm" -target=wasip1 -no-debug "./$name"
9+ echo "built hooks/$name.wasm ($(wc -c < "../hooks/$name.wasm") bytes)"
10+done
added demos/wasm-hooks/hooks-src/audit/main.go +60 -0
new file mode 100644
@@ -0,0 +1,60 @@
1+// audit is a postToolCall and postGeneration hook: it appends one JSON line
2+// per event to logs/hooks-audit.jsonl. Its entry opens logs/ for writing —
3+// the only folder it can see, so it could not read the files it audits.
4+//
5+// It only observes, so it prints nothing on stdout. One module serves two
6+// events: the event's name is in the input (and in MM_HOOK_EVENT).
7+package main
8+
9+import (
10+ "encoding/json"
11+ "fmt"
12+ "os"
13+ "time"
14+)
15+
16+type input struct {
17+ Event string `json:"event"`
18+ Prompt string `json:"prompt"`
19+ Tool *struct {
20+ Name string `json:"name"`
21+ Input any `json:"input"`
22+ } `json:"tool"`
23+ Output string `json:"output"`
24+ Answer string `json:"answer"`
25+ Error string `json:"error"`
26+}
27+
28+func clip(s string, n int) string {
29+ if r := []rune(s); len(r) > n {
30+ return string(r[:n])
31+ }
32+ return s
33+}
34+
35+func main() {
36+ var in input
37+ if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
38+ fmt.Fprintln(os.Stderr, "audit: bad input:", err)
39+ os.Exit(2)
40+ }
41+ line := map[string]any{"at": time.Now().UTC().Format(time.RFC3339), "event": in.Event}
42+ if in.Event == "postToolCall" && in.Tool != nil {
43+ line["tool"], line["input"], line["output"] = in.Tool.Name, in.Tool.Input, clip(in.Output, 200)
44+ } else {
45+ line["prompt"], line["answer"] = in.Prompt, clip(in.Answer, 200)
46+ if in.Error != "" {
47+ line["error"] = in.Error
48+ }
49+ }
50+ f, err := os.OpenFile("logs/hooks-audit.jsonl", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
51+ if err != nil {
52+ fmt.Fprintln(os.Stderr, "audit: cannot open the log:", err)
53+ os.Exit(1)
54+ }
55+ defer f.Close()
56+ if err := json.NewEncoder(f).Encode(line); err != nil {
57+ fmt.Fprintln(os.Stderr, "audit: cannot write the log:", err)
58+ os.Exit(1)
59+ }
60+}
new file mode 100644
@@ -0,0 +1,60 @@
1+// audit is a postToolCall and postGeneration hook: it appends one JSON line
2+// per event to logs/hooks-audit.jsonl. Its entry opens logs/ for writing —
3+// the only folder it can see, so it could not read the files it audits.
4+//
5+// It only observes, so it prints nothing on stdout. One module serves two
6+// events: the event's name is in the input (and in MM_HOOK_EVENT).
7+package main
8+
9+import (
10+ "encoding/json"
11+ "fmt"
12+ "os"
13+ "time"
14+)
15+
16+type input struct {
17+ Event string `json:"event"`
18+ Prompt string `json:"prompt"`
19+ Tool *struct {
20+ Name string `json:"name"`
21+ Input any `json:"input"`
22+ } `json:"tool"`
23+ Output string `json:"output"`
24+ Answer string `json:"answer"`
25+ Error string `json:"error"`
26+}
27+
28+func clip(s string, n int) string {
29+ if r := []rune(s); len(r) > n {
30+ return string(r[:n])
31+ }
32+ return s
33+}
34+
35+func main() {
36+ var in input
37+ if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
38+ fmt.Fprintln(os.Stderr, "audit: bad input:", err)
39+ os.Exit(2)
40+ }
41+ line := map[string]any{"at": time.Now().UTC().Format(time.RFC3339), "event": in.Event}
42+ if in.Event == "postToolCall" && in.Tool != nil {
43+ line["tool"], line["input"], line["output"] = in.Tool.Name, in.Tool.Input, clip(in.Output, 200)
44+ } else {
45+ line["prompt"], line["answer"] = in.Prompt, clip(in.Answer, 200)
46+ if in.Error != "" {
47+ line["error"] = in.Error
48+ }
49+ }
50+ f, err := os.OpenFile("logs/hooks-audit.jsonl", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
51+ if err != nil {
52+ fmt.Fprintln(os.Stderr, "audit: cannot open the log:", err)
53+ os.Exit(1)
54+ }
55+ defer f.Close()
56+ if err := json.NewEncoder(f).Encode(line); err != nil {
57+ fmt.Fprintln(os.Stderr, "audit: cannot write the log:", err)
58+ os.Exit(1)
59+ }
60+}
added demos/wasm-hooks/hooks-src/bash_guard/main.go +44 -0
new file mode 100644
@@ -0,0 +1,44 @@
1+// bash_guard is a preToolCall hook (matcher ^bash$): it refuses dangerous
2+// shell commands. The same rules as demos/hooks/hooks/bash-guard.js, as a
3+// WASM module that sees no file and no environment.
4+//
5+// mm writes {"event":"preToolCall","tool":{"name":"bash","input":{"command":"…"}},…}.
6+// Printing {"action":"deny","reason":"…"} means the command never runs; the
7+// model reads "[denied by a hook: <reason>]". Printing nothing lets it run.
8+package main
9+
10+import (
11+ "encoding/json"
12+ "fmt"
13+ "os"
14+ "regexp"
15+)
16+
17+var rules = []struct {
18+ reason string
19+ pattern *regexp.Regexp
20+}{
21+ {"recursive deletion (rm -r) is not allowed", regexp.MustCompile(`\brm\s+(-\w*\s+)*-\w*[rR]`)},
22+ {"sudo is not allowed", regexp.MustCompile(`\bsudo\b`)},
23+ {"piping a download into a shell is not allowed", regexp.MustCompile(`\b(curl|wget)\b[^|]*\|\s*(ba|z)?sh\b`)},
24+}
25+
26+func main() {
27+ var in struct {
28+ Tool struct {
29+ Input struct {
30+ Command string `json:"command"`
31+ } `json:"input"`
32+ } `json:"tool"`
33+ }
34+ if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
35+ fmt.Fprintln(os.Stderr, "bash_guard: bad input:", err)
36+ os.Exit(2)
37+ }
38+ for _, r := range rules {
39+ if r.pattern.MatchString(in.Tool.Input.Command) {
40+ json.NewEncoder(os.Stdout).Encode(map[string]string{"action": "deny", "reason": r.reason})
41+ return
42+ }
43+ }
44+}
new file mode 100644
@@ -0,0 +1,44 @@
1+// bash_guard is a preToolCall hook (matcher ^bash$): it refuses dangerous
2+// shell commands. The same rules as demos/hooks/hooks/bash-guard.js, as a
3+// WASM module that sees no file and no environment.
4+//
5+// mm writes {"event":"preToolCall","tool":{"name":"bash","input":{"command":"…"}},…}.
6+// Printing {"action":"deny","reason":"…"} means the command never runs; the
7+// model reads "[denied by a hook: <reason>]". Printing nothing lets it run.
8+package main
9+
10+import (
11+ "encoding/json"
12+ "fmt"
13+ "os"
14+ "regexp"
15+)
16+
17+var rules = []struct {
18+ reason string
19+ pattern *regexp.Regexp
20+}{
21+ {"recursive deletion (rm -r) is not allowed", regexp.MustCompile(`\brm\s+(-\w*\s+)*-\w*[rR]`)},
22+ {"sudo is not allowed", regexp.MustCompile(`\bsudo\b`)},
23+ {"piping a download into a shell is not allowed", regexp.MustCompile(`\b(curl|wget)\b[^|]*\|\s*(ba|z)?sh\b`)},
24+}
25+
26+func main() {
27+ var in struct {
28+ Tool struct {
29+ Input struct {
30+ Command string `json:"command"`
31+ } `json:"input"`
32+ } `json:"tool"`
33+ }
34+ if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
35+ fmt.Fprintln(os.Stderr, "bash_guard: bad input:", err)
36+ os.Exit(2)
37+ }
38+ for _, r := range rules {
39+ if r.pattern.MatchString(in.Tool.Input.Command) {
40+ json.NewEncoder(os.Stdout).Encode(map[string]string{"action": "deny", "reason": r.reason})
41+ return
42+ }
43+ }
44+}
added demos/wasm-hooks/hooks-src/go.mod +6 -0
new file mode 100644
@@ -0,0 +1,6 @@
1+// The sources of the demo's WASM hooks. Their own module, so that `go build
2+// ./...` in mini-me does not build them: they are built with TinyGo
3+// (../build.sh).
4+module wasmhooks
5+
6+go 1.24
new file mode 100644
@@ -0,0 +1,6 @@
1+// The sources of the demo's WASM hooks. Their own module, so that `go build
2+// ./...` in mini-me does not build them: they are built with TinyGo
3+// (../build.sh).
4+module wasmhooks
5+
6+go 1.24
added demos/wasm-hooks/hooks-src/no_secrets/main.go +45 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-hooks/hooks/audit.wasm +0 -0
new file mode 100755
Binary files /dev/null and b/demos/wasm-hooks/hooks/audit.wasm differ
new file mode 100755
Binary files /dev/null and b/demos/wasm-hooks/hooks/audit.wasm differBinary files /dev/null and b/demos/wasm-hooks/hooks/audit.wasm differ
added demos/wasm-hooks/hooks/bash_guard.wasm +0 -0
new file mode 100755
Binary files /dev/null and b/demos/wasm-hooks/hooks/bash_guard.wasm differ
new file mode 100755
Binary files /dev/null and b/demos/wasm-hooks/hooks/bash_guard.wasm differBinary files /dev/null and b/demos/wasm-hooks/hooks/bash_guard.wasm differ
added demos/wasm-hooks/hooks/no_secrets.wasm +0 -0
new file mode 100755
Binary files /dev/null and b/demos/wasm-hooks/hooks/no_secrets.wasm differ
new file mode 100755
Binary files /dev/null and b/demos/wasm-hooks/hooks/no_secrets.wasm differBinary files /dev/null and b/demos/wasm-hooks/hooks/no_secrets.wasm differ
added demos/wasm-hooks/playground/logs/.gitignore +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+# Written by the audit hook: everything here is generated.
2+*
3+!.gitignore
new file mode 100644
@@ -0,0 +1,3 @@
1+# Written by the audit hook: everything here is generated.
2+*
3+!.gitignore
added demos/wasm-hooks/playground/notes.md +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+# Notes
2+
3+The meeting is on Friday.
new file mode 100644
@@ -0,0 +1,3 @@
1+# Notes
2+
3+The meeting is on Friday.
added demos/wasm-hooks/playground/old-drafts/draft1.txt +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+draft 1
new file mode 100644
@@ -0,0 +1 @@
1+draft 1
added demos/wasm-hooks/playground/todo.txt +2 -0
new file mode 100644
@@ -0,0 +1,2 @@
1+Buy milk
2+Call the plumber
new file mode 100644
@@ -0,0 +1,2 @@
1+Buy milk
2+Call the plumber
added demos/wasm-tools/README.en.md +77 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/README.fr.md +77 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/agent.yaml +63 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/build.sh +10 -0
new file mode 100755
@@ -0,0 +1,10 @@
1+#!/bin/sh
2+# Builds the demo's WASM tools with TinyGo: tools-src/<name>/ → tools/<name>.wasm.
3+# cd demos/wasm-tools && ./build.sh
4+set -eu
5+cd "$(dirname "$0")/tools-src"
6+for dir in */; do
7+ name=${dir%/}
8+ tinygo build -o "../tools/$name.wasm" -target=wasip1 -no-debug "./$name"
9+ echo "built tools/$name.wasm ($(wc -c < "../tools/$name.wasm") bytes)"
10+done
new file mode 100755
@@ -0,0 +1,10 @@
1+#!/bin/sh
2+# Builds the demo's WASM tools with TinyGo: tools-src/<name>/ → tools/<name>.wasm.
3+# cd demos/wasm-tools && ./build.sh
4+set -eu
5+cd "$(dirname "$0")/tools-src"
6+for dir in */; do
7+ name=${dir%/}
8+ tinygo build -o "../tools/$name.wasm" -target=wasip1 -no-debug "./$name"
9+ echo "built tools/$name.wasm ($(wc -c < "../tools/$name.wasm") bytes)"
10+done
added demos/wasm-tools/notes/release-plan.md +5 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/notes/tinygo.md +6 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/notes/wazero.md +6 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/out/.gitignore +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+# Written by save_note: everything here is generated.
2+*
3+!.gitignore
new file mode 100644
@@ -0,0 +1,3 @@
1+# Written by save_note: everything here is generated.
2+*
3+!.gitignore
added demos/wasm-tools/tools-src/go.mod +6 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools-src/read_note/main.go +35 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools-src/save_note/main.go +54 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools-src/search_notes/main.go +59 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools-src/word_stats/main.go +58 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools/read_note.wasm +0 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools/save_note.wasm +0 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools/search_notes.wasm +0 -0
Large diff collapsed to keep the page fast — load it when you need it.
added demos/wasm-tools/tools/word_stats.wasm +0 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/diagrams/packages.drawio +1 -1
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/README.md +2 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/explanation/declared-tools.md +13 -1
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/explanation/hooks.md +10 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/how-to/declare-a-custom-tool.md +1 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/how-to/guard-the-agent-with-hooks.md +1 -0
Large diff collapsed to keep the page fast — load it when you need it.
added docs/en/how-to/write-a-wasm-hook.md +77 -0
Large diff collapsed to keep the page fast — load it when you need it.
added docs/en/how-to/write-a-wasm-tool.md +89 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/reference/configuration.md +20 -4
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/reference/hooks.md +34 -4
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/en/reference/tools.md +21 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/README.md +2 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/explanation/declared-tools.md +13 -1
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/explanation/hooks.md +10 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/how-to/declare-a-custom-tool.md +1 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/how-to/guard-the-agent-with-hooks.md +1 -0
Large diff collapsed to keep the page fast — load it when you need it.
added docs/fr/how-to/write-a-wasm-hook.md +77 -0
Large diff collapsed to keep the page fast — load it when you need it.
added docs/fr/how-to/write-a-wasm-tool.md +89 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/reference/configuration.md +20 -4
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/reference/hooks.md +34 -4
Large diff collapsed to keep the page fast — load it when you need it.
modified docs/fr/reference/tools.md +21 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified go.mod +1 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified go.sum +2 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/config/config.go +1 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/config/hooks.go +52 -2
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/config/hooks_test.go +44 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/config/tools.go +115 -4
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/config/tools_test.go +56 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/hooks/exec.go +46 -2
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/hooks/hooks.go +2 -2
Large diff collapsed to keep the page fast — load it when you need it.
added internal/hooks/wasm_test.go +92 -0
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/tools/declared.go +95 -17
Large diff collapsed to keep the page fast — load it when you need it.
modified internal/tools/declared_test.go +75 -0
Large diff collapsed to keep the page fast — load it when you need it.
added internal/wasmtool/testdata/probe.wasm +0 -0
Large diff collapsed to keep the page fast — load it when you need it.
added internal/wasmtool/testdata/probe/main.go +89 -0
Large diff collapsed to keep the page fast — load it when you need it.
added internal/wasmtool/wasmtool.go +276 -0
Large diff collapsed to keep the page fast — load it when you need it.
added internal/wasmtool/wasmtool_test.go +206 -0
Large diff collapsed to keep the page fast — load it when you need it.