modified
.tickets/epics.yaml +6 -0 | @@ -12,3 +12,9 @@ epics: | ||
| 12 | 12 | - title: Ideas |
| 13 | 13 | state: open |
| 14 | 14 | color: "#bfdadc" |
| 15 | + - title: Agent Memory | |
| 16 | + state: open | |
| 17 | + color: "#f9d0c4" | |
| 18 | + - title: Providers (LLM Engine) | |
| 19 | + state: open | |
| 20 | + color: "#0e8a16" | |
| @@ -12,3 +12,9 @@ epics: | |||
| 12 | - title: Ideas | 12 | - title: Ideas |
| 13 | state: open | 13 | state: open |
| 14 | color: "#bfdadc" | 14 | color: "#bfdadc" |
| 15 | + - title: Agent Memory | ||
| 16 | + state: open | ||
| 17 | + color: "#f9d0c4" | ||
| 18 | + - title: Providers (LLM Engine) | ||
| 19 | + state: open | ||
| 20 | + color: "#0e8a16" | ||
modified
.tickets/issues/0013-activate-deactivate-yolo-mode.yaml +15 -3 | @@ -1,13 +1,25 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 13 |
| 3 | 3 | title: Activate / Deactivate YOLO mode |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-16T05:47:28.194Z |
| 9 | -updatedAt: 2026-10-06T02:52:48.473Z | |
| 9 | +updatedAt: 2026-10-06T03:53:37.224Z | |
| 10 | 10 | labels: [] |
| 11 | -body: "" | |
| 11 | +related: | |
| 12 | + - 59 | |
| 13 | +body: | | |
| 14 | + User's request (2026-10-06, FR): enable/disable yolo mode for tool | |
| 15 | + execution through the config file. Default: yolo. With yolo off, every | |
| 16 | + tool call asks: once, never, or for the whole session. Examples: | |
| 17 | + `yolo: false` + `bashTool: true` (asks), `yolo: true` + `bashTool: true`, | |
| 18 | + `yolo: true` + `bashTool: false` (bash disabled, other tools not | |
| 19 | + necessarily). With `mmConfigs`, the main agent (the one holding | |
| 20 | + mmConfigs) wins. | |
| 21 | + | |
| 22 | + Implemented on branch `feature/yolo` (2026-10-06) — details in ticket | |
| 23 | + 59's comment 2. | |
| 12 | 24 | tasks: [] |
| 13 | 25 | comments: [] |
| @@ -1,13 +1,25 @@ | |||
| 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: 13 | 2 | id: 13 |
| 3 | title: Activate / Deactivate YOLO mode | 3 | title: Activate / Deactivate YOLO mode |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-16T05:47:28.194Z | 8 | createdAt: 2026-09-16T05:47:28.194Z |
| 9 | -updatedAt: 2026-10-06T02:52:48.473Z | 9 | +updatedAt: 2026-10-06T03:53:37.224Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -body: "" | 11 | +related: |
| 12 | + - 59 | ||
| 13 | +body: | | ||
| 14 | + User's request (2026-10-06, FR): enable/disable yolo mode for tool | ||
| 15 | + execution through the config file. Default: yolo. With yolo off, every | ||
| 16 | + tool call asks: once, never, or for the whole session. Examples: | ||
| 17 | + `yolo: false` + `bashTool: true` (asks), `yolo: true` + `bashTool: true`, | ||
| 18 | + `yolo: true` + `bashTool: false` (bash disabled, other tools not | ||
| 19 | + necessarily). With `mmConfigs`, the main agent (the one holding | ||
| 20 | + mmConfigs) wins. | ||
| 21 | + | ||
| 22 | + Implemented on branch `feature/yolo` (2026-10-06) — details in ticket | ||
| 23 | + 59's comment 2. | ||
| 12 | tasks: [] | 24 | tasks: [] |
| 13 | comments: [] | 25 | comments: [] |
modified
.tickets/issues/0030-how-to-use-mini-me-with-claude.yaml +2 -1 | @@ -6,8 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:00:56.971Z |
| 9 | -updatedAt: 2026-09-22T20:57:22.310Z | |
| 9 | +updatedAt: 2026-10-06T04:00:56.602Z | |
| 10 | 10 | labels: [] |
| 11 | +epic: Providers (LLM Engine) | |
| 11 | 12 | body: | |
| 12 | 13 | ## Analysis (Claude, 2026-09-21 — not implemented) |
| 13 | 14 | |
| @@ -6,8 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:00:56.971Z | 8 | createdAt: 2026-09-21T04:00:56.971Z |
| 9 | -updatedAt: 2026-09-22T20:57:22.310Z | 9 | +updatedAt: 2026-10-06T04:00:56.602Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | +epic: Providers (LLM Engine) | ||
| 11 | body: | | 12 | body: | |
| 12 | ## Analysis (Claude, 2026-09-21 — not implemented) | 13 | ## Analysis (Claude, 2026-09-21 — not implemented) |
| 13 | 14 | ||
modified
.tickets/issues/0031-how-to-use-mini-me-with-gemini.yaml +2 -1 | @@ -6,8 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:01:15.166Z |
| 9 | -updatedAt: 2026-09-22T20:56:55.345Z | |
| 9 | +updatedAt: 2026-10-06T04:01:18.848Z | |
| 10 | 10 | labels: [] |
| 11 | +epic: Providers (LLM Engine) | |
| 11 | 12 | body: | |
| 12 | 13 | ## Analysis (Claude, 2026-09-21 — not implemented) |
| 13 | 14 | |
| @@ -6,8 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:01:15.166Z | 8 | createdAt: 2026-09-21T04:01:15.166Z |
| 9 | -updatedAt: 2026-09-22T20:56:55.345Z | 9 | +updatedAt: 2026-10-06T04:01:18.848Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | +epic: Providers (LLM Engine) | ||
| 11 | body: | | 12 | body: | |
| 12 | ## Analysis (Claude, 2026-09-21 — not implemented) | 13 | ## Analysis (Claude, 2026-09-21 — not implemented) |
| 13 | 14 | ||
modified
.tickets/issues/0033-invoke-another-mini-me.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 33 |
| 3 | 3 | title: Invoke (another) Mini Me |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:48:03.596Z |
| 9 | -updatedAt: 2026-09-29T06:25:43.079Z | |
| 9 | +updatedAt: 2026-10-06T03:55:11.169Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.3.0 |
| 12 | 12 | epic: Builtin tools |
| @@ -1,12 +1,12 @@ | |||
| 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: 33 | 2 | id: 33 |
| 3 | title: Invoke (another) Mini Me | 3 | title: Invoke (another) Mini Me |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:48:03.596Z | 8 | createdAt: 2026-09-21T04:48:03.596Z |
| 9 | -updatedAt: 2026-09-29T06:25:43.079Z | 9 | +updatedAt: 2026-10-06T03:55:11.169Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.3.0 | 11 | milestone: v0.3.0 |
| 12 | epic: Builtin tools | 12 | epic: Builtin tools |
modified
.tickets/issues/0034-anatolian-mini-me-s-orchestrator.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 34 |
| 3 | 3 | title: Anatolian -> Mini Me(s) Orchestrator? |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:49:57.129Z |
| 9 | -updatedAt: 2026-09-21T04:49:57.129Z | |
| 9 | +updatedAt: 2026-10-06T03:57:06.058Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | epic: Ideas |
| 12 | 12 | body: "" |
| @@ -1,12 +1,12 @@ | |||
| 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: 34 | 2 | id: 34 |
| 3 | title: Anatolian -> Mini Me(s) Orchestrator? | 3 | title: Anatolian -> Mini Me(s) Orchestrator? |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:49:57.129Z | 8 | createdAt: 2026-09-21T04:49:57.129Z |
| 9 | -updatedAt: 2026-09-21T04:49:57.129Z | 9 | +updatedAt: 2026-10-06T03:57:06.058Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | epic: Ideas | 11 | epic: Ideas |
| 12 | body: "" | 12 | body: "" |
modified
.tickets/issues/0040-better-list-of-detected-tools-at-the-start-of-mi.yaml +2 -1 | @@ -6,8 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-23T10:28:36.392Z |
| 9 | -updatedAt: 2026-09-23T10:28:36.392Z | |
| 9 | +updatedAt: 2026-10-06T04:01:48.816Z | |
| 10 | 10 | labels: [] |
| 11 | +epic: Improve TUI | |
| 11 | 12 | body: "" |
| 12 | 13 | tasks: [] |
| 13 | 14 | comments: [] |
| @@ -6,8 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-23T10:28:36.392Z | 8 | createdAt: 2026-09-23T10:28:36.392Z |
| 9 | -updatedAt: 2026-09-23T10:28:36.392Z | 9 | +updatedAt: 2026-10-06T04:01:48.816Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | +epic: Improve TUI | ||
| 11 | body: "" | 12 | body: "" |
| 12 | tasks: [] | 13 | tasks: [] |
| 13 | comments: [] | 14 | comments: [] |
modified
.tickets/issues/0048-add-a-builtin-tool-to-search-into-the-mm-session.yaml +2 -1 | @@ -6,9 +6,10 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-27T03:45:48.121Z |
| 9 | -updatedAt: 2026-09-29T06:25:13.129Z | |
| 9 | +updatedAt: 2026-10-06T03:59:41.259Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.3.0 |
| 12 | +epic: Agent Memory | |
| 12 | 13 | body: "" |
| 13 | 14 | tasks: [] |
| 14 | 15 | comments: [] |
| @@ -6,9 +6,10 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-27T03:45:48.121Z | 8 | createdAt: 2026-09-27T03:45:48.121Z |
| 9 | -updatedAt: 2026-09-29T06:25:13.129Z | 9 | +updatedAt: 2026-10-06T03:59:41.259Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.3.0 | 11 | milestone: v0.3.0 |
| 12 | +epic: Agent Memory | ||
| 12 | body: "" | 13 | body: "" |
| 13 | tasks: [] | 14 | tasks: [] |
| 14 | comments: [] | 15 | comments: [] |
modified
.tickets/issues/0050-create-a-web-site.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 50 |
| 3 | 3 | title: Create a web site |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-28T18:44:59.825Z |
| 9 | -updatedAt: 2026-09-29T06:27:12.181Z | |
| 9 | +updatedAt: 2026-10-06T03:58:01.122Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.3.0 |
| 12 | 12 | body: "" |
| @@ -1,12 +1,12 @@ | |||
| 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: 50 | 2 | id: 50 |
| 3 | title: Create a web site | 3 | title: Create a web site |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-28T18:44:59.825Z | 8 | createdAt: 2026-09-28T18:44:59.825Z |
| 9 | -updatedAt: 2026-09-29T06:27:12.181Z | 9 | +updatedAt: 2026-10-06T03:58:01.122Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.3.0 | 11 | milestone: v0.3.0 |
| 12 | body: "" | 12 | body: "" |
modified
.tickets/issues/0052-buitin-acp-tool-to-query-other-agent-like-mini-m.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 52 |
| 3 | 3 | title: Buitin ACP tool to query other agent (like mini me, but not only) |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-29T05:18:13.897Z |
| 9 | -updatedAt: 2026-10-02T21:15:00.000Z | |
| 9 | +updatedAt: 2026-10-06T03:55:00.084Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::high |
| 12 | 12 | - kind::feature |
| @@ -1,12 +1,12 @@ | |||
| 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: 52 | 2 | id: 52 |
| 3 | title: Buitin ACP tool to query other agent (like mini me, but not only) | 3 | title: Buitin ACP tool to query other agent (like mini me, but not only) |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-29T05:18:13.897Z | 8 | createdAt: 2026-09-29T05:18:13.897Z |
| 9 | -updatedAt: 2026-10-02T21:15:00.000Z | 9 | +updatedAt: 2026-10-06T03:55:00.084Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::high | 11 | - priority::high |
| 12 | - kind::feature | 12 | - kind::feature |
modified
.tickets/issues/0059-how-to-start-in-yolo-mode.yaml +63 -29 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 59 |
| 3 | 3 | title: How to start in yolo mode? |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-01T04:39:53.878Z |
| 9 | -updatedAt: 2026-10-03T10:00:00.000Z | |
| 9 | +updatedAt: 2026-10-06T03:53:27.047Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | - priority::low |
| @@ -50,104 +50,104 @@ body: | | ||
| 50 | 50 | - Make it visible: banner/status line "yolo" so nobody runs it by mistake. |
| 51 | 51 | tasks: |
| 52 | 52 | - id: 1 |
| 53 | - title: "Decide the open questions below (flag name, ACP scope, MCP scope, banner)" | |
| 54 | - state: open | |
| 53 | + title: Decide the open questions below (flag name, ACP scope, MCP scope, banner) | |
| 54 | + state: closed | |
| 55 | 55 | priority: none |
| 56 | 56 | author: |
| 57 | 57 | name: k33g |
| 58 | 58 | email: ph.charriere@gmail.com |
| 59 | 59 | createdAt: 2026-10-01T06:30:00.000Z |
| 60 | - updatedAt: 2026-10-01T06:30:00.000Z | |
| 60 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 61 | 61 | - id: 2 |
| 62 | 62 | title: "Failing tests first: with yolo on, window mode (permissions: ask) and ACP Allow() return true without asking; MCP ask-tools run" |
| 63 | - state: open | |
| 63 | + state: closed | |
| 64 | 64 | priority: none |
| 65 | 65 | author: |
| 66 | 66 | name: k33g |
| 67 | 67 | email: ph.charriere@gmail.com |
| 68 | 68 | createdAt: 2026-10-01T06:30:00.000Z |
| 69 | - updatedAt: 2026-10-01T06:30:00.000Z | |
| 69 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 70 | 70 | - id: 3 |
| 71 | - title: "Flag --yolo (main.go, alongside -p/-tui/-acp) + optional env/config override; forces Permissions=auto and mcp.permissions=auto, per-server MCP permissions included" | |
| 72 | - state: open | |
| 71 | + title: Flag --yolo (main.go, alongside -p/-tui/-acp) + optional env/config override; forces Permissions=auto and mcp.permissions=auto, per-server MCP permissions included | |
| 72 | + state: closed | |
| 73 | 73 | priority: none |
| 74 | 74 | author: |
| 75 | 75 | name: k33g |
| 76 | 76 | email: ph.charriere@gmail.com |
| 77 | 77 | createdAt: 2026-10-01T06:30:00.000Z |
| 78 | - updatedAt: 2026-10-01T06:30:00.000Z | |
| 78 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 79 | 79 | - id: 4 |
| 80 | 80 | title: "ACP sink Allow(): short-circuit when yolo is on (no RequestPermission sent to the editor)" |
| 81 | - state: open | |
| 81 | + state: closed | |
| 82 | 82 | priority: none |
| 83 | 83 | author: |
| 84 | 84 | name: k33g |
| 85 | 85 | email: ph.charriere@gmail.com |
| 86 | 86 | createdAt: 2026-10-01T06:30:00.000Z |
| 87 | - updatedAt: 2026-10-01T06:30:00.000Z | |
| 87 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 88 | 88 | - id: 5 |
| 89 | - title: "Banner/status line shows that yolo is on (terminal, window, ACP agent info)" | |
| 90 | - state: open | |
| 89 | + title: Banner/status line shows that yolo is on (terminal, window, ACP agent info) | |
| 90 | + state: closed | |
| 91 | 91 | priority: none |
| 92 | 92 | author: |
| 93 | 93 | name: k33g |
| 94 | 94 | email: ph.charriere@gmail.com |
| 95 | 95 | createdAt: 2026-10-01T06:30:00.000Z |
| 96 | - updatedAt: 2026-10-01T06:30:00.000Z | |
| 96 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 97 | 97 | - id: 6 |
| 98 | 98 | title: "Docs EN+FR: reference/cli.md, reference/configuration.md, how-to (window mode, editor); answer the question in the how-to" |
| 99 | - state: open | |
| 99 | + state: closed | |
| 100 | 100 | priority: none |
| 101 | 101 | author: |
| 102 | 102 | name: k33g |
| 103 | 103 | email: ph.charriere@gmail.com |
| 104 | 104 | createdAt: 2026-10-01T06:30:00.000Z |
| 105 | - updatedAt: 2026-10-01T06:30:00.000Z | |
| 105 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 106 | 106 | - id: 7 |
| 107 | 107 | title: "Per-tool permission: bashTool/editTools accept true | false | ask (config validation, defaults unchanged)" |
| 108 | - state: open | |
| 108 | + state: closed | |
| 109 | 109 | priority: none |
| 110 | 110 | author: |
| 111 | 111 | name: k33g |
| 112 | 112 | email: ph.charriere@gmail.com |
| 113 | 113 | createdAt: 2026-10-03T10:00:00.000Z |
| 114 | - updatedAt: 2026-10-03T10:00:00.000Z | |
| 114 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 115 | 115 | - id: 8 |
| 116 | 116 | title: "Line mode: a terminal permission prompt (y once / s session / n reject) so `ask` works there too" |
| 117 | - state: open | |
| 117 | + state: closed | |
| 118 | 118 | priority: none |
| 119 | 119 | author: |
| 120 | 120 | name: k33g |
| 121 | 121 | email: ph.charriere@gmail.com |
| 122 | 122 | createdAt: 2026-10-03T10:00:00.000Z |
| 123 | - updatedAt: 2026-10-03T10:00:00.000Z | |
| 123 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 124 | 124 | - id: 9 |
| 125 | - title: "Decide precedence between the per-tool value, the global `permissions` key and --yolo" | |
| 126 | - state: open | |
| 125 | + title: Decide precedence between the per-tool value, the global `permissions` key and --yolo | |
| 126 | + state: closed | |
| 127 | 127 | priority: none |
| 128 | 128 | author: |
| 129 | 129 | name: k33g |
| 130 | 130 | email: ph.charriere@gmail.com |
| 131 | 131 | createdAt: 2026-10-03T10:00:00.000Z |
| 132 | - updatedAt: 2026-10-03T10:00:00.000Z | |
| 132 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 133 | 133 | - id: 10 |
| 134 | - title: "Ship `permissions:` (commented, explained) in agent.*.yaml, demo*/ and config.samples/" | |
| 135 | - state: open | |
| 134 | + title: Ship `permissions:` (commented, explained) in agent.*.yaml, demo*/ and config.samples/ | |
| 135 | + state: closed | |
| 136 | 136 | priority: none |
| 137 | 137 | author: |
| 138 | 138 | name: k33g |
| 139 | 139 | email: ph.charriere@gmail.com |
| 140 | 140 | createdAt: 2026-10-03T10:00:00.000Z |
| 141 | - updatedAt: 2026-10-03T10:00:00.000Z | |
| 141 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 142 | 142 | - id: 11 |
| 143 | 143 | title: "mm --ask: let the caller forbid or confirm the asked agent's tools (ticket 73 open task)" |
| 144 | - state: open | |
| 144 | + state: closed | |
| 145 | 145 | priority: none |
| 146 | 146 | author: |
| 147 | 147 | name: k33g |
| 148 | 148 | email: ph.charriere@gmail.com |
| 149 | 149 | createdAt: 2026-10-03T10:00:00.000Z |
| 150 | - updatedAt: 2026-10-03T10:00:00.000Z | |
| 150 | + updatedAt: 2026-10-06T12:00:00.000Z | |
| 151 | 151 | comments: |
| 152 | 152 | - id: 1 |
| 153 | 153 | author: |
| @@ -195,3 +195,37 @@ comments: | ||
| 195 | 195 | 1. Precedence: per-tool value > `permissions` > default — OK? |
| 196 | 196 | 2. In -p mode, should `ask` reject (safe, proposed) or allow? |
| 197 | 197 | 3. Should `ask` for editTools cover read_file too (today never gated)? |
| 198 | + - id: 2 | |
| 199 | + author: | |
| 200 | + name: claude | |
| 201 | + email: noreply@anthropic.com | |
| 202 | + createdAt: 2026-10-06T12:00:00.000Z | |
| 203 | + body: | | |
| 204 | + **Implemented on branch `feature/yolo` (2026-10-06, with ticket 13), uncommitted.** | |
| 205 | + | |
| 206 | + User's decisions: a config key, not a flag; yolo everywhere ACP | |
| 207 | + included; `yolo` replaces `permissions` (still read, deprecated); | |
| 208 | + `yolo: false` forces MCP to ask; four answers (once / session / | |
| 209 | + reject / never); the agent that holds mmConfigs wins. | |
| 210 | + | |
| 211 | + - `yolo: true|false` (default true), override `AGENT_YOLO`. | |
| 212 | + `bashTool`/`editTools` unchanged: they say whether a tool exists. | |
| 213 | + - `permissions: ask` = `yolo: false` (+ banner warning), `auto` = | |
| 214 | + warning only. Answers question 9: there is no per-tool value. | |
| 215 | + - New package `internal/consent`: `Gate` (yolo check), `Answer`, | |
| 216 | + `Policy` (session memory per tool), `SetPrompter`/`Terminal`/ | |
| 217 | + `Prompt` for the line mode. All six built-in call sites use it. | |
| 218 | + - Line mode: `🔐` prompt routed by the REPL waiting room | |
| 219 | + (`internal/agent/prompt.go`), also for /ask. `-p` and `--ask` | |
| 220 | + reject with a stderr notice (open question 2: reject). | |
| 221 | + - Window: a/s/r/n. ACP: 4th option `never` (reject_always); with | |
| 222 | + yolo on no RequestPermission is sent (ticket 57's pending-dialog | |
| 223 | + case cannot happen in yolo mode). | |
| 224 | + - MCP: asks when `permissions: ask` OR yolo off; the terminal path | |
| 225 | + now asks too. | |
| 226 | + - /ask children run with `AGENT_YOLO=false`; the asker's | |
| 227 | + `client.RequestPermission` goes through ITS `consent.Gate`. | |
| 228 | + - Banner: `| yolo: on` / `| yolo: off (tools ask)`. | |
| 229 | + - Open question 3 (read_file gated?): no, reads never ask. | |
| 230 | + Docs EN+FR: how-to/confirm-tool-calls.md, explanation/yolo-and-permissions.md, | |
| 231 | + reference configuration/cli/acp/tools/slash-commands updated. | |
| @@ -1,12 +1,12 @@ | |||
| 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: 59 | 2 | id: 59 |
| 3 | title: How to start in yolo mode? | 3 | title: How to start in yolo mode? |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-01T04:39:53.878Z | 8 | createdAt: 2026-10-01T04:39:53.878Z |
| 9 | -updatedAt: 2026-10-03T10:00:00.000Z | 9 | +updatedAt: 2026-10-06T03:53:27.047Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | - priority::low | 12 | - priority::low |
| @@ -50,104 +50,104 @@ body: | | |||
| 50 | - Make it visible: banner/status line "yolo" so nobody runs it by mistake. | 50 | - Make it visible: banner/status line "yolo" so nobody runs it by mistake. |
| 51 | tasks: | 51 | tasks: |
| 52 | - id: 1 | 52 | - id: 1 |
| 53 | - title: "Decide the open questions below (flag name, ACP scope, MCP scope, banner)" | 53 | + title: Decide the open questions below (flag name, ACP scope, MCP scope, banner) |
| 54 | - state: open | 54 | + state: closed |
| 55 | priority: none | 55 | priority: none |
| 56 | author: | 56 | author: |
| 57 | name: k33g | 57 | name: k33g |
| 58 | email: ph.charriere@gmail.com | 58 | email: ph.charriere@gmail.com |
| 59 | createdAt: 2026-10-01T06:30:00.000Z | 59 | createdAt: 2026-10-01T06:30:00.000Z |
| 60 | - updatedAt: 2026-10-01T06:30:00.000Z | 60 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 61 | - id: 2 | 61 | - id: 2 |
| 62 | title: "Failing tests first: with yolo on, window mode (permissions: ask) and ACP Allow() return true without asking; MCP ask-tools run" | 62 | title: "Failing tests first: with yolo on, window mode (permissions: ask) and ACP Allow() return true without asking; MCP ask-tools run" |
| 63 | - state: open | 63 | + state: closed |
| 64 | priority: none | 64 | priority: none |
| 65 | author: | 65 | author: |
| 66 | name: k33g | 66 | name: k33g |
| 67 | email: ph.charriere@gmail.com | 67 | email: ph.charriere@gmail.com |
| 68 | createdAt: 2026-10-01T06:30:00.000Z | 68 | createdAt: 2026-10-01T06:30:00.000Z |
| 69 | - updatedAt: 2026-10-01T06:30:00.000Z | 69 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 70 | - id: 3 | 70 | - id: 3 |
| 71 | - title: "Flag --yolo (main.go, alongside -p/-tui/-acp) + optional env/config override; forces Permissions=auto and mcp.permissions=auto, per-server MCP permissions included" | 71 | + title: Flag --yolo (main.go, alongside -p/-tui/-acp) + optional env/config override; forces Permissions=auto and mcp.permissions=auto, per-server MCP permissions included |
| 72 | - state: open | 72 | + state: closed |
| 73 | priority: none | 73 | priority: none |
| 74 | author: | 74 | author: |
| 75 | name: k33g | 75 | name: k33g |
| 76 | email: ph.charriere@gmail.com | 76 | email: ph.charriere@gmail.com |
| 77 | createdAt: 2026-10-01T06:30:00.000Z | 77 | createdAt: 2026-10-01T06:30:00.000Z |
| 78 | - updatedAt: 2026-10-01T06:30:00.000Z | 78 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 79 | - id: 4 | 79 | - id: 4 |
| 80 | title: "ACP sink Allow(): short-circuit when yolo is on (no RequestPermission sent to the editor)" | 80 | title: "ACP sink Allow(): short-circuit when yolo is on (no RequestPermission sent to the editor)" |
| 81 | - state: open | 81 | + state: closed |
| 82 | priority: none | 82 | priority: none |
| 83 | author: | 83 | author: |
| 84 | name: k33g | 84 | name: k33g |
| 85 | email: ph.charriere@gmail.com | 85 | email: ph.charriere@gmail.com |
| 86 | createdAt: 2026-10-01T06:30:00.000Z | 86 | createdAt: 2026-10-01T06:30:00.000Z |
| 87 | - updatedAt: 2026-10-01T06:30:00.000Z | 87 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 88 | - id: 5 | 88 | - id: 5 |
| 89 | - title: "Banner/status line shows that yolo is on (terminal, window, ACP agent info)" | 89 | + title: Banner/status line shows that yolo is on (terminal, window, ACP agent info) |
| 90 | - state: open | 90 | + state: closed |
| 91 | priority: none | 91 | priority: none |
| 92 | author: | 92 | author: |
| 93 | name: k33g | 93 | name: k33g |
| 94 | email: ph.charriere@gmail.com | 94 | email: ph.charriere@gmail.com |
| 95 | createdAt: 2026-10-01T06:30:00.000Z | 95 | createdAt: 2026-10-01T06:30:00.000Z |
| 96 | - updatedAt: 2026-10-01T06:30:00.000Z | 96 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 97 | - id: 6 | 97 | - id: 6 |
| 98 | title: "Docs EN+FR: reference/cli.md, reference/configuration.md, how-to (window mode, editor); answer the question in the how-to" | 98 | title: "Docs EN+FR: reference/cli.md, reference/configuration.md, how-to (window mode, editor); answer the question in the how-to" |
| 99 | - state: open | 99 | + state: closed |
| 100 | priority: none | 100 | priority: none |
| 101 | author: | 101 | author: |
| 102 | name: k33g | 102 | name: k33g |
| 103 | email: ph.charriere@gmail.com | 103 | email: ph.charriere@gmail.com |
| 104 | createdAt: 2026-10-01T06:30:00.000Z | 104 | createdAt: 2026-10-01T06:30:00.000Z |
| 105 | - updatedAt: 2026-10-01T06:30:00.000Z | 105 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 106 | - id: 7 | 106 | - id: 7 |
| 107 | title: "Per-tool permission: bashTool/editTools accept true | false | ask (config validation, defaults unchanged)" | 107 | title: "Per-tool permission: bashTool/editTools accept true | false | ask (config validation, defaults unchanged)" |
| 108 | - state: open | 108 | + state: closed |
| 109 | priority: none | 109 | priority: none |
| 110 | author: | 110 | author: |
| 111 | name: k33g | 111 | name: k33g |
| 112 | email: ph.charriere@gmail.com | 112 | email: ph.charriere@gmail.com |
| 113 | createdAt: 2026-10-03T10:00:00.000Z | 113 | createdAt: 2026-10-03T10:00:00.000Z |
| 114 | - updatedAt: 2026-10-03T10:00:00.000Z | 114 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 115 | - id: 8 | 115 | - id: 8 |
| 116 | title: "Line mode: a terminal permission prompt (y once / s session / n reject) so `ask` works there too" | 116 | title: "Line mode: a terminal permission prompt (y once / s session / n reject) so `ask` works there too" |
| 117 | - state: open | 117 | + state: closed |
| 118 | priority: none | 118 | priority: none |
| 119 | author: | 119 | author: |
| 120 | name: k33g | 120 | name: k33g |
| 121 | email: ph.charriere@gmail.com | 121 | email: ph.charriere@gmail.com |
| 122 | createdAt: 2026-10-03T10:00:00.000Z | 122 | createdAt: 2026-10-03T10:00:00.000Z |
| 123 | - updatedAt: 2026-10-03T10:00:00.000Z | 123 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 124 | - id: 9 | 124 | - id: 9 |
| 125 | - title: "Decide precedence between the per-tool value, the global `permissions` key and --yolo" | 125 | + title: Decide precedence between the per-tool value, the global `permissions` key and --yolo |
| 126 | - state: open | 126 | + state: closed |
| 127 | priority: none | 127 | priority: none |
| 128 | author: | 128 | author: |
| 129 | name: k33g | 129 | name: k33g |
| 130 | email: ph.charriere@gmail.com | 130 | email: ph.charriere@gmail.com |
| 131 | createdAt: 2026-10-03T10:00:00.000Z | 131 | createdAt: 2026-10-03T10:00:00.000Z |
| 132 | - updatedAt: 2026-10-03T10:00:00.000Z | 132 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 133 | - id: 10 | 133 | - id: 10 |
| 134 | - title: "Ship `permissions:` (commented, explained) in agent.*.yaml, demo*/ and config.samples/" | 134 | + title: Ship `permissions:` (commented, explained) in agent.*.yaml, demo*/ and config.samples/ |
| 135 | - state: open | 135 | + state: closed |
| 136 | priority: none | 136 | priority: none |
| 137 | author: | 137 | author: |
| 138 | name: k33g | 138 | name: k33g |
| 139 | email: ph.charriere@gmail.com | 139 | email: ph.charriere@gmail.com |
| 140 | createdAt: 2026-10-03T10:00:00.000Z | 140 | createdAt: 2026-10-03T10:00:00.000Z |
| 141 | - updatedAt: 2026-10-03T10:00:00.000Z | 141 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 142 | - id: 11 | 142 | - id: 11 |
| 143 | title: "mm --ask: let the caller forbid or confirm the asked agent's tools (ticket 73 open task)" | 143 | title: "mm --ask: let the caller forbid or confirm the asked agent's tools (ticket 73 open task)" |
| 144 | - state: open | 144 | + state: closed |
| 145 | priority: none | 145 | priority: none |
| 146 | author: | 146 | author: |
| 147 | name: k33g | 147 | name: k33g |
| 148 | email: ph.charriere@gmail.com | 148 | email: ph.charriere@gmail.com |
| 149 | createdAt: 2026-10-03T10:00:00.000Z | 149 | createdAt: 2026-10-03T10:00:00.000Z |
| 150 | - updatedAt: 2026-10-03T10:00:00.000Z | 150 | + updatedAt: 2026-10-06T12:00:00.000Z |
| 151 | comments: | 151 | comments: |
| 152 | - id: 1 | 152 | - id: 1 |
| 153 | author: | 153 | author: |
| @@ -195,3 +195,37 @@ comments: | |||
| 195 | 1. Precedence: per-tool value > `permissions` > default — OK? | 195 | 1. Precedence: per-tool value > `permissions` > default — OK? |
| 196 | 2. In -p mode, should `ask` reject (safe, proposed) or allow? | 196 | 2. In -p mode, should `ask` reject (safe, proposed) or allow? |
| 197 | 3. Should `ask` for editTools cover read_file too (today never gated)? | 197 | 3. Should `ask` for editTools cover read_file too (today never gated)? |
| 198 | + - id: 2 | ||
| 199 | + author: | ||
| 200 | + name: claude | ||
| 201 | + email: noreply@anthropic.com | ||
| 202 | + createdAt: 2026-10-06T12:00:00.000Z | ||
| 203 | + body: | | ||
| 204 | + **Implemented on branch `feature/yolo` (2026-10-06, with ticket 13), uncommitted.** | ||
| 205 | + | ||
| 206 | + User's decisions: a config key, not a flag; yolo everywhere ACP | ||
| 207 | + included; `yolo` replaces `permissions` (still read, deprecated); | ||
| 208 | + `yolo: false` forces MCP to ask; four answers (once / session / | ||
| 209 | + reject / never); the agent that holds mmConfigs wins. | ||
| 210 | + | ||
| 211 | + - `yolo: true|false` (default true), override `AGENT_YOLO`. | ||
| 212 | + `bashTool`/`editTools` unchanged: they say whether a tool exists. | ||
| 213 | + - `permissions: ask` = `yolo: false` (+ banner warning), `auto` = | ||
| 214 | + warning only. Answers question 9: there is no per-tool value. | ||
| 215 | + - New package `internal/consent`: `Gate` (yolo check), `Answer`, | ||
| 216 | + `Policy` (session memory per tool), `SetPrompter`/`Terminal`/ | ||
| 217 | + `Prompt` for the line mode. All six built-in call sites use it. | ||
| 218 | + - Line mode: `🔐` prompt routed by the REPL waiting room | ||
| 219 | + (`internal/agent/prompt.go`), also for /ask. `-p` and `--ask` | ||
| 220 | + reject with a stderr notice (open question 2: reject). | ||
| 221 | + - Window: a/s/r/n. ACP: 4th option `never` (reject_always); with | ||
| 222 | + yolo on no RequestPermission is sent (ticket 57's pending-dialog | ||
| 223 | + case cannot happen in yolo mode). | ||
| 224 | + - MCP: asks when `permissions: ask` OR yolo off; the terminal path | ||
| 225 | + now asks too. | ||
| 226 | + - /ask children run with `AGENT_YOLO=false`; the asker's | ||
| 227 | + `client.RequestPermission` goes through ITS `consent.Gate`. | ||
| 228 | + - Banner: `| yolo: on` / `| yolo: off (tools ask)`. | ||
| 229 | + - Open question 3 (read_file gated?): no, reads never ask. | ||
| 230 | + Docs EN+FR: how-to/confirm-tool-calls.md, explanation/yolo-and-permissions.md, | ||
| 231 | + reference configuration/cli/acp/tools/slash-commands updated. | ||
modified
.tickets/issues/0060-add-llmman-provider.yaml +8 -8 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 60 |
| 3 | 3 | title: Add llmman provider |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-01T04:48:02.829Z |
| 9 | -updatedAt: 2026-10-01T07:00:00.000Z | |
| 9 | +updatedAt: 2026-10-06T03:56:22.404Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | - priority::medium |
| @@ -19,7 +19,7 @@ body: | | ||
| 19 | 19 | demo-llmman/ using hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M. |
| 20 | 20 | tasks: |
| 21 | 21 | - id: 1 |
| 22 | - title: "Registry entry llmman (URL, fallback, key, progress, thinking off)" | |
| 22 | + title: Registry entry llmman (URL, fallback, key, progress, thinking off) | |
| 23 | 23 | state: closed |
| 24 | 24 | priority: none |
| 25 | 25 | author: |
| @@ -46,7 +46,7 @@ tasks: | ||
| 46 | 46 | createdAt: 2026-10-01T07:00:00.000Z |
| 47 | 47 | updatedAt: 2026-10-01T07:00:00.000Z |
| 48 | 48 | - id: 4 |
| 49 | - title: "demo-llmman/agent.llmman.yaml with hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M" | |
| 49 | + title: demo-llmman/agent.llmman.yaml with hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 50 | 50 | state: closed |
| 51 | 51 | priority: none |
| 52 | 52 | author: |
| @@ -55,7 +55,7 @@ tasks: | ||
| 55 | 55 | createdAt: 2026-10-01T07:00:00.000Z |
| 56 | 56 | updatedAt: 2026-10-01T07:00:00.000Z |
| 57 | 57 | - id: 5 |
| 58 | - title: "Docs EN+FR (how-to, reference, explanation, indexes)" | |
| 58 | + title: Docs EN+FR (how-to, reference, explanation, indexes) | |
| 59 | 59 | state: closed |
| 60 | 60 | priority: none |
| 61 | 61 | author: |
| @@ -64,7 +64,7 @@ tasks: | ||
| 64 | 64 | createdAt: 2026-10-01T07:00:00.000Z |
| 65 | 65 | updatedAt: 2026-10-01T07:00:00.000Z |
| 66 | 66 | - id: 6 |
| 67 | - title: "Live check against llmman serve (tool call, ctx, compaction)" | |
| 67 | + title: Live check against llmman serve (tool call, ctx, compaction) | |
| 68 | 68 | state: closed |
| 69 | 69 | priority: none |
| 70 | 70 | author: |
| @@ -73,7 +73,7 @@ tasks: | ||
| 73 | 73 | createdAt: 2026-10-01T07:00:00.000Z |
| 74 | 74 | updatedAt: 2026-10-01T07:00:00.000Z |
| 75 | 75 | - id: 7 |
| 76 | - title: "Quality gate (qlty) where available" | |
| 76 | + title: Quality gate (qlty) where available | |
| 77 | 77 | state: open |
| 78 | 78 | priority: none |
| 79 | 79 | author: |
| @@ -82,7 +82,7 @@ tasks: | ||
| 82 | 82 | createdAt: 2026-10-01T07:00:00.000Z |
| 83 | 83 | updatedAt: 2026-10-01T07:00:00.000Z |
| 84 | 84 | - id: 8 |
| 85 | - title: "Review, commit, merge" | |
| 85 | + title: Review, commit, merge | |
| 86 | 86 | state: open |
| 87 | 87 | priority: none |
| 88 | 88 | author: |
| @@ -1,12 +1,12 @@ | |||
| 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: 60 | 2 | id: 60 |
| 3 | title: Add llmman provider | 3 | title: Add llmman provider |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-01T04:48:02.829Z | 8 | createdAt: 2026-10-01T04:48:02.829Z |
| 9 | -updatedAt: 2026-10-01T07:00:00.000Z | 9 | +updatedAt: 2026-10-06T03:56:22.404Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | - priority::medium | 12 | - priority::medium |
| @@ -19,7 +19,7 @@ body: | | |||
| 19 | demo-llmman/ using hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M. | 19 | demo-llmman/ using hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M. |
| 20 | tasks: | 20 | tasks: |
| 21 | - id: 1 | 21 | - id: 1 |
| 22 | - title: "Registry entry llmman (URL, fallback, key, progress, thinking off)" | 22 | + title: Registry entry llmman (URL, fallback, key, progress, thinking off) |
| 23 | state: closed | 23 | state: closed |
| 24 | priority: none | 24 | priority: none |
| 25 | author: | 25 | author: |
| @@ -46,7 +46,7 @@ tasks: | |||
| 46 | createdAt: 2026-10-01T07:00:00.000Z | 46 | createdAt: 2026-10-01T07:00:00.000Z |
| 47 | updatedAt: 2026-10-01T07:00:00.000Z | 47 | updatedAt: 2026-10-01T07:00:00.000Z |
| 48 | - id: 4 | 48 | - id: 4 |
| 49 | - title: "demo-llmman/agent.llmman.yaml with hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M" | 49 | + title: demo-llmman/agent.llmman.yaml with hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M |
| 50 | state: closed | 50 | state: closed |
| 51 | priority: none | 51 | priority: none |
| 52 | author: | 52 | author: |
| @@ -55,7 +55,7 @@ tasks: | |||
| 55 | createdAt: 2026-10-01T07:00:00.000Z | 55 | createdAt: 2026-10-01T07:00:00.000Z |
| 56 | updatedAt: 2026-10-01T07:00:00.000Z | 56 | updatedAt: 2026-10-01T07:00:00.000Z |
| 57 | - id: 5 | 57 | - id: 5 |
| 58 | - title: "Docs EN+FR (how-to, reference, explanation, indexes)" | 58 | + title: Docs EN+FR (how-to, reference, explanation, indexes) |
| 59 | state: closed | 59 | state: closed |
| 60 | priority: none | 60 | priority: none |
| 61 | author: | 61 | author: |
| @@ -64,7 +64,7 @@ tasks: | |||
| 64 | createdAt: 2026-10-01T07:00:00.000Z | 64 | createdAt: 2026-10-01T07:00:00.000Z |
| 65 | updatedAt: 2026-10-01T07:00:00.000Z | 65 | updatedAt: 2026-10-01T07:00:00.000Z |
| 66 | - id: 6 | 66 | - id: 6 |
| 67 | - title: "Live check against llmman serve (tool call, ctx, compaction)" | 67 | + title: Live check against llmman serve (tool call, ctx, compaction) |
| 68 | state: closed | 68 | state: closed |
| 69 | priority: none | 69 | priority: none |
| 70 | author: | 70 | author: |
| @@ -73,7 +73,7 @@ tasks: | |||
| 73 | createdAt: 2026-10-01T07:00:00.000Z | 73 | createdAt: 2026-10-01T07:00:00.000Z |
| 74 | updatedAt: 2026-10-01T07:00:00.000Z | 74 | updatedAt: 2026-10-01T07:00:00.000Z |
| 75 | - id: 7 | 75 | - id: 7 |
| 76 | - title: "Quality gate (qlty) where available" | 76 | + title: Quality gate (qlty) where available |
| 77 | state: open | 77 | state: open |
| 78 | priority: none | 78 | priority: none |
| 79 | author: | 79 | author: |
| @@ -82,7 +82,7 @@ tasks: | |||
| 82 | createdAt: 2026-10-01T07:00:00.000Z | 82 | createdAt: 2026-10-01T07:00:00.000Z |
| 83 | updatedAt: 2026-10-01T07:00:00.000Z | 83 | updatedAt: 2026-10-01T07:00:00.000Z |
| 84 | - id: 8 | 84 | - id: 8 |
| 85 | - title: "Review, commit, merge" | 85 | + title: Review, commit, merge |
| 86 | state: open | 86 | state: open |
| 87 | priority: none | 87 | priority: none |
| 88 | author: | 88 | author: |
modified
.tickets/issues/0062-add-ollama-provider.yaml +11 -11 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 62 |
| 3 | 3 | title: Add Ollama provider |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-01T06:10:07.295Z |
| 9 | -updatedAt: 2026-10-01T07:00:00.000Z | |
| 9 | +updatedAt: 2026-10-06T03:56:14.111Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | - priority::medium |
| @@ -19,7 +19,7 @@ body: | | ||
| 19 | 19 | later is one field to change. |
| 20 | 20 | tasks: |
| 21 | 21 | - id: 1 |
| 22 | - title: "Measure Ollama's /v1 (reasoning field, progress, thinking switch, window, 404)" | |
| 22 | + title: Measure Ollama's /v1 (reasoning field, progress, thinking switch, window, 404) | |
| 23 | 23 | state: closed |
| 24 | 24 | priority: none |
| 25 | 25 | author: |
| @@ -28,7 +28,7 @@ tasks: | ||
| 28 | 28 | createdAt: 2026-10-01T07:00:00.000Z |
| 29 | 29 | updatedAt: 2026-10-01T07:00:00.000Z |
| 30 | 30 | - id: 2 |
| 31 | - title: "Per-provider thinkingOff field; reasoning_effort none for Ollama" | |
| 31 | + title: Per-provider thinkingOff field; reasoning_effort none for Ollama | |
| 32 | 32 | state: closed |
| 33 | 33 | priority: none |
| 34 | 34 | author: |
| @@ -37,7 +37,7 @@ tasks: | ||
| 37 | 37 | createdAt: 2026-10-01T07:00:00.000Z |
| 38 | 38 | updatedAt: 2026-10-01T07:00:00.000Z |
| 39 | 39 | - id: 3 |
| 40 | - title: "Registry entry ollama + probeOllama (shared /api/ps reading with llmman)" | |
| 40 | + title: Registry entry ollama + probeOllama (shared /api/ps reading with llmman) | |
| 41 | 41 | state: closed |
| 42 | 42 | priority: none |
| 43 | 43 | author: |
| @@ -55,7 +55,7 @@ tasks: | ||
| 55 | 55 | createdAt: 2026-10-01T07:00:00.000Z |
| 56 | 56 | updatedAt: 2026-10-01T07:00:00.000Z |
| 57 | 57 | - id: 5 |
| 58 | - title: "demo-ollama/agent.ollama.yaml with qwen3.5:2b" | |
| 58 | + title: demo-ollama/agent.ollama.yaml with qwen3.5:2b | |
| 59 | 59 | state: closed |
| 60 | 60 | priority: none |
| 61 | 61 | author: |
| @@ -64,7 +64,7 @@ tasks: | ||
| 64 | 64 | createdAt: 2026-10-01T07:00:00.000Z |
| 65 | 65 | updatedAt: 2026-10-01T07:00:00.000Z |
| 66 | 66 | - id: 6 |
| 67 | - title: "Docs EN+FR (how-to, reference, explanation, indexes)" | |
| 67 | + title: Docs EN+FR (how-to, reference, explanation, indexes) | |
| 68 | 68 | state: closed |
| 69 | 69 | priority: none |
| 70 | 70 | author: |
| @@ -73,7 +73,7 @@ tasks: | ||
| 73 | 73 | createdAt: 2026-10-01T07:00:00.000Z |
| 74 | 74 | updatedAt: 2026-10-01T07:00:00.000Z |
| 75 | 75 | - id: 7 |
| 76 | - title: "Live check against Ollama (tool call, ctx, compaction)" | |
| 76 | + title: Live check against Ollama (tool call, ctx, compaction) | |
| 77 | 77 | state: closed |
| 78 | 78 | priority: none |
| 79 | 79 | author: |
| @@ -82,7 +82,7 @@ tasks: | ||
| 82 | 82 | createdAt: 2026-10-01T07:00:00.000Z |
| 83 | 83 | updatedAt: 2026-10-01T07:00:00.000Z |
| 84 | 84 | - id: 8 |
| 85 | - title: "Check reasoning_effort none on a non-thinking model" | |
| 85 | + title: Check reasoning_effort none on a non-thinking model | |
| 86 | 86 | state: open |
| 87 | 87 | priority: none |
| 88 | 88 | author: |
| @@ -91,7 +91,7 @@ tasks: | ||
| 91 | 91 | createdAt: 2026-10-01T07:00:00.000Z |
| 92 | 92 | updatedAt: 2026-10-01T07:00:00.000Z |
| 93 | 93 | - id: 9 |
| 94 | - title: "Quality gate (qlty) where available" | |
| 94 | + title: Quality gate (qlty) where available | |
| 95 | 95 | state: open |
| 96 | 96 | priority: none |
| 97 | 97 | author: |
| @@ -100,7 +100,7 @@ tasks: | ||
| 100 | 100 | createdAt: 2026-10-01T07:00:00.000Z |
| 101 | 101 | updatedAt: 2026-10-01T07:00:00.000Z |
| 102 | 102 | - id: 10 |
| 103 | - title: "Review, commit, merge" | |
| 103 | + title: Review, commit, merge | |
| 104 | 104 | state: open |
| 105 | 105 | priority: none |
| 106 | 106 | author: |
| @@ -1,12 +1,12 @@ | |||
| 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: 62 | 2 | id: 62 |
| 3 | title: Add Ollama provider | 3 | title: Add Ollama provider |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-01T06:10:07.295Z | 8 | createdAt: 2026-10-01T06:10:07.295Z |
| 9 | -updatedAt: 2026-10-01T07:00:00.000Z | 9 | +updatedAt: 2026-10-06T03:56:14.111Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | - priority::medium | 12 | - priority::medium |
| @@ -19,7 +19,7 @@ body: | | |||
| 19 | later is one field to change. | 19 | later is one field to change. |
| 20 | tasks: | 20 | tasks: |
| 21 | - id: 1 | 21 | - id: 1 |
| 22 | - title: "Measure Ollama's /v1 (reasoning field, progress, thinking switch, window, 404)" | 22 | + title: Measure Ollama's /v1 (reasoning field, progress, thinking switch, window, 404) |
| 23 | state: closed | 23 | state: closed |
| 24 | priority: none | 24 | priority: none |
| 25 | author: | 25 | author: |
| @@ -28,7 +28,7 @@ tasks: | |||
| 28 | createdAt: 2026-10-01T07:00:00.000Z | 28 | createdAt: 2026-10-01T07:00:00.000Z |
| 29 | updatedAt: 2026-10-01T07:00:00.000Z | 29 | updatedAt: 2026-10-01T07:00:00.000Z |
| 30 | - id: 2 | 30 | - id: 2 |
| 31 | - title: "Per-provider thinkingOff field; reasoning_effort none for Ollama" | 31 | + title: Per-provider thinkingOff field; reasoning_effort none for Ollama |
| 32 | state: closed | 32 | state: closed |
| 33 | priority: none | 33 | priority: none |
| 34 | author: | 34 | author: |
| @@ -37,7 +37,7 @@ tasks: | |||
| 37 | createdAt: 2026-10-01T07:00:00.000Z | 37 | createdAt: 2026-10-01T07:00:00.000Z |
| 38 | updatedAt: 2026-10-01T07:00:00.000Z | 38 | updatedAt: 2026-10-01T07:00:00.000Z |
| 39 | - id: 3 | 39 | - id: 3 |
| 40 | - title: "Registry entry ollama + probeOllama (shared /api/ps reading with llmman)" | 40 | + title: Registry entry ollama + probeOllama (shared /api/ps reading with llmman) |
| 41 | state: closed | 41 | state: closed |
| 42 | priority: none | 42 | priority: none |
| 43 | author: | 43 | author: |
| @@ -55,7 +55,7 @@ tasks: | |||
| 55 | createdAt: 2026-10-01T07:00:00.000Z | 55 | createdAt: 2026-10-01T07:00:00.000Z |
| 56 | updatedAt: 2026-10-01T07:00:00.000Z | 56 | updatedAt: 2026-10-01T07:00:00.000Z |
| 57 | - id: 5 | 57 | - id: 5 |
| 58 | - title: "demo-ollama/agent.ollama.yaml with qwen3.5:2b" | 58 | + title: demo-ollama/agent.ollama.yaml with qwen3.5:2b |
| 59 | state: closed | 59 | state: closed |
| 60 | priority: none | 60 | priority: none |
| 61 | author: | 61 | author: |
| @@ -64,7 +64,7 @@ tasks: | |||
| 64 | createdAt: 2026-10-01T07:00:00.000Z | 64 | createdAt: 2026-10-01T07:00:00.000Z |
| 65 | updatedAt: 2026-10-01T07:00:00.000Z | 65 | updatedAt: 2026-10-01T07:00:00.000Z |
| 66 | - id: 6 | 66 | - id: 6 |
| 67 | - title: "Docs EN+FR (how-to, reference, explanation, indexes)" | 67 | + title: Docs EN+FR (how-to, reference, explanation, indexes) |
| 68 | state: closed | 68 | state: closed |
| 69 | priority: none | 69 | priority: none |
| 70 | author: | 70 | author: |
| @@ -73,7 +73,7 @@ tasks: | |||
| 73 | createdAt: 2026-10-01T07:00:00.000Z | 73 | createdAt: 2026-10-01T07:00:00.000Z |
| 74 | updatedAt: 2026-10-01T07:00:00.000Z | 74 | updatedAt: 2026-10-01T07:00:00.000Z |
| 75 | - id: 7 | 75 | - id: 7 |
| 76 | - title: "Live check against Ollama (tool call, ctx, compaction)" | 76 | + title: Live check against Ollama (tool call, ctx, compaction) |
| 77 | state: closed | 77 | state: closed |
| 78 | priority: none | 78 | priority: none |
| 79 | author: | 79 | author: |
| @@ -82,7 +82,7 @@ tasks: | |||
| 82 | createdAt: 2026-10-01T07:00:00.000Z | 82 | createdAt: 2026-10-01T07:00:00.000Z |
| 83 | updatedAt: 2026-10-01T07:00:00.000Z | 83 | updatedAt: 2026-10-01T07:00:00.000Z |
| 84 | - id: 8 | 84 | - id: 8 |
| 85 | - title: "Check reasoning_effort none on a non-thinking model" | 85 | + title: Check reasoning_effort none on a non-thinking model |
| 86 | state: open | 86 | state: open |
| 87 | priority: none | 87 | priority: none |
| 88 | author: | 88 | author: |
| @@ -91,7 +91,7 @@ tasks: | |||
| 91 | createdAt: 2026-10-01T07:00:00.000Z | 91 | createdAt: 2026-10-01T07:00:00.000Z |
| 92 | updatedAt: 2026-10-01T07:00:00.000Z | 92 | updatedAt: 2026-10-01T07:00:00.000Z |
| 93 | - id: 9 | 93 | - id: 9 |
| 94 | - title: "Quality gate (qlty) where available" | 94 | + title: Quality gate (qlty) where available |
| 95 | state: open | 95 | state: open |
| 96 | priority: none | 96 | priority: none |
| 97 | author: | 97 | author: |
| @@ -100,7 +100,7 @@ tasks: | |||
| 100 | createdAt: 2026-10-01T07:00:00.000Z | 100 | createdAt: 2026-10-01T07:00:00.000Z |
| 101 | updatedAt: 2026-10-01T07:00:00.000Z | 101 | updatedAt: 2026-10-01T07:00:00.000Z |
| 102 | - id: 10 | 102 | - id: 10 |
| 103 | - title: "Review, commit, merge" | 103 | + title: Review, commit, merge |
| 104 | state: open | 104 | state: open |
| 105 | priority: none | 105 | priority: none |
| 106 | author: | 106 | author: |
modified
.tickets/issues/0066-talk-to-another-mm.yaml +8 -8 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 66 |
| 3 | 3 | title: talk to another mm |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T18:48:36.275Z |
| 9 | -updatedAt: 2026-10-02T21:15:00.000Z | |
| 9 | +updatedAt: 2026-10-06T03:55:28.931Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | related: |
| @@ -226,7 +226,7 @@ body: | | ||
| 226 | 226 | c. Editor: one /ask with a hint, or one command per agent? |
| 227 | 227 | tasks: |
| 228 | 228 | - id: 1 |
| 229 | - title: "Answer the open questions (key name, immediate answer, multi-turn, fronts, context passed)" | |
| 229 | + title: Answer the open questions (key name, immediate answer, multi-turn, fronts, context passed) | |
| 230 | 230 | state: closed |
| 231 | 231 | priority: none |
| 232 | 232 | author: |
| @@ -235,7 +235,7 @@ tasks: | ||
| 235 | 235 | createdAt: 2026-10-02T19:30:00.000Z |
| 236 | 236 | updatedAt: 2026-10-02T20:00:00.000Z |
| 237 | 237 | - id: 2 |
| 238 | - title: "Confirm the remaining points a/b/c (context format, fresh session per /ask, editor hint vs one command per agent)" | |
| 238 | + title: Confirm the remaining points a/b/c (context format, fresh session per /ask, editor hint vs one command per agent) | |
| 239 | 239 | state: closed |
| 240 | 240 | priority: none |
| 241 | 241 | author: |
| @@ -289,7 +289,7 @@ tasks: | ||
| 289 | 289 | createdAt: 2026-10-02T19:30:00.000Z |
| 290 | 290 | updatedAt: 2026-10-02T20:00:00.000Z |
| 291 | 291 | - id: 8 |
| 292 | - title: "Insert the exchange in the current history (format, cap, valid alternation), persisted by the store and replayed on /resume" | |
| 292 | + title: Insert the exchange in the current history (format, cap, valid alternation), persisted by the store and replayed on /resume | |
| 293 | 293 | state: closed |
| 294 | 294 | priority: none |
| 295 | 295 | author: |
| @@ -298,7 +298,7 @@ tasks: | ||
| 298 | 298 | createdAt: 2026-10-02T19:30:00.000Z |
| 299 | 299 | updatedAt: 2026-10-02T20:00:00.000Z |
| 300 | 300 | - id: 9 |
| 301 | - title: "/abort, Ctrl+C and editor cancel -> session/cancel on the child; child crash -> one-line error, history untouched" | |
| 301 | + title: /abort, Ctrl+C and editor cancel -> session/cancel on the child; child crash -> one-line error, history untouched | |
| 302 | 302 | state: closed |
| 303 | 303 | priority: none |
| 304 | 304 | author: |
| @@ -334,7 +334,7 @@ tasks: | ||
| 334 | 334 | createdAt: 2026-10-02T20:30:00.000Z |
| 335 | 335 | updatedAt: 2026-10-02T20:30:00.000Z |
| 336 | 336 | - id: 13 |
| 337 | - title: "Live check of the window mode (/ask-jan in mm -tui) — unit-tested only" | |
| 337 | + title: Live check of the window mode (/ask-jan in mm -tui) — unit-tested only | |
| 338 | 338 | state: open |
| 339 | 339 | priority: none |
| 340 | 340 | author: |
| @@ -343,7 +343,7 @@ tasks: | ||
| 343 | 343 | createdAt: 2026-10-02T20:30:00.000Z |
| 344 | 344 | updatedAt: 2026-10-02T20:30:00.000Z |
| 345 | 345 | - id: 14 |
| 346 | - title: "Review, commit, merge" | |
| 346 | + title: Review, commit, merge | |
| 347 | 347 | state: open |
| 348 | 348 | priority: none |
| 349 | 349 | author: |
| @@ -1,12 +1,12 @@ | |||
| 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: 66 | 2 | id: 66 |
| 3 | title: talk to another mm | 3 | title: talk to another mm |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T18:48:36.275Z | 8 | createdAt: 2026-10-02T18:48:36.275Z |
| 9 | -updatedAt: 2026-10-02T21:15:00.000Z | 9 | +updatedAt: 2026-10-06T03:55:28.931Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | related: | 12 | related: |
| @@ -226,7 +226,7 @@ body: | | |||
| 226 | c. Editor: one /ask with a hint, or one command per agent? | 226 | c. Editor: one /ask with a hint, or one command per agent? |
| 227 | tasks: | 227 | tasks: |
| 228 | - id: 1 | 228 | - id: 1 |
| 229 | - title: "Answer the open questions (key name, immediate answer, multi-turn, fronts, context passed)" | 229 | + title: Answer the open questions (key name, immediate answer, multi-turn, fronts, context passed) |
| 230 | state: closed | 230 | state: closed |
| 231 | priority: none | 231 | priority: none |
| 232 | author: | 232 | author: |
| @@ -235,7 +235,7 @@ tasks: | |||
| 235 | createdAt: 2026-10-02T19:30:00.000Z | 235 | createdAt: 2026-10-02T19:30:00.000Z |
| 236 | updatedAt: 2026-10-02T20:00:00.000Z | 236 | updatedAt: 2026-10-02T20:00:00.000Z |
| 237 | - id: 2 | 237 | - id: 2 |
| 238 | - title: "Confirm the remaining points a/b/c (context format, fresh session per /ask, editor hint vs one command per agent)" | 238 | + title: Confirm the remaining points a/b/c (context format, fresh session per /ask, editor hint vs one command per agent) |
| 239 | state: closed | 239 | state: closed |
| 240 | priority: none | 240 | priority: none |
| 241 | author: | 241 | author: |
| @@ -289,7 +289,7 @@ tasks: | |||
| 289 | createdAt: 2026-10-02T19:30:00.000Z | 289 | createdAt: 2026-10-02T19:30:00.000Z |
| 290 | updatedAt: 2026-10-02T20:00:00.000Z | 290 | updatedAt: 2026-10-02T20:00:00.000Z |
| 291 | - id: 8 | 291 | - id: 8 |
| 292 | - title: "Insert the exchange in the current history (format, cap, valid alternation), persisted by the store and replayed on /resume" | 292 | + title: Insert the exchange in the current history (format, cap, valid alternation), persisted by the store and replayed on /resume |
| 293 | state: closed | 293 | state: closed |
| 294 | priority: none | 294 | priority: none |
| 295 | author: | 295 | author: |
| @@ -298,7 +298,7 @@ tasks: | |||
| 298 | createdAt: 2026-10-02T19:30:00.000Z | 298 | createdAt: 2026-10-02T19:30:00.000Z |
| 299 | updatedAt: 2026-10-02T20:00:00.000Z | 299 | updatedAt: 2026-10-02T20:00:00.000Z |
| 300 | - id: 9 | 300 | - id: 9 |
| 301 | - title: "/abort, Ctrl+C and editor cancel -> session/cancel on the child; child crash -> one-line error, history untouched" | 301 | + title: /abort, Ctrl+C and editor cancel -> session/cancel on the child; child crash -> one-line error, history untouched |
| 302 | state: closed | 302 | state: closed |
| 303 | priority: none | 303 | priority: none |
| 304 | author: | 304 | author: |
| @@ -334,7 +334,7 @@ tasks: | |||
| 334 | createdAt: 2026-10-02T20:30:00.000Z | 334 | createdAt: 2026-10-02T20:30:00.000Z |
| 335 | updatedAt: 2026-10-02T20:30:00.000Z | 335 | updatedAt: 2026-10-02T20:30:00.000Z |
| 336 | - id: 13 | 336 | - id: 13 |
| 337 | - title: "Live check of the window mode (/ask-jan in mm -tui) — unit-tested only" | 337 | + title: Live check of the window mode (/ask-jan in mm -tui) — unit-tested only |
| 338 | state: open | 338 | state: open |
| 339 | priority: none | 339 | priority: none |
| 340 | author: | 340 | author: |
| @@ -343,7 +343,7 @@ tasks: | |||
| 343 | createdAt: 2026-10-02T20:30:00.000Z | 343 | createdAt: 2026-10-02T20:30:00.000Z |
| 344 | updatedAt: 2026-10-02T20:30:00.000Z | 344 | updatedAt: 2026-10-02T20:30:00.000Z |
| 345 | - id: 14 | 345 | - id: 14 |
| 346 | - title: "Review, commit, merge" | 346 | + title: Review, commit, merge |
| 347 | state: open | 347 | state: open |
| 348 | priority: none | 348 | priority: none |
| 349 | author: | 349 | author: |
modified
.tickets/issues/0067-enable-disable-reasoning-mode.yaml +187 -5 | @@ -6,8 +6,190 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T06:09:42.599Z |
| 9 | -updatedAt: 2026-10-02T06:09:42.599Z | |
| 10 | -labels: [] | |
| 11 | -body: "" | |
| 12 | -tasks: [] | |
| 13 | -comments: [] | |
| 9 | +updatedAt: 2026-10-06T14:00:00.000Z | |
| 10 | +labels: | |
| 11 | + - kind::feature | |
| 12 | +related: | |
| 13 | + - 62 | |
| 14 | +body: | | |
| 15 | + Question (user, 2026-10-06): can mm turn a model's reasoning mode on or | |
| 16 | + off? | |
| 17 | + | |
| 18 | + Answer as of 2026-10-06 (branch feature/yolo, code verified): | |
| 19 | + | |
| 20 | + 1. HIDE it: yes. `showThinking: false` hides the reasoning in the three | |
| 21 | + front ends. The model still reasons (same time, same tokens): only | |
| 22 | + the display changes. | |
| 23 | + | |
| 24 | + 2. TURN IT OFF for the /compact summary only: done automatically, per | |
| 25 | + provider (`thinkingOff`, internal/engine/openai_compat.go): | |
| 26 | + dmr / llamacpp / llmman -> `chat_template_kwargs: {"enable_thinking": false}`, | |
| 27 | + ollama -> `reasoning_effort: "none"` (ticket 62: Ollama ignores | |
| 28 | + chat_template_kwargs). Not configurable, never applied to normal turns. | |
| 29 | + | |
| 30 | + 3. TURN IT OFF for every turn through `sampling`: only half works. | |
| 31 | + - `sampling.reasoning_effort: none` reaches the server (Ollama should | |
| 32 | + obey it; not verified live on a normal turn). | |
| 33 | + - `sampling.chat_template_kwargs: {enable_thinking: false}` is | |
| 34 | + SILENTLY DROPPED before the request leaves mm, so it is impossible | |
| 35 | + from the config for dmr / llamacpp / llmman, the servers that need | |
| 36 | + exactly that field. | |
| 37 | + | |
| 38 | + ## Why `chat_template_kwargs` is dropped | |
| 39 | + | |
| 40 | + `chat_template_kwargs` is not an OpenAI API field: it is a llama.cpp | |
| 41 | + extension (llama-server passes it to the model's Jinja chat template, | |
| 42 | + where `enable_thinking` switches Qwen3-style thinking off). DMR and | |
| 43 | + llmman run llama-server underneath, so they expect it too. | |
| 44 | + | |
| 45 | + The `sampling` map does not go to the server as is. Its road: | |
| 46 | + | |
| 47 | + 1. internal/engine/engine.go:353 hands it to Genkit: | |
| 48 | + `ai.WithConfig(config.Cfg.Sampling)`. | |
| 49 | + 2. Genkit's compat_oai plugin (plugins/compat_oai/generate.go:142, | |
| 50 | + `WithConfig`) converts the map into the TYPED request struct of | |
| 51 | + openai-go: `base.MapToStruct[openai.ChatCompletionNewParams](cfg)`. | |
| 52 | + 3. `MapToStruct` (genkit internal/base/json.go:183) is a JSON round trip: | |
| 53 | + `json.Marshal(map)` then `json.Unmarshal` into the struct. | |
| 54 | + 4. `ChatCompletionNewParams` has a field for every OpenAI parameter — | |
| 55 | + `temperature`, `top_p`, `max_tokens`, `parallel_tool_calls`, | |
| 56 | + `reasoning_effort` … — and NONE for `chat_template_kwargs`. | |
| 57 | + `json.Unmarshal` ignores keys that match no field: the key is gone. | |
| 58 | + 5. The HTTP body is then serialised FROM THE STRUCT, so the server never | |
| 59 | + sees it. | |
| 60 | + | |
| 61 | + Measured with a round trip in the module (openai-go v1.8.2): | |
| 62 | + input `{"temperature":0, "reasoning_effort":"none", | |
| 63 | + "chat_template_kwargs":{"enable_thinking":false}}` | |
| 64 | + output `{"temperature":0,"reasoning_effort":"none"}`. | |
| 65 | + | |
| 66 | + Same reason the summary path does NOT use `sampling`: mm adds the field | |
| 67 | + in its own openai-go middleware (`streamOptions` -> `withoutReasoning`, | |
| 68 | + internal/engine/usage.go:83 and :137), which edits the raw HTTP body | |
| 69 | + AFTER serialisation, where nothing can drop it any more. | |
| 70 | + | |
| 71 | + Workarounds today (no code change): server side — | |
| 72 | + `llama-server --reasoning-budget 0` (or `--chat-template-kwargs | |
| 73 | + '{"enable_thinking":false}'` on recent builds — neither flag verified | |
| 74 | + in this sandbox), or a model without a thinking mode. | |
| 75 | + | |
| 76 | + ## Proposal | |
| 77 | + | |
| 78 | + A config key `thinking: true|false` (default true, override | |
| 79 | + `AGENT_THINKING`), applied by the SAME middleware to every request when | |
| 80 | + false: the provider's own way (`thinkingOff`), so it works on | |
| 81 | + dmr/llamacpp/llmman (chat_template_kwargs) and ollama (reasoning_effort). | |
| 82 | + A request that already carries the field keeps it (rule already in | |
| 83 | + `withoutReasoning`). Optional: `/think on|off` to switch during a session. | |
| 84 | +tasks: | |
| 85 | + - id: 1 | |
| 86 | + title: "Decide: key name (`thinking`?), env override, and whether `/think on|off` is wanted" | |
| 87 | + state: open | |
| 88 | + priority: none | |
| 89 | + author: | |
| 90 | + name: k33g | |
| 91 | + email: ph.charriere@gmail.com | |
| 92 | + createdAt: 2026-10-06T13:00:00.000Z | |
| 93 | + updatedAt: 2026-10-06T13:00:00.000Z | |
| 94 | + - id: 2 | |
| 95 | + title: "Failing tests first: with thinking off, every request body carries the provider's field (template / effort); unknown provider = unchanged" | |
| 96 | + state: open | |
| 97 | + priority: none | |
| 98 | + author: | |
| 99 | + name: k33g | |
| 100 | + email: ph.charriere@gmail.com | |
| 101 | + createdAt: 2026-10-06T13:00:00.000Z | |
| 102 | + updatedAt: 2026-10-06T13:00:00.000Z | |
| 103 | + - id: 3 | |
| 104 | + title: "Config key + AGENT_THINKING; engine: mark every request (not only summaries) with noThinkKey when off" | |
| 105 | + state: open | |
| 106 | + priority: none | |
| 107 | + author: | |
| 108 | + name: k33g | |
| 109 | + email: ph.charriere@gmail.com | |
| 110 | + createdAt: 2026-10-06T13:00:00.000Z | |
| 111 | + updatedAt: 2026-10-06T13:00:00.000Z | |
| 112 | + - id: 4 | |
| 113 | + title: "Banner shows `thinking: off`; `showThinking` stays a display setting" | |
| 114 | + state: open | |
| 115 | + priority: none | |
| 116 | + author: | |
| 117 | + name: k33g | |
| 118 | + email: ph.charriere@gmail.com | |
| 119 | + createdAt: 2026-10-06T13:00:00.000Z | |
| 120 | + updatedAt: 2026-10-06T13:00:00.000Z | |
| 121 | + - id: 5 | |
| 122 | + title: "Live check per provider: DMR + llmman (chat_template_kwargs), Ollama (reasoning_effort none on a normal turn)" | |
| 123 | + state: open | |
| 124 | + priority: none | |
| 125 | + author: | |
| 126 | + name: k33g | |
| 127 | + email: ph.charriere@gmail.com | |
| 128 | + createdAt: 2026-10-06T13:00:00.000Z | |
| 129 | + updatedAt: 2026-10-06T13:00:00.000Z | |
| 130 | + - id: 6 | |
| 131 | + title: "Docs EN+FR: reference configuration, how-to, explanation/thinking-models (why sampling cannot do it)" | |
| 132 | + state: open | |
| 133 | + priority: none | |
| 134 | + author: | |
| 135 | + name: k33g | |
| 136 | + email: ph.charriere@gmail.com | |
| 137 | + createdAt: 2026-10-06T13:00:00.000Z | |
| 138 | + updatedAt: 2026-10-06T13:00:00.000Z | |
| 139 | +comments: | |
| 140 | + - id: 1 | |
| 141 | + author: | |
| 142 | + name: claude | |
| 143 | + email: noreply@anthropic.com | |
| 144 | + createdAt: 2026-10-06T14:00:00.000Z | |
| 145 | + body: | | |
| 146 | + **Correction: a newer Genkit fixes the "sampling" road (verified in a | |
| 147 | + scratch copy, the repo is unchanged — still genkit v1.10.0).** | |
| 148 | + | |
| 149 | + Latest Genkit Go is v1.13.1 (2026-09-03). Since **v1.12.0**, | |
| 150 | + compat_oai's `WithConfig` keeps the keys `ChatCompletionNewParams` | |
| 151 | + does not know and sends them as JSON extras (`SetExtraFields`, | |
| 152 | + plugins/compat_oai/generate.go ~l.231: "the OpenAI SDK silently | |
| 153 | + ignores provider-specific fields while unmarshaling, so retain them | |
| 154 | + as JSON extras"). v1.11.0 does not have it. | |
| 155 | + | |
| 156 | + BUT bumping alone is not enough — found by testing: | |
| 157 | + 1. In v1.13.1 `OpenAICompatible.DefineModel` is DEPRECATED and | |
| 158 | + returns an UNREGISTERED model. mm discards its result | |
| 159 | + (internal/engine/openai_compat.go:189), so the model actually used | |
| 160 | + is the one the plugin resolves on the fly (`ResolveAction` -> | |
| 161 | + `newSDKModel`), whose config schema is the strict reflected SDK | |
| 162 | + schema: `chat_template_kwargs` is then REJECTED at the action | |
| 163 | + boundary — "config: Additional property chat_template_kwargs is | |
| 164 | + not allowed" (and every unknown sampling key would fail the | |
| 165 | + request, where v1.10.0 dropped it silently). | |
| 166 | + `ModelOptions.ConfigSchema` does not help: it is set on the model | |
| 167 | + nobody registered. | |
| 168 | + 2. Fix: register it — `genkit.RegisterAction(g, | |
| 169 | + client.DefineModel(...).(*ai.ModelAction))`. DefineModel's model | |
| 170 | + takes its config untyped, no validation, unknown keys ride to the | |
| 171 | + wire. (The non-deprecated `NewModel` validates against the strict | |
| 172 | + schema; it would need an explicit permissive `ConfigSchema`.) | |
| 173 | + | |
| 174 | + Measured (recording server): v1.13.1 + RegisterAction sends | |
| 175 | + `"chat_template_kwargs": {"enable_thinking": false}` with mm's own | |
| 176 | + middleware fields (return_progress, stream_options) intact. Live on | |
| 177 | + DMR qwen3.5-4b, `-p "What is 17*23?"` with | |
| 178 | + `sampling.chat_template_kwargs.enable_thinking: false`: current mm = | |
| 179 | + 4 s, a 💭 reasoning block, then 391; patched mm = under 1 s, no | |
| 180 | + reasoning, 391. | |
| 181 | + | |
| 182 | + Upgrade side effects seen: `go get` + `go mod tidy` also bump | |
| 183 | + dotprompt, invopop/jsonschema v0.14.0, goccy/go-yaml, jsonparser and | |
| 184 | + add go.yaml.in/yaml/v4; `go test ./...` green (scratch copy; the | |
| 185 | + skills test failed only because the copy had no shipped YAML); | |
| 186 | + Genkit now logs `INFO Genkit initialized …` on stderr at start-up | |
| 187 | + (to silence). Not re-checked after the bump: reasoning.go's tap and | |
| 188 | + toolargs.go's workarounds (v1.13.1 reads reasoning_content itself | |
| 189 | + now — generate.go ~l.548), the ollama/llmman paths. | |
| 190 | + | |
| 191 | + Consequence for the proposal: with the bump, `sampling` alone can | |
| 192 | + carry `chat_template_kwargs` (llama.cpp family) or | |
| 193 | + `reasoning_effort` (Ollama) — per-server spelling left to the user. | |
| 194 | + A `thinking: false` key is still worth it for one spelling across | |
| 195 | + providers (it would reuse `withoutReasoning`) and for `/think`. | |
| @@ -6,8 +6,190 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T06:09:42.599Z | 8 | createdAt: 2026-10-02T06:09:42.599Z |
| 9 | -updatedAt: 2026-10-02T06:09:42.599Z | 9 | +updatedAt: 2026-10-06T14:00:00.000Z |
| 10 | -labels: [] | 10 | +labels: |
| 11 | -body: "" | 11 | + - kind::feature |
| 12 | -tasks: [] | 12 | +related: |
| 13 | -comments: [] | 13 | + - 62 |
| 14 | +body: | | ||
| 15 | + Question (user, 2026-10-06): can mm turn a model's reasoning mode on or | ||
| 16 | + off? | ||
| 17 | + | ||
| 18 | + Answer as of 2026-10-06 (branch feature/yolo, code verified): | ||
| 19 | + | ||
| 20 | + 1. HIDE it: yes. `showThinking: false` hides the reasoning in the three | ||
| 21 | + front ends. The model still reasons (same time, same tokens): only | ||
| 22 | + the display changes. | ||
| 23 | + | ||
| 24 | + 2. TURN IT OFF for the /compact summary only: done automatically, per | ||
| 25 | + provider (`thinkingOff`, internal/engine/openai_compat.go): | ||
| 26 | + dmr / llamacpp / llmman -> `chat_template_kwargs: {"enable_thinking": false}`, | ||
| 27 | + ollama -> `reasoning_effort: "none"` (ticket 62: Ollama ignores | ||
| 28 | + chat_template_kwargs). Not configurable, never applied to normal turns. | ||
| 29 | + | ||
| 30 | + 3. TURN IT OFF for every turn through `sampling`: only half works. | ||
| 31 | + - `sampling.reasoning_effort: none` reaches the server (Ollama should | ||
| 32 | + obey it; not verified live on a normal turn). | ||
| 33 | + - `sampling.chat_template_kwargs: {enable_thinking: false}` is | ||
| 34 | + SILENTLY DROPPED before the request leaves mm, so it is impossible | ||
| 35 | + from the config for dmr / llamacpp / llmman, the servers that need | ||
| 36 | + exactly that field. | ||
| 37 | + | ||
| 38 | + ## Why `chat_template_kwargs` is dropped | ||
| 39 | + | ||
| 40 | + `chat_template_kwargs` is not an OpenAI API field: it is a llama.cpp | ||
| 41 | + extension (llama-server passes it to the model's Jinja chat template, | ||
| 42 | + where `enable_thinking` switches Qwen3-style thinking off). DMR and | ||
| 43 | + llmman run llama-server underneath, so they expect it too. | ||
| 44 | + | ||
| 45 | + The `sampling` map does not go to the server as is. Its road: | ||
| 46 | + | ||
| 47 | + 1. internal/engine/engine.go:353 hands it to Genkit: | ||
| 48 | + `ai.WithConfig(config.Cfg.Sampling)`. | ||
| 49 | + 2. Genkit's compat_oai plugin (plugins/compat_oai/generate.go:142, | ||
| 50 | + `WithConfig`) converts the map into the TYPED request struct of | ||
| 51 | + openai-go: `base.MapToStruct[openai.ChatCompletionNewParams](cfg)`. | ||
| 52 | + 3. `MapToStruct` (genkit internal/base/json.go:183) is a JSON round trip: | ||
| 53 | + `json.Marshal(map)` then `json.Unmarshal` into the struct. | ||
| 54 | + 4. `ChatCompletionNewParams` has a field for every OpenAI parameter — | ||
| 55 | + `temperature`, `top_p`, `max_tokens`, `parallel_tool_calls`, | ||
| 56 | + `reasoning_effort` … — and NONE for `chat_template_kwargs`. | ||
| 57 | + `json.Unmarshal` ignores keys that match no field: the key is gone. | ||
| 58 | + 5. The HTTP body is then serialised FROM THE STRUCT, so the server never | ||
| 59 | + sees it. | ||
| 60 | + | ||
| 61 | + Measured with a round trip in the module (openai-go v1.8.2): | ||
| 62 | + input `{"temperature":0, "reasoning_effort":"none", | ||
| 63 | + "chat_template_kwargs":{"enable_thinking":false}}` | ||
| 64 | + output `{"temperature":0,"reasoning_effort":"none"}`. | ||
| 65 | + | ||
| 66 | + Same reason the summary path does NOT use `sampling`: mm adds the field | ||
| 67 | + in its own openai-go middleware (`streamOptions` -> `withoutReasoning`, | ||
| 68 | + internal/engine/usage.go:83 and :137), which edits the raw HTTP body | ||
| 69 | + AFTER serialisation, where nothing can drop it any more. | ||
| 70 | + | ||
| 71 | + Workarounds today (no code change): server side — | ||
| 72 | + `llama-server --reasoning-budget 0` (or `--chat-template-kwargs | ||
| 73 | + '{"enable_thinking":false}'` on recent builds — neither flag verified | ||
| 74 | + in this sandbox), or a model without a thinking mode. | ||
| 75 | + | ||
| 76 | + ## Proposal | ||
| 77 | + | ||
| 78 | + A config key `thinking: true|false` (default true, override | ||
| 79 | + `AGENT_THINKING`), applied by the SAME middleware to every request when | ||
| 80 | + false: the provider's own way (`thinkingOff`), so it works on | ||
| 81 | + dmr/llamacpp/llmman (chat_template_kwargs) and ollama (reasoning_effort). | ||
| 82 | + A request that already carries the field keeps it (rule already in | ||
| 83 | + `withoutReasoning`). Optional: `/think on|off` to switch during a session. | ||
| 84 | +tasks: | ||
| 85 | + - id: 1 | ||
| 86 | + title: "Decide: key name (`thinking`?), env override, and whether `/think on|off` is wanted" | ||
| 87 | + state: open | ||
| 88 | + priority: none | ||
| 89 | + author: | ||
| 90 | + name: k33g | ||
| 91 | + email: ph.charriere@gmail.com | ||
| 92 | + createdAt: 2026-10-06T13:00:00.000Z | ||
| 93 | + updatedAt: 2026-10-06T13:00:00.000Z | ||
| 94 | + - id: 2 | ||
| 95 | + title: "Failing tests first: with thinking off, every request body carries the provider's field (template / effort); unknown provider = unchanged" | ||
| 96 | + state: open | ||
| 97 | + priority: none | ||
| 98 | + author: | ||
| 99 | + name: k33g | ||
| 100 | + email: ph.charriere@gmail.com | ||
| 101 | + createdAt: 2026-10-06T13:00:00.000Z | ||
| 102 | + updatedAt: 2026-10-06T13:00:00.000Z | ||
| 103 | + - id: 3 | ||
| 104 | + title: "Config key + AGENT_THINKING; engine: mark every request (not only summaries) with noThinkKey when off" | ||
| 105 | + state: open | ||
| 106 | + priority: none | ||
| 107 | + author: | ||
| 108 | + name: k33g | ||
| 109 | + email: ph.charriere@gmail.com | ||
| 110 | + createdAt: 2026-10-06T13:00:00.000Z | ||
| 111 | + updatedAt: 2026-10-06T13:00:00.000Z | ||
| 112 | + - id: 4 | ||
| 113 | + title: "Banner shows `thinking: off`; `showThinking` stays a display setting" | ||
| 114 | + state: open | ||
| 115 | + priority: none | ||
| 116 | + author: | ||
| 117 | + name: k33g | ||
| 118 | + email: ph.charriere@gmail.com | ||
| 119 | + createdAt: 2026-10-06T13:00:00.000Z | ||
| 120 | + updatedAt: 2026-10-06T13:00:00.000Z | ||
| 121 | + - id: 5 | ||
| 122 | + title: "Live check per provider: DMR + llmman (chat_template_kwargs), Ollama (reasoning_effort none on a normal turn)" | ||
| 123 | + state: open | ||
| 124 | + priority: none | ||
| 125 | + author: | ||
| 126 | + name: k33g | ||
| 127 | + email: ph.charriere@gmail.com | ||
| 128 | + createdAt: 2026-10-06T13:00:00.000Z | ||
| 129 | + updatedAt: 2026-10-06T13:00:00.000Z | ||
| 130 | + - id: 6 | ||
| 131 | + title: "Docs EN+FR: reference configuration, how-to, explanation/thinking-models (why sampling cannot do it)" | ||
| 132 | + state: open | ||
| 133 | + priority: none | ||
| 134 | + author: | ||
| 135 | + name: k33g | ||
| 136 | + email: ph.charriere@gmail.com | ||
| 137 | + createdAt: 2026-10-06T13:00:00.000Z | ||
| 138 | + updatedAt: 2026-10-06T13:00:00.000Z | ||
| 139 | +comments: | ||
| 140 | + - id: 1 | ||
| 141 | + author: | ||
| 142 | + name: claude | ||
| 143 | + email: noreply@anthropic.com | ||
| 144 | + createdAt: 2026-10-06T14:00:00.000Z | ||
| 145 | + body: | | ||
| 146 | + **Correction: a newer Genkit fixes the "sampling" road (verified in a | ||
| 147 | + scratch copy, the repo is unchanged — still genkit v1.10.0).** | ||
| 148 | + | ||
| 149 | + Latest Genkit Go is v1.13.1 (2026-09-03). Since **v1.12.0**, | ||
| 150 | + compat_oai's `WithConfig` keeps the keys `ChatCompletionNewParams` | ||
| 151 | + does not know and sends them as JSON extras (`SetExtraFields`, | ||
| 152 | + plugins/compat_oai/generate.go ~l.231: "the OpenAI SDK silently | ||
| 153 | + ignores provider-specific fields while unmarshaling, so retain them | ||
| 154 | + as JSON extras"). v1.11.0 does not have it. | ||
| 155 | + | ||
| 156 | + BUT bumping alone is not enough — found by testing: | ||
| 157 | + 1. In v1.13.1 `OpenAICompatible.DefineModel` is DEPRECATED and | ||
| 158 | + returns an UNREGISTERED model. mm discards its result | ||
| 159 | + (internal/engine/openai_compat.go:189), so the model actually used | ||
| 160 | + is the one the plugin resolves on the fly (`ResolveAction` -> | ||
| 161 | + `newSDKModel`), whose config schema is the strict reflected SDK | ||
| 162 | + schema: `chat_template_kwargs` is then REJECTED at the action | ||
| 163 | + boundary — "config: Additional property chat_template_kwargs is | ||
| 164 | + not allowed" (and every unknown sampling key would fail the | ||
| 165 | + request, where v1.10.0 dropped it silently). | ||
| 166 | + `ModelOptions.ConfigSchema` does not help: it is set on the model | ||
| 167 | + nobody registered. | ||
| 168 | + 2. Fix: register it — `genkit.RegisterAction(g, | ||
| 169 | + client.DefineModel(...).(*ai.ModelAction))`. DefineModel's model | ||
| 170 | + takes its config untyped, no validation, unknown keys ride to the | ||
| 171 | + wire. (The non-deprecated `NewModel` validates against the strict | ||
| 172 | + schema; it would need an explicit permissive `ConfigSchema`.) | ||
| 173 | + | ||
| 174 | + Measured (recording server): v1.13.1 + RegisterAction sends | ||
| 175 | + `"chat_template_kwargs": {"enable_thinking": false}` with mm's own | ||
| 176 | + middleware fields (return_progress, stream_options) intact. Live on | ||
| 177 | + DMR qwen3.5-4b, `-p "What is 17*23?"` with | ||
| 178 | + `sampling.chat_template_kwargs.enable_thinking: false`: current mm = | ||
| 179 | + 4 s, a 💭 reasoning block, then 391; patched mm = under 1 s, no | ||
| 180 | + reasoning, 391. | ||
| 181 | + | ||
| 182 | + Upgrade side effects seen: `go get` + `go mod tidy` also bump | ||
| 183 | + dotprompt, invopop/jsonschema v0.14.0, goccy/go-yaml, jsonparser and | ||
| 184 | + add go.yaml.in/yaml/v4; `go test ./...` green (scratch copy; the | ||
| 185 | + skills test failed only because the copy had no shipped YAML); | ||
| 186 | + Genkit now logs `INFO Genkit initialized …` on stderr at start-up | ||
| 187 | + (to silence). Not re-checked after the bump: reasoning.go's tap and | ||
| 188 | + toolargs.go's workarounds (v1.13.1 reads reasoning_content itself | ||
| 189 | + now — generate.go ~l.548), the ollama/llmman paths. | ||
| 190 | + | ||
| 191 | + Consequence for the proposal: with the bump, `sampling` alone can | ||
| 192 | + carry `chat_template_kwargs` (llama.cpp family) or | ||
| 193 | + `reasoning_effort` (Ollama) — per-server spelling left to the user. | ||
| 194 | + A `thinking: false` key is still worth it for one spelling across | ||
| 195 | + providers (it would reuse `withoutReasoning`) and for `/think`. | ||
renamed
.tickets/issues/0073-mm-ask-ask-with-context-ask-another-agent-from-a.yaml +9 -9 | similarity index 94% | ||
| rename from .tickets/issues/0073-mm-ask-from-a-skill-or-a-script.yaml | ||
| rename to .tickets/issues/0073-mm-ask-ask-with-context-ask-another-agent-from-a.yaml | ||
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 73 |
| 3 | 3 | title: "mm --ask / --ask-with-context: ask another agent from a skill or a script" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T23:30:00.000Z |
| 9 | -updatedAt: 2026-10-02T23:30:00.000Z | |
| 9 | +updatedAt: 2026-10-06T03:54:22.627Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | related: |
| @@ -106,7 +106,7 @@ body: | | ||
| 106 | 106 | - English-only questions work best with these small models. |
| 107 | 107 | tasks: |
| 108 | 108 | - id: 1 |
| 109 | - title: "--ask / --ask-with-context flags; answer on stdout only; exit codes 0/1/130 (main.go runAsk, internal/ask/cli.go)" | |
| 109 | + title: --ask / --ask-with-context flags; answer on stdout only; exit codes 0/1/130 (main.go runAsk, internal/ask/cli.go) | |
| 110 | 110 | state: closed |
| 111 | 111 | priority: none |
| 112 | 112 | author: |
| @@ -115,7 +115,7 @@ tasks: | ||
| 115 | 115 | createdAt: 2026-10-02T23:30:00.000Z |
| 116 | 116 | updatedAt: 2026-10-02T23:30:00.000Z |
| 117 | 117 | - id: 2 |
| 118 | - title: "MM_BIN and MM_CONFIG exported for the bash tool; --ask falls back to MM_CONFIG" | |
| 118 | + title: MM_BIN and MM_CONFIG exported for the bash tool; --ask falls back to MM_CONFIG | |
| 119 | 119 | state: closed |
| 120 | 120 | priority: none |
| 121 | 121 | author: |
| @@ -133,7 +133,7 @@ tasks: | ||
| 133 | 133 | createdAt: 2026-10-02T23:30:00.000Z |
| 134 | 134 | updatedAt: 2026-10-02T23:30:00.000Z |
| 135 | 135 | - id: 4 |
| 136 | - title: "bashTimeout config key (default 30s)" | |
| 136 | + title: bashTimeout config key (default 30s) | |
| 137 | 137 | state: closed |
| 138 | 138 | priority: none |
| 139 | 139 | author: |
| @@ -178,7 +178,7 @@ tasks: | ||
| 178 | 178 | createdAt: 2026-10-02T23:30:00.000Z |
| 179 | 179 | updatedAt: 2026-10-02T23:30:00.000Z |
| 180 | 180 | - id: 9 |
| 181 | - title: "Live-test compare-agents" | |
| 181 | + title: Live-test compare-agents | |
| 182 | 182 | state: open |
| 183 | 183 | priority: none |
| 184 | 184 | author: |
| @@ -187,7 +187,7 @@ tasks: | ||
| 187 | 187 | createdAt: 2026-10-02T23:30:00.000Z |
| 188 | 188 | updatedAt: 2026-10-02T23:30:00.000Z |
| 189 | 189 | - id: 10 |
| 190 | - title: "Option to forbid the asked agent's tools from the command line" | |
| 190 | + title: Option to forbid the asked agent's tools from the command line | |
| 191 | 191 | state: open |
| 192 | 192 | priority: none |
| 193 | 193 | author: |
| @@ -196,7 +196,7 @@ tasks: | ||
| 196 | 196 | createdAt: 2026-10-02T23:30:00.000Z |
| 197 | 197 | updatedAt: 2026-10-02T23:30:00.000Z |
| 198 | 198 | - id: 11 |
| 199 | - title: "Quality gate (qlty) where available" | |
| 199 | + title: Quality gate (qlty) where available | |
| 200 | 200 | state: open |
| 201 | 201 | priority: none |
| 202 | 202 | author: |
| @@ -205,7 +205,7 @@ tasks: | ||
| 205 | 205 | createdAt: 2026-10-02T23:30:00.000Z |
| 206 | 206 | updatedAt: 2026-10-02T23:30:00.000Z |
| 207 | 207 | - id: 12 |
| 208 | - title: "Review, commit, merge" | |
| 208 | + title: Review, commit, merge | |
| 209 | 209 | state: open |
| 210 | 210 | priority: none |
| 211 | 211 | author: |
| similarity index 94% | |||
| rename from .tickets/issues/0073-mm-ask-from-a-skill-or-a-script.yaml | |||
| rename to .tickets/issues/0073-mm-ask-ask-with-context-ask-another-agent-from-a.yaml | |||
| @@ -1,12 +1,12 @@ | |||
| 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: 73 | 2 | id: 73 |
| 3 | title: "mm --ask / --ask-with-context: ask another agent from a skill or a script" | 3 | title: "mm --ask / --ask-with-context: ask another agent from a skill or a script" |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T23:30:00.000Z | 8 | createdAt: 2026-10-02T23:30:00.000Z |
| 9 | -updatedAt: 2026-10-02T23:30:00.000Z | 9 | +updatedAt: 2026-10-06T03:54:22.627Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | related: | 12 | related: |
| @@ -106,7 +106,7 @@ body: | | |||
| 106 | - English-only questions work best with these small models. | 106 | - English-only questions work best with these small models. |
| 107 | tasks: | 107 | tasks: |
| 108 | - id: 1 | 108 | - id: 1 |
| 109 | - title: "--ask / --ask-with-context flags; answer on stdout only; exit codes 0/1/130 (main.go runAsk, internal/ask/cli.go)" | 109 | + title: --ask / --ask-with-context flags; answer on stdout only; exit codes 0/1/130 (main.go runAsk, internal/ask/cli.go) |
| 110 | state: closed | 110 | state: closed |
| 111 | priority: none | 111 | priority: none |
| 112 | author: | 112 | author: |
| @@ -115,7 +115,7 @@ tasks: | |||
| 115 | createdAt: 2026-10-02T23:30:00.000Z | 115 | createdAt: 2026-10-02T23:30:00.000Z |
| 116 | updatedAt: 2026-10-02T23:30:00.000Z | 116 | updatedAt: 2026-10-02T23:30:00.000Z |
| 117 | - id: 2 | 117 | - id: 2 |
| 118 | - title: "MM_BIN and MM_CONFIG exported for the bash tool; --ask falls back to MM_CONFIG" | 118 | + title: MM_BIN and MM_CONFIG exported for the bash tool; --ask falls back to MM_CONFIG |
| 119 | state: closed | 119 | state: closed |
| 120 | priority: none | 120 | priority: none |
| 121 | author: | 121 | author: |
| @@ -133,7 +133,7 @@ tasks: | |||
| 133 | createdAt: 2026-10-02T23:30:00.000Z | 133 | createdAt: 2026-10-02T23:30:00.000Z |
| 134 | updatedAt: 2026-10-02T23:30:00.000Z | 134 | updatedAt: 2026-10-02T23:30:00.000Z |
| 135 | - id: 4 | 135 | - id: 4 |
| 136 | - title: "bashTimeout config key (default 30s)" | 136 | + title: bashTimeout config key (default 30s) |
| 137 | state: closed | 137 | state: closed |
| 138 | priority: none | 138 | priority: none |
| 139 | author: | 139 | author: |
| @@ -178,7 +178,7 @@ tasks: | |||
| 178 | createdAt: 2026-10-02T23:30:00.000Z | 178 | createdAt: 2026-10-02T23:30:00.000Z |
| 179 | updatedAt: 2026-10-02T23:30:00.000Z | 179 | updatedAt: 2026-10-02T23:30:00.000Z |
| 180 | - id: 9 | 180 | - id: 9 |
| 181 | - title: "Live-test compare-agents" | 181 | + title: Live-test compare-agents |
| 182 | state: open | 182 | state: open |
| 183 | priority: none | 183 | priority: none |
| 184 | author: | 184 | author: |
| @@ -187,7 +187,7 @@ tasks: | |||
| 187 | createdAt: 2026-10-02T23:30:00.000Z | 187 | createdAt: 2026-10-02T23:30:00.000Z |
| 188 | updatedAt: 2026-10-02T23:30:00.000Z | 188 | updatedAt: 2026-10-02T23:30:00.000Z |
| 189 | - id: 10 | 189 | - id: 10 |
| 190 | - title: "Option to forbid the asked agent's tools from the command line" | 190 | + title: Option to forbid the asked agent's tools from the command line |
| 191 | state: open | 191 | state: open |
| 192 | priority: none | 192 | priority: none |
| 193 | author: | 193 | author: |
| @@ -196,7 +196,7 @@ tasks: | |||
| 196 | createdAt: 2026-10-02T23:30:00.000Z | 196 | createdAt: 2026-10-02T23:30:00.000Z |
| 197 | updatedAt: 2026-10-02T23:30:00.000Z | 197 | updatedAt: 2026-10-02T23:30:00.000Z |
| 198 | - id: 11 | 198 | - id: 11 |
| 199 | - title: "Quality gate (qlty) where available" | 199 | + title: Quality gate (qlty) where available |
| 200 | state: open | 200 | state: open |
| 201 | priority: none | 201 | priority: none |
| 202 | author: | 202 | author: |
| @@ -205,7 +205,7 @@ tasks: | |||
| 205 | createdAt: 2026-10-02T23:30:00.000Z | 205 | createdAt: 2026-10-02T23:30:00.000Z |
| 206 | updatedAt: 2026-10-02T23:30:00.000Z | 206 | updatedAt: 2026-10-02T23:30:00.000Z |
| 207 | - id: 12 | 207 | - id: 12 |
| 208 | - title: "Review, commit, merge" | 208 | + title: Review, commit, merge |
| 209 | state: open | 209 | state: open |
| 210 | priority: none | 210 | priority: none |
| 211 | author: | 211 | author: |
modified
.tickets/issues/0074-memory-memgpt-hierarchic-memory.yaml +2 -1 | @@ -6,9 +6,10 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-04T15:12:36.643Z |
| 9 | -updatedAt: 2026-10-04T15:12:50.958Z | |
| 9 | +updatedAt: 2026-10-06T03:59:13.083Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::high |
| 12 | +epic: Agent Memory | |
| 12 | 13 | body: "" |
| 13 | 14 | tasks: [] |
| 14 | 15 | comments: [] |
| @@ -6,9 +6,10 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-04T15:12:36.643Z | 8 | createdAt: 2026-10-04T15:12:36.643Z |
| 9 | -updatedAt: 2026-10-04T15:12:50.958Z | 9 | +updatedAt: 2026-10-06T03:59:13.083Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::high | 11 | - priority::high |
| 12 | +epic: Agent Memory | ||
| 12 | body: "" | 13 | body: "" |
| 13 | tasks: [] | 14 | tasks: [] |
| 14 | comments: [] | 15 | comments: [] |
added
.tickets/issues/0075-add-gronk-provider.yaml +14 -0 | new file mode 100644 | ||
| @@ -0,0 +1,14 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 75 | |
| 3 | +title: Add Gronk provider | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-06T03:58:37.611Z | |
| 9 | +updatedAt: 2026-10-06T04:00:38.851Z | |
| 10 | +labels: [] | |
| 11 | +epic: Providers (LLM Engine) | |
| 12 | +body: "" | |
| 13 | +tasks: [] | |
| 14 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 75 | ||
| 3 | +title: Add Gronk provider | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-06T03:58:37.611Z | ||
| 9 | +updatedAt: 2026-10-06T04:00:38.851Z | ||
| 10 | +labels: [] | ||
| 11 | +epic: Providers (LLM Engine) | ||
| 12 | +body: "" | ||
| 13 | +tasks: [] | ||
| 14 | +comments: [] | ||
modified
README.md +1 -1 | @@ -55,7 +55,7 @@ Type a question at the `>` prompt. Each command the model runs is shown with its | ||
| 55 | 55 | mm -tui |
| 56 | 56 | ``` |
| 57 | 57 | |
| 58 | -The conversation above, a box to type in below. **Enter** sends, **Ctrl-J** adds a line, **Esc** cancels a turn, **F1** lists the keys. With `permissions: ask` in the YAML, the agent asks before running a command or writing a file. | |
| 58 | +The conversation above, a box to type in below. **Enter** sends, **Ctrl-J** adds a line, **Esc** cancels a turn, **F1** lists the keys. With `yolo: false` in the YAML, the agent asks before running a command or writing a file — in the window, the line mode and an editor alike (`yolo: true`, the default, runs them unasked). | |
| 59 | 59 | |
| 60 | 60 | ### 3. Inside an editor (ACP) |
| 61 | 61 | |
| @@ -55,7 +55,7 @@ Type a question at the `>` prompt. Each command the model runs is shown with its | |||
| 55 | mm -tui | 55 | mm -tui |
| 56 | ``` | 56 | ``` |
| 57 | 57 | ||
| 58 | -The conversation above, a box to type in below. **Enter** sends, **Ctrl-J** adds a line, **Esc** cancels a turn, **F1** lists the keys. With `permissions: ask` in the YAML, the agent asks before running a command or writing a file. | 58 | +The conversation above, a box to type in below. **Enter** sends, **Ctrl-J** adds a line, **Esc** cancels a turn, **F1** lists the keys. With `yolo: false` in the YAML, the agent asks before running a command or writing a file — in the window, the line mode and an editor alike (`yolo: true`, the default, runs them unasked). |
| 59 | 59 | ||
| 60 | ### 3. Inside an editor (ACP) | 60 | ### 3. Inside an editor (ACP) |
| 61 | 61 | ||
modified
agent.dmr.yaml +8 -0 | @@ -19,6 +19,14 @@ maxOutput: 16000 | ||
| 19 | 19 | # Raise it for tasks that need long chains of commands. |
| 20 | 20 | maxTurns: 40 |
| 21 | 21 | |
| 22 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 23 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 24 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 25 | +# terminal, the window and the editor; MCP tools ask too. | |
| 26 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 27 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 28 | +yolo: true | |
| 29 | + | |
| 22 | 30 | # The `bash` tool. |
| 23 | 31 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 24 | 32 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -19,6 +19,14 @@ maxOutput: 16000 | |||
| 19 | # Raise it for tasks that need long chains of commands. | 19 | # Raise it for tasks that need long chains of commands. |
| 20 | maxTurns: 40 | 20 | maxTurns: 40 |
| 21 | 21 | ||
| 22 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 23 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 24 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 25 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 26 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 27 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 28 | +yolo: true | ||
| 29 | + | ||
| 22 | # The `bash` tool. | 30 | # The `bash` tool. |
| 23 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 31 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 24 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 32 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
agent.llamacpp.yaml +8 -0 | @@ -15,6 +15,14 @@ fallback: "" | ||
| 15 | 15 | # Only when llama-server was started with --api-key; unset otherwise. |
| 16 | 16 | # apiKeyEnv: LLAMA_API_KEY |
| 17 | 17 | |
| 18 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 19 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 20 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 21 | +# terminal, the window and the editor; MCP tools ask too. | |
| 22 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 23 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 24 | +yolo: true | |
| 25 | + | |
| 18 | 26 | # The `bash` tool. |
| 19 | 27 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 20 | 28 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -15,6 +15,14 @@ fallback: "" | |||
| 15 | # Only when llama-server was started with --api-key; unset otherwise. | 15 | # Only when llama-server was started with --api-key; unset otherwise. |
| 16 | # apiKeyEnv: LLAMA_API_KEY | 16 | # apiKeyEnv: LLAMA_API_KEY |
| 17 | 17 | ||
| 18 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 19 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 20 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 21 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 22 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 23 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 24 | +yolo: true | ||
| 25 | + | ||
| 18 | # The `bash` tool. | 26 | # The `bash` tool. |
| 19 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 27 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 20 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 28 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
agent.llmman.yaml +8 -0 | @@ -22,6 +22,14 @@ fallback: http://host.docker.internal:17434/v1 | ||
| 22 | 22 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 23 | 23 | # apiKeyEnv: LLMMAN_API_KEY |
| 24 | 24 | |
| 25 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 26 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 27 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 28 | +# terminal, the window and the editor; MCP tools ask too. | |
| 29 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 30 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 31 | +yolo: true | |
| 32 | + | |
| 25 | 33 | # The `bash` tool. |
| 26 | 34 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 27 | 35 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -22,6 +22,14 @@ fallback: http://host.docker.internal:17434/v1 | |||
| 22 | # listens beyond loopback); unset otherwise. The key never goes in this file. | 22 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 23 | # apiKeyEnv: LLMMAN_API_KEY | 23 | # apiKeyEnv: LLMMAN_API_KEY |
| 24 | 24 | ||
| 25 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 26 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 27 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 28 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 29 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 30 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 31 | +yolo: true | ||
| 32 | + | ||
| 25 | # The `bash` tool. | 33 | # The `bash` tool. |
| 26 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 34 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 27 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 35 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
agent.lsp.yaml +8 -0 | @@ -26,6 +26,14 @@ maxOutput: 16000 | ||
| 26 | 26 | # Raise it for tasks that need long chains of commands. |
| 27 | 27 | maxTurns: 40 |
| 28 | 28 | |
| 29 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 30 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 31 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 32 | +# terminal, the window and the editor; MCP tools ask too. | |
| 33 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 34 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 35 | +yolo: true | |
| 36 | + | |
| 29 | 37 | # The `bash` tool. |
| 30 | 38 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 31 | 39 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -26,6 +26,14 @@ maxOutput: 16000 | |||
| 26 | # Raise it for tasks that need long chains of commands. | 26 | # Raise it for tasks that need long chains of commands. |
| 27 | maxTurns: 40 | 27 | maxTurns: 40 |
| 28 | 28 | ||
| 29 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 30 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 31 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 32 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 33 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 34 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 35 | +yolo: true | ||
| 36 | + | ||
| 29 | # The `bash` tool. | 37 | # The `bash` tool. |
| 30 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 38 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 31 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 39 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
agent.mcp-example.yaml +5 -0 | @@ -4,6 +4,11 @@ | ||
| 4 | 4 | provider: dmr |
| 5 | 5 | model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M |
| 6 | 6 | |
| 7 | +# Yolo mode. `true` (default): the built-in tools run without asking, and each MCP tool | |
| 8 | +# follows mcp.permissions below. `false`: every tool call waits for your answer — | |
| 9 | +# [a] once, [s] for the session, [r] reject, [n] never — MCP tools included, even "auto". | |
| 10 | +yolo: true | |
| 11 | + | |
| 7 | 12 | # Enable MCP client |
| 8 | 13 | mcp: |
| 9 | 14 | enabled: true |
| @@ -4,6 +4,11 @@ | |||
| 4 | provider: dmr | 4 | provider: dmr |
| 5 | model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | 5 | model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M |
| 6 | 6 | ||
| 7 | +# Yolo mode. `true` (default): the built-in tools run without asking, and each MCP tool | ||
| 8 | +# follows mcp.permissions below. `false`: every tool call waits for your answer — | ||
| 9 | +# [a] once, [s] for the session, [r] reject, [n] never — MCP tools included, even "auto". | ||
| 10 | +yolo: true | ||
| 11 | + | ||
| 7 | # Enable MCP client | 12 | # Enable MCP client |
| 8 | mcp: | 13 | mcp: |
| 9 | enabled: true | 14 | enabled: true |
modified
config.samples/agent.dmr.gemma.yaml +8 -0 | @@ -12,6 +12,14 @@ fallback: http://host.docker.internal:12434/engines/v1 | ||
| 12 | 12 | # Only when llama-server was started with --api-key; unset otherwise. |
| 13 | 13 | # apiKeyEnv: LLAMA_API_KEY |
| 14 | 14 | |
| 15 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 16 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 17 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 18 | +# terminal, the window and the editor; MCP tools ask too. | |
| 19 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 20 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 21 | +yolo: true | |
| 22 | + | |
| 15 | 23 | # The `bash` tool. |
| 16 | 24 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 17 | 25 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -12,6 +12,14 @@ fallback: http://host.docker.internal:12434/engines/v1 | |||
| 12 | # Only when llama-server was started with --api-key; unset otherwise. | 12 | # Only when llama-server was started with --api-key; unset otherwise. |
| 13 | # apiKeyEnv: LLAMA_API_KEY | 13 | # apiKeyEnv: LLAMA_API_KEY |
| 14 | 14 | ||
| 15 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 16 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 17 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 18 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 19 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 20 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 21 | +yolo: true | ||
| 22 | + | ||
| 15 | # The `bash` tool. | 23 | # The `bash` tool. |
| 16 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 24 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 17 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 25 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
config.samples/agent.dmr.gpt-oss.yaml +8 -0 | @@ -13,6 +13,14 @@ fallback: http://host.docker.internal:12434/engines/v1 | ||
| 13 | 13 | # Only when llama-server was started with --api-key; unset otherwise. |
| 14 | 14 | # apiKeyEnv: LLAMA_API_KEY |
| 15 | 15 | |
| 16 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 17 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 18 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 19 | +# terminal, the window and the editor; MCP tools ask too. | |
| 20 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 21 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 22 | +yolo: true | |
| 23 | + | |
| 16 | 24 | # The `bash` tool. |
| 17 | 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 18 | 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -13,6 +13,14 @@ fallback: http://host.docker.internal:12434/engines/v1 | |||
| 13 | # Only when llama-server was started with --api-key; unset otherwise. | 13 | # Only when llama-server was started with --api-key; unset otherwise. |
| 14 | # apiKeyEnv: LLAMA_API_KEY | 14 | # apiKeyEnv: LLAMA_API_KEY |
| 15 | 15 | ||
| 16 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 17 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 18 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 19 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 20 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 21 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 22 | +yolo: true | ||
| 23 | + | ||
| 16 | # The `bash` tool. | 24 | # The `bash` tool. |
| 17 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 18 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
config.samples/agent.dmr.qwen3.6.yaml +8 -0 | @@ -15,6 +15,14 @@ fallback: http://host.docker.internal:12434/engines/v1 | ||
| 15 | 15 | # Only when llama-server was started with --api-key; unset otherwise. |
| 16 | 16 | # apiKeyEnv: LLAMA_API_KEY |
| 17 | 17 | |
| 18 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 19 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 20 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 21 | +# terminal, the window and the editor; MCP tools ask too. | |
| 22 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 23 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 24 | +yolo: true | |
| 25 | + | |
| 18 | 26 | # The `bash` tool. |
| 19 | 27 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 20 | 28 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -15,6 +15,14 @@ fallback: http://host.docker.internal:12434/engines/v1 | |||
| 15 | # Only when llama-server was started with --api-key; unset otherwise. | 15 | # Only when llama-server was started with --api-key; unset otherwise. |
| 16 | # apiKeyEnv: LLAMA_API_KEY | 16 | # apiKeyEnv: LLAMA_API_KEY |
| 17 | 17 | ||
| 18 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 19 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 20 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 21 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 22 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 23 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 24 | +yolo: true | ||
| 25 | + | ||
| 18 | # The `bash` tool. | 26 | # The `bash` tool. |
| 19 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 27 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 20 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 28 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
config.samples/agent.llamacpp.ornith.yaml +8 -0 | @@ -18,6 +18,14 @@ fallback: "" | ||
| 18 | 18 | # Only when llama-server was started with --api-key; unset otherwise. |
| 19 | 19 | # apiKeyEnv: LLAMA_API_KEY |
| 20 | 20 | |
| 21 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 22 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 23 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 24 | +# terminal, the window and the editor; MCP tools ask too. | |
| 25 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 26 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 27 | +yolo: true | |
| 28 | + | |
| 21 | 29 | # The `bash` tool. |
| 22 | 30 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 23 | 31 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -18,6 +18,14 @@ fallback: "" | |||
| 18 | # Only when llama-server was started with --api-key; unset otherwise. | 18 | # Only when llama-server was started with --api-key; unset otherwise. |
| 19 | # apiKeyEnv: LLAMA_API_KEY | 19 | # apiKeyEnv: LLAMA_API_KEY |
| 20 | 20 | ||
| 21 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 22 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 23 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 24 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 25 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 26 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 27 | +yolo: true | ||
| 28 | + | ||
| 21 | # The `bash` tool. | 29 | # The `bash` tool. |
| 22 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 30 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 23 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 31 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
config.samples/agent.llamacpp.qwen3.8.yaml +8 -0 | @@ -18,6 +18,14 @@ fallback: "" | ||
| 18 | 18 | # Only when llama-server was started with --api-key; unset otherwise. |
| 19 | 19 | # apiKeyEnv: LLAMA_API_KEY |
| 20 | 20 | |
| 21 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 22 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 23 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 24 | +# terminal, the window and the editor; MCP tools ask too. | |
| 25 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 26 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 27 | +yolo: true | |
| 28 | + | |
| 21 | 29 | # The `bash` tool. |
| 22 | 30 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 23 | 31 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -18,6 +18,14 @@ fallback: "" | |||
| 18 | # Only when llama-server was started with --api-key; unset otherwise. | 18 | # Only when llama-server was started with --api-key; unset otherwise. |
| 19 | # apiKeyEnv: LLAMA_API_KEY | 19 | # apiKeyEnv: LLAMA_API_KEY |
| 20 | 20 | ||
| 21 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 22 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 23 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 24 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 25 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 26 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 27 | +yolo: true | ||
| 28 | + | ||
| 21 | # The `bash` tool. | 29 | # The `bash` tool. |
| 22 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 30 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 23 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 31 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo-llmman/agent.jan-nano.yaml +8 -0 | @@ -21,6 +21,14 @@ fallback: http://host.docker.internal:17434/v1 | ||
| 21 | 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 22 | 22 | # apiKeyEnv: LLMMAN_API_KEY |
| 23 | 23 | |
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 27 | +# terminal, the window and the editor; MCP tools ask too. | |
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 30 | +yolo: true | |
| 31 | + | |
| 24 | 32 | # The `bash` tool. |
| 25 | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -21,6 +21,14 @@ fallback: http://host.docker.internal:17434/v1 | |||
| 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. | 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 22 | # apiKeyEnv: LLMMAN_API_KEY | 22 | # apiKeyEnv: LLMMAN_API_KEY |
| 23 | 23 | ||
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 27 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 30 | +yolo: true | ||
| 31 | + | ||
| 24 | # The `bash` tool. | 32 | # The `bash` tool. |
| 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo-llmman/agent.llmman.yaml +8 -0 | @@ -34,6 +34,14 @@ mmConfigs: | ||
| 34 | 34 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 35 | 35 | # apiKeyEnv: LLMMAN_API_KEY |
| 36 | 36 | |
| 37 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 38 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 39 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 40 | +# terminal, the window and the editor; MCP tools ask too. | |
| 41 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 42 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 43 | +yolo: false | |
| 44 | + | |
| 37 | 45 | # The `bash` tool. |
| 38 | 46 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 39 | 47 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -34,6 +34,14 @@ mmConfigs: | |||
| 34 | # listens beyond loopback); unset otherwise. The key never goes in this file. | 34 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 35 | # apiKeyEnv: LLMMAN_API_KEY | 35 | # apiKeyEnv: LLMMAN_API_KEY |
| 36 | 36 | ||
| 37 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 38 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 39 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 40 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 41 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 42 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 43 | +yolo: false | ||
| 44 | + | ||
| 37 | # The `bash` tool. | 45 | # The `bash` tool. |
| 38 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 46 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 39 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 47 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo-llmman/agent.lucy-reviewer.yaml +8 -0 | @@ -21,6 +21,14 @@ fallback: http://host.docker.internal:17434/v1 | ||
| 21 | 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 22 | 22 | # apiKeyEnv: LLMMAN_API_KEY |
| 23 | 23 | |
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 27 | +# terminal, the window and the editor; MCP tools ask too. | |
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 30 | +yolo: true | |
| 31 | + | |
| 24 | 32 | # The `bash` tool. |
| 25 | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -21,6 +21,14 @@ fallback: http://host.docker.internal:17434/v1 | |||
| 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. | 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 22 | # apiKeyEnv: LLMMAN_API_KEY | 22 | # apiKeyEnv: LLMMAN_API_KEY |
| 23 | 23 | ||
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 27 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 30 | +yolo: true | ||
| 31 | + | ||
| 24 | # The `bash` tool. | 32 | # The `bash` tool. |
| 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo-llmman/agent.lucy.yaml +8 -0 | @@ -21,6 +21,14 @@ fallback: http://host.docker.internal:17434/v1 | ||
| 21 | 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 22 | 22 | # apiKeyEnv: LLMMAN_API_KEY |
| 23 | 23 | |
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 27 | +# terminal, the window and the editor; MCP tools ask too. | |
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 30 | +yolo: true | |
| 31 | + | |
| 24 | 32 | # The `bash` tool. |
| 25 | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -21,6 +21,14 @@ fallback: http://host.docker.internal:17434/v1 | |||
| 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. | 21 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| 22 | # apiKeyEnv: LLMMAN_API_KEY | 22 | # apiKeyEnv: LLMMAN_API_KEY |
| 23 | 23 | ||
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 27 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 30 | +yolo: true | ||
| 31 | + | ||
| 24 | # The `bash` tool. | 32 | # The `bash` tool. |
| 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo-ollama/agent.ollama.yaml +8 -0 | @@ -22,6 +22,14 @@ fallback: http://host.docker.internal:11434/v1 | ||
| 22 | 22 | # never put it here): |
| 23 | 23 | # apiKeyEnv: OLLAMA_API_KEY |
| 24 | 24 | |
| 25 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 26 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 27 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 28 | +# terminal, the window and the editor; MCP tools ask too. | |
| 29 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 30 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 31 | +yolo: true | |
| 32 | + | |
| 25 | 33 | # The `bash` tool. |
| 26 | 34 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 27 | 35 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -22,6 +22,14 @@ fallback: http://host.docker.internal:11434/v1 | |||
| 22 | # never put it here): | 22 | # never put it here): |
| 23 | # apiKeyEnv: OLLAMA_API_KEY | 23 | # apiKeyEnv: OLLAMA_API_KEY |
| 24 | 24 | ||
| 25 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 26 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 27 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 28 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 29 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 30 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 31 | +yolo: true | ||
| 32 | + | ||
| 25 | # The `bash` tool. | 33 | # The `bash` tool. |
| 26 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 34 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 27 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 35 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo/agent.dmr.2.yaml +8 -0 | @@ -21,6 +21,14 @@ maxOutput: 16000 | ||
| 21 | 21 | # Raise it for tasks that need long chains of commands. |
| 22 | 22 | maxTurns: 40 |
| 23 | 23 | |
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 27 | +# terminal, the window and the editor; MCP tools ask too. | |
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 30 | +yolo: true | |
| 31 | + | |
| 24 | 32 | # The `bash` tool. |
| 25 | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -21,6 +21,14 @@ maxOutput: 16000 | |||
| 21 | # Raise it for tasks that need long chains of commands. | 21 | # Raise it for tasks that need long chains of commands. |
| 22 | maxTurns: 40 | 22 | maxTurns: 40 |
| 23 | 23 | ||
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 27 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 30 | +yolo: true | ||
| 31 | + | ||
| 24 | # The `bash` tool. | 32 | # The `bash` tool. |
| 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo/agent.dmr.github-oauth.yaml +8 -0 | @@ -24,6 +24,14 @@ contextWindow: 0 | ||
| 24 | 24 | maxOutput: 16000 |
| 25 | 25 | maxTurns: 40 |
| 26 | 26 | |
| 27 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 28 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 29 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 30 | +# terminal, the window and the editor; MCP tools ask too. | |
| 31 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 32 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 33 | +yolo: true | |
| 34 | + | |
| 27 | 35 | bashTool: true |
| 28 | 36 | editTools: true |
| 29 | 37 | planTool: true |
| @@ -24,6 +24,14 @@ contextWindow: 0 | |||
| 24 | maxOutput: 16000 | 24 | maxOutput: 16000 |
| 25 | maxTurns: 40 | 25 | maxTurns: 40 |
| 26 | 26 | ||
| 27 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 28 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 29 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 30 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 31 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 32 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 33 | +yolo: true | ||
| 34 | + | ||
| 27 | bashTool: true | 35 | bashTool: true |
| 28 | editTools: true | 36 | editTools: true |
| 29 | planTool: true | 37 | planTool: true |
modified
demo/agent.dmr.mcp.yaml +8 -0 | @@ -22,6 +22,14 @@ maxOutput: 16000 | ||
| 22 | 22 | # Raise it for tasks that need long chains of commands. |
| 23 | 23 | maxTurns: 40 |
| 24 | 24 | |
| 25 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 26 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 27 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 28 | +# terminal, the window and the editor; MCP tools ask too. | |
| 29 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 30 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 31 | +yolo: true | |
| 32 | + | |
| 25 | 33 | # The `bash` tool. |
| 26 | 34 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 27 | 35 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -22,6 +22,14 @@ maxOutput: 16000 | |||
| 22 | # Raise it for tasks that need long chains of commands. | 22 | # Raise it for tasks that need long chains of commands. |
| 23 | maxTurns: 40 | 23 | maxTurns: 40 |
| 24 | 24 | ||
| 25 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 26 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 27 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 28 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 29 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 30 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 31 | +yolo: true | ||
| 32 | + | ||
| 25 | # The `bash` tool. | 33 | # The `bash` tool. |
| 26 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 34 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 27 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 35 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo/agent.dmr.sbx.yaml +8 -0 | @@ -26,6 +26,14 @@ contextWindow: 0 | ||
| 26 | 26 | maxOutput: 16000 |
| 27 | 27 | maxTurns: 40 |
| 28 | 28 | |
| 29 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 30 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 31 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 32 | +# terminal, the window and the editor; MCP tools ask too. | |
| 33 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 34 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 35 | +yolo: true | |
| 36 | + | |
| 29 | 37 | bashTool: true |
| 30 | 38 | editTools: true |
| 31 | 39 | planTool: true |
| @@ -26,6 +26,14 @@ contextWindow: 0 | |||
| 26 | maxOutput: 16000 | 26 | maxOutput: 16000 |
| 27 | maxTurns: 40 | 27 | maxTurns: 40 |
| 28 | 28 | ||
| 29 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 30 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 31 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 32 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 33 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 34 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 35 | +yolo: true | ||
| 36 | + | ||
| 29 | bashTool: true | 37 | bashTool: true |
| 30 | editTools: true | 38 | editTools: true |
| 31 | planTool: true | 39 | planTool: true |
modified
demo/agent.dmr.yaml +8 -0 | @@ -21,6 +21,14 @@ maxOutput: 16000 | ||
| 21 | 21 | # Raise it for tasks that need long chains of commands. |
| 22 | 22 | maxTurns: 40 |
| 23 | 23 | |
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 27 | +# terminal, the window and the editor; MCP tools ask too. | |
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 30 | +yolo: true | |
| 31 | + | |
| 24 | 32 | # The `bash` tool. |
| 25 | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -21,6 +21,14 @@ maxOutput: 16000 | |||
| 21 | # Raise it for tasks that need long chains of commands. | 21 | # Raise it for tasks that need long chains of commands. |
| 22 | maxTurns: 40 | 22 | maxTurns: 40 |
| 23 | 23 | ||
| 24 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 25 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 26 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 27 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 28 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 29 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 30 | +yolo: true | ||
| 31 | + | ||
| 24 | # The `bash` tool. | 32 | # The `bash` tool. |
| 25 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 33 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 26 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 34 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
demo/agent.lsp.yaml +8 -0 | @@ -31,6 +31,14 @@ maxOutput: 16000 | ||
| 31 | 31 | maxTurns: 200 |
| 32 | 32 | |
| 33 | 33 | |
| 34 | +# Yolo mode. `true` (default): the tools run without asking. | |
| 35 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | |
| 36 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | |
| 37 | +# terminal, the window and the editor; MCP tools ask too. | |
| 38 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | |
| 39 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | |
| 40 | +yolo: true | |
| 41 | + | |
| 34 | 42 | # The `bash` tool. |
| 35 | 43 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 36 | 44 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
| @@ -31,6 +31,14 @@ maxOutput: 16000 | |||
| 31 | maxTurns: 200 | 31 | maxTurns: 200 |
| 32 | 32 | ||
| 33 | 33 | ||
| 34 | +# Yolo mode. `true` (default): the tools run without asking. | ||
| 35 | +# `false`: every bash call, file write/edit, fetch_content save, lsp rename and plan change | ||
| 36 | +# waits for your answer — [a] once, [s] for the session, [r] reject, [n] never — in the | ||
| 37 | +# terminal, the window and the editor; MCP tools ask too. | ||
| 38 | +# bashTool / editTools still decide which tools EXIST. Called through /ask (mmConfigs), | ||
| 39 | +# an agent follows its asker's yolo, not this one. Override for one run: AGENT_YOLO=false. | ||
| 40 | +yolo: true | ||
| 41 | + | ||
| 34 | # The `bash` tool. | 42 | # The `bash` tool. |
| 35 | # `false` removes it: the model can still read and edit files (editTools) and load skills, | 43 | # `false` removes it: the model can still read and edit files (editTools) and load skills, |
| 36 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | 44 | # but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". |
modified
docs/diagrams/packages.drawio +2 -2 | @@ -1,3 +1,3 @@ | ||
| 1 | -<mxfile host="app.diagrams.net" modified="2026-09-15T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry"/></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry"/></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry"/></mxCell> | |
| 1 | +<mxfile host="app.diagrams.net" modified="2026-10-06T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry"/></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry"/></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry"/></mxCell> | |
| 2 | 2 | <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry"/></mxCell> |
| 3 | - <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry"/></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry"/></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry"/></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry"/></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry"/></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry"/></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry"/></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry"/></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry"/></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry"/></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry"/></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry"/></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile> | |
| \ No newline at end of file | ||
| 3 | + <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry"/></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry"/></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry"/></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry"/></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry"/></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry"/></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry"/></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry"/></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry"/></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry"/></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry"/></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry"/></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="consent" value="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry"/></mxCell><mxCell id="mcp" value="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry"/></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile> | |
| \ No newline at end of file | ||
| @@ -1,3 +1,3 @@ | |||
| 1 | -<mxfile host="app.diagrams.net" modified="2026-09-15T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry"/></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry"/></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry"/></mxCell> | 1 | +<mxfile host="app.diagrams.net" modified="2026-10-06T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry"/></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry"/></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry"/></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry"/></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry"/></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry"/></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry"/></mxCell> |
| 2 | <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry"/></mxCell> | 2 | <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry"/></mxCell> |
| 3 | - <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry"/></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry"/></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry"/></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry"/></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry"/></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry"/></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry"/></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry"/></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry"/></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry"/></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry"/></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry"/></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile> | ||
| \ No newline at end of file | \ No newline at end of file | ||
| 3 | + <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry"/></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry"/></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry"/></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry"/></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry"/></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry"/></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry"/></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry"/></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry"/></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry"/></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry"/></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry"/></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="consent" value="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry"/></mxCell><mxCell id="mcp" value="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry"/></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry"/></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile> | ||
| \ No newline at end of file | \ No newline at end of file | ||
modified
docs/en/README.md +2 -0 | @@ -16,6 +16,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 16 | 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` |
| 17 | 17 | - [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills |
| 18 | 18 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) |
| 19 | +- [Confirm tool calls before they run](how-to/confirm-tool-calls.md) — `yolo: false`, the four answers, `AGENT_YOLO`, who decides with `mmConfigs` | |
| 19 | 20 | - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the template and the two kits under `docker-sbx/`, window mode and editor |
| 20 | 21 | - [Run mini-me with the v3 sandbox kits](how-to/run-with-sandbox-kits-v3.md) — the same two kits for the kit specification v3, under `docker-sbx-v3/` |
| 21 | 22 | - [Add a skill](how-to/add-a-skill.md) |
| @@ -58,6 +59,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 58 | 59 | - [The `lsp` tool](explanation/lsp-tool.md) — one tool with an `action` field, a `symbol` field instead of columns, files synced from disk, rename gated by permission |
| 59 | 60 | - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits |
| 60 | 61 | - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive |
| 62 | +- [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides | |
| 61 | 63 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, the depth limit |
| 62 | 64 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 63 | 65 | |
| @@ -16,6 +16,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` | 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` |
| 17 | - [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills | 17 | - [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills |
| 18 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) | 18 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) |
| 19 | +- [Confirm tool calls before they run](how-to/confirm-tool-calls.md) — `yolo: false`, the four answers, `AGENT_YOLO`, who decides with `mmConfigs` | ||
| 19 | - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the template and the two kits under `docker-sbx/`, window mode and editor | 20 | - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the template and the two kits under `docker-sbx/`, window mode and editor |
| 20 | - [Run mini-me with the v3 sandbox kits](how-to/run-with-sandbox-kits-v3.md) — the same two kits for the kit specification v3, under `docker-sbx-v3/` | 21 | - [Run mini-me with the v3 sandbox kits](how-to/run-with-sandbox-kits-v3.md) — the same two kits for the kit specification v3, under `docker-sbx-v3/` |
| 21 | - [Add a skill](how-to/add-a-skill.md) | 22 | - [Add a skill](how-to/add-a-skill.md) |
| @@ -58,6 +59,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 58 | - [The `lsp` tool](explanation/lsp-tool.md) — one tool with an `action` field, a `symbol` field instead of columns, files synced from disk, rename gated by permission | 59 | - [The `lsp` tool](explanation/lsp-tool.md) — one tool with an `action` field, a `symbol` field instead of columns, files synced from disk, rename gated by permission |
| 59 | - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits | 60 | - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits |
| 60 | - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive | 61 | - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive |
| 62 | +- [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides | ||
| 61 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, the depth limit | 63 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, the depth limit |
| 62 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) | 64 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 63 | 65 | ||
modified
docs/en/explanation/architecture.md +2 -1 | @@ -24,6 +24,7 @@ The entry point, `main.go`, only does wiring: it loads the settings, opens the e | ||
| 24 | 24 | | `spinner` | The "still working" indicator on a terminal. | |
| 25 | 25 | | `version` | Which `mm` this is: the tag stamped at build time, or the commit Go embeds otherwise. Read by `--version`, by the start-up banner and by the window's status bar. | |
| 26 | 26 | | `ui` | Where human-facing output goes, and the event sink the front ends share. | |
| 27 | +| `consent` | Whether a tool call may run: the `yolo` switch (`Gate`), the four answers (`Answer`), what a session remembers per tool (`Policy`), and the hook through which the line mode asks (`SetPrompter`). The window, the editor façade and `/ask` draw the question their own way and share these rules. See [Yolo mode and permissions](yolo-and-permissions.md). | | |
| 27 | 28 | | `agent` | The terminal REPL. | |
| 28 | 29 | | `acp` | The Agent Client Protocol façade. | |
| 29 | 30 | | `tui` | The single-window front end (`mm -tui`): a `ui.Sink` drawing on tcell, one transcript value, an event loop woken by the engine's and tools' goroutines. | |
| @@ -38,7 +39,7 @@ A draw.io diagram of these packages and their dependencies is kept at [`docs/dia | ||
| 38 | 39 | |
| 39 | 40 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. |
| 40 | 41 | |
| 41 | -**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines and nobody asks permission. In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `permissions: ask` is the terminal's version of the editor's dialog. | |
| 42 | +**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines, and ask through a one-line `🔐` prompt only when `yolo` is off (see [Yolo mode and permissions](yolo-and-permissions.md)). In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `yolo: false` is the window's version of the editor's dialog. | |
| 42 | 43 | |
| 43 | 44 | **One command table.** The slash commands live in `internal/command`, marked with the front ends that offer them. The REPL and the window read the same table; the ACP façade advertises only the Editor slice (`/new`, `/compact`) through `available_commands_update`, because the editor owns the process and cancels turns itself. A command added in one place is therefore offered — or deliberately not — everywhere at once. |
| 44 | 45 | |
| @@ -24,6 +24,7 @@ The entry point, `main.go`, only does wiring: it loads the settings, opens the e | |||
| 24 | | `spinner` | The "still working" indicator on a terminal. | | 24 | | `spinner` | The "still working" indicator on a terminal. | |
| 25 | | `version` | Which `mm` this is: the tag stamped at build time, or the commit Go embeds otherwise. Read by `--version`, by the start-up banner and by the window's status bar. | | 25 | | `version` | Which `mm` this is: the tag stamped at build time, or the commit Go embeds otherwise. Read by `--version`, by the start-up banner and by the window's status bar. | |
| 26 | | `ui` | Where human-facing output goes, and the event sink the front ends share. | | 26 | | `ui` | Where human-facing output goes, and the event sink the front ends share. | |
| 27 | +| `consent` | Whether a tool call may run: the `yolo` switch (`Gate`), the four answers (`Answer`), what a session remembers per tool (`Policy`), and the hook through which the line mode asks (`SetPrompter`). The window, the editor façade and `/ask` draw the question their own way and share these rules. See [Yolo mode and permissions](yolo-and-permissions.md). | | ||
| 27 | | `agent` | The terminal REPL. | | 28 | | `agent` | The terminal REPL. | |
| 28 | | `acp` | The Agent Client Protocol façade. | | 29 | | `acp` | The Agent Client Protocol façade. | |
| 29 | | `tui` | The single-window front end (`mm -tui`): a `ui.Sink` drawing on tcell, one transcript value, an event loop woken by the engine's and tools' goroutines. | | 30 | | `tui` | The single-window front end (`mm -tui`): a `ui.Sink` drawing on tcell, one transcript value, an event loop woken by the engine's and tools' goroutines. | |
| @@ -38,7 +39,7 @@ A draw.io diagram of these packages and their dependencies is kept at [`docs/dia | |||
| 38 | 39 | ||
| 39 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. | 40 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. |
| 40 | 41 | ||
| 41 | -**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines and nobody asks permission. In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `permissions: ask` is the terminal's version of the editor's dialog. | 42 | +**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines, and ask through a one-line `🔐` prompt only when `yolo` is off (see [Yolo mode and permissions](yolo-and-permissions.md)). In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `yolo: false` is the window's version of the editor's dialog. |
| 42 | 43 | ||
| 43 | **One command table.** The slash commands live in `internal/command`, marked with the front ends that offer them. The REPL and the window read the same table; the ACP façade advertises only the Editor slice (`/new`, `/compact`) through `available_commands_update`, because the editor owns the process and cancels turns itself. A command added in one place is therefore offered — or deliberately not — everywhere at once. | 44 | **One command table.** The slash commands live in `internal/command`, marked with the front ends that offer them. The REPL and the window read the same table; the ACP façade advertises only the Editor slice (`/new`, `/compact`) through `available_commands_update`, because the editor owns the process and cancels turns itself. A command added in one place is therefore offered — or deliberately not — everywhere at once. |
| 44 | 45 | ||
modified
docs/en/explanation/asking-another-agent.md +2 -2 | @@ -10,7 +10,7 @@ | ||
| 10 | 10 | |
| 11 | 11 | **The two talk ACP.** mm already speaks the Agent Client Protocol as a server, for editors. Asking bob makes the asking agent an ACP *client* of its child. The protocol gives, for free, what scraping a terminal never would: bob's answer as text, his tool calls as structured events, his permission requests as requests that can be answered, and a standard way to cancel. mm's ACP server only ever calls two client methods — session updates and permission requests — so the client side is small; file and terminal requests are refused as absent. |
| 12 | 12 | |
| 13 | -**Bob's permissions are yours to give.** When bob wants to run a command, the request is forwarded to the front end you are using: in an editor its own dialog, in the window the `permissions` setting and its inline prompt, in the line mode nothing — exactly as for your own agent's tools there. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. | |
| 13 | +**Bob's permissions are yours to give.** Bob is started with `AGENT_YOLO=false`, so every call he would gate becomes a permission request to your agent, and YOUR `yolo` decides: on, it runs; off, the question goes to the front end you are using — the editor's dialog, the window's box, the line mode's `🔐` prompt — exactly as for your own agent's tools there. The `yolo` in bob's own file does not count while he answers you. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. See [Yolo mode and permissions](yolo-and-permissions.md). | |
| 14 | 14 | |
| 15 | 15 | **The answer is a pair of messages.** It enters your history as a user message (`[asked bob (provider/model): …]`) followed by an assistant message (`[bob's answer]` and the text). One message would have been simpler, but your next question is a user message too, and several chat templates (Gemma's, Mistral's) refuse two user messages in a row. The marks say who said what, so your model does not mistake bob's answer for its own. The answer is capped like a tool's output, at `maxOutput` characters: it lands in your model's window. |
| 16 | 16 | |
| @@ -22,7 +22,7 @@ | ||
| 22 | 22 | |
| 23 | 23 | **Chains stop.** `/ask` is a command a person types; no model can call it, so no agent loops on its own. A person could still type `/ask` into an agent that was itself asked, so each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone. |
| 24 | 24 | |
| 25 | -**The same machinery answers scripts.** `mm --ask <agent>` and `mm --ask-with-context <agent>` are the slash commands for whatever can only run a command — a skill, a script. They start the same child over ACP, with the same environment fence and depth limit, but the answer goes to stdout and everything else to stderr, so a caller captures the answer alone. mm exports `MM_BIN` and `MM_CONFIG` to the commands its `bash` tool runs, so a skill calls "this mm, with this configuration" without knowing either path. Two things differ from the slash commands, both because the caller is a command and not a person. The agent's tool calls are allowed, as in the line mode: the person already approved the `mm --ask` command itself. And `mm --ask` watches its parent process: the `bash` tool kills `bash` when its budget runs out, not bash's children, and an orphaned ask kept its agent running — and writing files — after nobody waited for it any more. | |
| 25 | +**The same machinery answers scripts.** `mm --ask <agent>` and `mm --ask-with-context <agent>` are the slash commands for whatever can only run a command — a skill, a script. They start the same child over ACP, with the same environment fence and depth limit, but the answer goes to stdout and everything else to stderr, so a caller captures the answer alone. mm exports `MM_BIN` and `MM_CONFIG` to the commands its `bash` tool runs, so a skill calls "this mm, with this configuration" without knowing either path. Two things differ from the slash commands, both because the caller is a command and not a person. The agent's tool calls follow the calling configuration's `yolo`: allowed when it is on — the person already approved the `mm --ask` command itself —, rejected when it is off, since stdin carried the question and nobody is there to answer. And `mm --ask` watches its parent process: the `bash` tool kills `bash` when its budget runs out, not bash's children, and an orphaned ask kept its agent running — and writing files — after nobody waited for it any more. | |
| 26 | 26 | |
| 27 | 27 | **Time is the weak point.** A local model can take a minute to answer, and one `bash` command had 30 seconds; `bashTimeout` now sets that budget. The watchdog of the asking agent also keeps counting while a command runs (ticket 57), so it has to be raised too until that ticket is fixed. |
| 28 | 28 | |
| @@ -10,7 +10,7 @@ | |||
| 10 | 10 | ||
| 11 | **The two talk ACP.** mm already speaks the Agent Client Protocol as a server, for editors. Asking bob makes the asking agent an ACP *client* of its child. The protocol gives, for free, what scraping a terminal never would: bob's answer as text, his tool calls as structured events, his permission requests as requests that can be answered, and a standard way to cancel. mm's ACP server only ever calls two client methods — session updates and permission requests — so the client side is small; file and terminal requests are refused as absent. | 11 | **The two talk ACP.** mm already speaks the Agent Client Protocol as a server, for editors. Asking bob makes the asking agent an ACP *client* of its child. The protocol gives, for free, what scraping a terminal never would: bob's answer as text, his tool calls as structured events, his permission requests as requests that can be answered, and a standard way to cancel. mm's ACP server only ever calls two client methods — session updates and permission requests — so the client side is small; file and terminal requests are refused as absent. |
| 12 | 12 | ||
| 13 | -**Bob's permissions are yours to give.** When bob wants to run a command, the request is forwarded to the front end you are using: in an editor its own dialog, in the window the `permissions` setting and its inline prompt, in the line mode nothing — exactly as for your own agent's tools there. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. | 13 | +**Bob's permissions are yours to give.** Bob is started with `AGENT_YOLO=false`, so every call he would gate becomes a permission request to your agent, and YOUR `yolo` decides: on, it runs; off, the question goes to the front end you are using — the editor's dialog, the window's box, the line mode's `🔐` prompt — exactly as for your own agent's tools there. The `yolo` in bob's own file does not count while he answers you. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. See [Yolo mode and permissions](yolo-and-permissions.md). |
| 14 | 14 | ||
| 15 | **The answer is a pair of messages.** It enters your history as a user message (`[asked bob (provider/model): …]`) followed by an assistant message (`[bob's answer]` and the text). One message would have been simpler, but your next question is a user message too, and several chat templates (Gemma's, Mistral's) refuse two user messages in a row. The marks say who said what, so your model does not mistake bob's answer for its own. The answer is capped like a tool's output, at `maxOutput` characters: it lands in your model's window. | 15 | **The answer is a pair of messages.** It enters your history as a user message (`[asked bob (provider/model): …]`) followed by an assistant message (`[bob's answer]` and the text). One message would have been simpler, but your next question is a user message too, and several chat templates (Gemma's, Mistral's) refuse two user messages in a row. The marks say who said what, so your model does not mistake bob's answer for its own. The answer is capped like a tool's output, at `maxOutput` characters: it lands in your model's window. |
| 16 | 16 | ||
| @@ -22,7 +22,7 @@ | |||
| 22 | 22 | ||
| 23 | **Chains stop.** `/ask` is a command a person types; no model can call it, so no agent loops on its own. A person could still type `/ask` into an agent that was itself asked, so each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone. | 23 | **Chains stop.** `/ask` is a command a person types; no model can call it, so no agent loops on its own. A person could still type `/ask` into an agent that was itself asked, so each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone. |
| 24 | 24 | ||
| 25 | -**The same machinery answers scripts.** `mm --ask <agent>` and `mm --ask-with-context <agent>` are the slash commands for whatever can only run a command — a skill, a script. They start the same child over ACP, with the same environment fence and depth limit, but the answer goes to stdout and everything else to stderr, so a caller captures the answer alone. mm exports `MM_BIN` and `MM_CONFIG` to the commands its `bash` tool runs, so a skill calls "this mm, with this configuration" without knowing either path. Two things differ from the slash commands, both because the caller is a command and not a person. The agent's tool calls are allowed, as in the line mode: the person already approved the `mm --ask` command itself. And `mm --ask` watches its parent process: the `bash` tool kills `bash` when its budget runs out, not bash's children, and an orphaned ask kept its agent running — and writing files — after nobody waited for it any more. | 25 | +**The same machinery answers scripts.** `mm --ask <agent>` and `mm --ask-with-context <agent>` are the slash commands for whatever can only run a command — a skill, a script. They start the same child over ACP, with the same environment fence and depth limit, but the answer goes to stdout and everything else to stderr, so a caller captures the answer alone. mm exports `MM_BIN` and `MM_CONFIG` to the commands its `bash` tool runs, so a skill calls "this mm, with this configuration" without knowing either path. Two things differ from the slash commands, both because the caller is a command and not a person. The agent's tool calls follow the calling configuration's `yolo`: allowed when it is on — the person already approved the `mm --ask` command itself —, rejected when it is off, since stdin carried the question and nobody is there to answer. And `mm --ask` watches its parent process: the `bash` tool kills `bash` when its budget runs out, not bash's children, and an orphaned ask kept its agent running — and writing files — after nobody waited for it any more. |
| 26 | 26 | ||
| 27 | **Time is the weak point.** A local model can take a minute to answer, and one `bash` command had 30 seconds; `bashTimeout` now sets that budget. The watchdog of the asking agent also keeps counting while a command runs (ticket 57), so it has to be raised too until that ticket is fixed. | 27 | **Time is the weak point.** A local model can take a minute to answer, and one `bash` command had 30 seconds; `bashTimeout` now sets that budget. The watchdog of the asking agent also keeps counting while a command runs (ticket 57), so it has to be raised too until that ticket is fixed. |
| 28 | 28 | ||
modified
docs/en/explanation/lsp-tool.md +1 -1 | @@ -20,7 +20,7 @@ An editor tells its server about every keystroke. `mm` has no keystrokes: the mo | ||
| 20 | 20 | |
| 21 | 21 | ## Why `rename` writes the files itself, and asks first |
| 22 | 22 | |
| 23 | -A rename is a `WorkspaceEdit`: ranges to replace in several files. The client applies them to disk — from the end of each file backwards so earlier positions stay valid, with UTF-16 columns converted to bytes — then re-syncs the changed files so the next question is answered on the renamed code. In the terminal it runs like `edit_file` does; where a front end has a permission dialog (an editor over ACP, the window mode with `permissions: ask`) it asks, because it writes, while the five navigation actions do not: a dialog on every read teaches people to click "allow" without reading. | |
| 23 | +A rename is a `WorkspaceEdit`: ranges to replace in several files. The client applies them to disk — from the end of each file backwards so earlier positions stay valid, with UTF-16 columns converted to bytes — then re-syncs the changed files so the next question is answered on the renamed code. In the terminal it runs like `edit_file` does; where a front end has a permission dialog (with `yolo: false`: the line mode, the window, an editor over ACP) it asks, because it writes, while the five navigation actions do not: a dialog on every read teaches people to click "allow" without reading. | |
| 24 | 24 | |
| 25 | 25 | ## Why a missing server is a warning |
| 26 | 26 | |
| @@ -20,7 +20,7 @@ An editor tells its server about every keystroke. `mm` has no keystrokes: the mo | |||
| 20 | 20 | ||
| 21 | ## Why `rename` writes the files itself, and asks first | 21 | ## Why `rename` writes the files itself, and asks first |
| 22 | 22 | ||
| 23 | -A rename is a `WorkspaceEdit`: ranges to replace in several files. The client applies them to disk — from the end of each file backwards so earlier positions stay valid, with UTF-16 columns converted to bytes — then re-syncs the changed files so the next question is answered on the renamed code. In the terminal it runs like `edit_file` does; where a front end has a permission dialog (an editor over ACP, the window mode with `permissions: ask`) it asks, because it writes, while the five navigation actions do not: a dialog on every read teaches people to click "allow" without reading. | 23 | +A rename is a `WorkspaceEdit`: ranges to replace in several files. The client applies them to disk — from the end of each file backwards so earlier positions stay valid, with UTF-16 columns converted to bytes — then re-syncs the changed files so the next question is answered on the renamed code. In the terminal it runs like `edit_file` does; where a front end has a permission dialog (with `yolo: false`: the line mode, the window, an editor over ACP) it asks, because it writes, while the five navigation actions do not: a dialog on every read teaches people to click "allow" without reading. |
| 24 | 24 | ||
| 25 | ## Why a missing server is a warning | 25 | ## Why a missing server is a warning |
| 26 | 26 | ||
modified
docs/en/explanation/mcp-architecture.md +1 -1 | @@ -37,7 +37,7 @@ A draw.io diagram of these packages and their dependencies is kept at [`docs/dia | ||
| 37 | 37 | |
| 38 | 38 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. |
| 39 | 39 | |
| 40 | -**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines and nobody asks permission. In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `permissions: ask` is the terminal's version of the editor's dialog. | |
| 40 | +**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines, and ask through a one-line `🔐` prompt only when `yolo` is off (see [Yolo mode and permissions](yolo-and-permissions.md)). In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `yolo: false` is the window's version of the editor's dialog. | |
| 41 | 41 | |
| 42 | 42 | **One command table.** The slash commands live in `internal/command`, marked with the front ends that offer them. The REPL and the window read the same table; the ACP façade advertises only the Editor slice (`/new`, `/compact`) through `available_commands_update`, because the editor owns the process and cancels turns itself. A command added in one place is therefore offered — or deliberately not — everywhere at once. |
| 43 | 43 | |
| @@ -37,7 +37,7 @@ A draw.io diagram of these packages and their dependencies is kept at [`docs/dia | |||
| 37 | 37 | ||
| 38 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. | 38 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. |
| 39 | 39 | ||
| 40 | -**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines and nobody asks permission. In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `permissions: ask` is the terminal's version of the editor's dialog. | 40 | +**One loop, three front ends.** The terminal REPL, the window and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines, and ask through a one-line `🔐` prompt only when `yolo` is off (see [Yolo mode and permissions](yolo-and-permissions.md)). In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. The window mode is the same trick a second time: `tui` installs its own `ui.Sink` for each turn, and the events become entries of a transcript drawn on the screen — so the tools needed no change to gain a third home, and the inline permission prompt of `yolo: false` is the window's version of the editor's dialog. |
| 41 | 41 | ||
| 42 | **One command table.** The slash commands live in `internal/command`, marked with the front ends that offer them. The REPL and the window read the same table; the ACP façade advertises only the Editor slice (`/new`, `/compact`) through `available_commands_update`, because the editor owns the process and cancels turns itself. A command added in one place is therefore offered — or deliberately not — everywhere at once. | 42 | **One command table.** The slash commands live in `internal/command`, marked with the front ends that offer them. The REPL and the window read the same table; the ACP façade advertises only the Editor slice (`/new`, `/compact`) through `available_commands_update`, because the editor owns the process and cancels turns itself. A command added in one place is therefore offered — or deliberately not — everywhere at once. |
| 43 | 43 | ||
added
docs/en/explanation/yolo-and-permissions.md +51 -0 | new file mode 100644 | ||
| @@ -0,0 +1,51 @@ | ||
| 1 | +# Yolo mode and permissions — explanation | |
| 2 | + | |
| 3 | +## What is this about? | |
| 4 | + | |
| 5 | +A coding agent runs commands and writes files on your machine. Somebody has to decide whether each of those calls may run: either the user, call by call, or nobody, because the user decided once and for all to let the agent act. mini-me calls the second choice **yolo mode** and makes it one setting, `yolo`, read the same way by the three front ends — the line mode, the window and the editor (ACP) — and by an agent asked by another one. | |
| 6 | + | |
| 7 | +## Why yolo is the default | |
| 8 | + | |
| 9 | +mini-me is built to drive a small local model through many short steps. Asking before each `ls` makes that loop unusable in practice, and the line mode — the first front end — never asked at all. Before this setting existed, the behaviour depended on where you ran mm: the line mode never asked, the window asked only with `permissions: ask`, and the editor always asked, whatever you wanted. The same agent file did not mean the same thing in three places. | |
| 10 | + | |
| 11 | +The default was therefore set to what most runs already did — run without asking — and the protection became a deliberate, visible choice: `yolo: false`. The banner says which way it is set (`yolo: on` / `yolo: off (tools ask)`), so nobody finds out by surprise, in either direction. The consequence worth knowing: in an editor, the permission dialog only appears with `yolo: false`; it used to appear always. | |
| 12 | + | |
| 13 | +## Existing versus asking | |
| 14 | + | |
| 15 | +Two different questions are kept apart on purpose: | |
| 16 | + | |
| 17 | +- **Does the tool exist?** `bashTool`, `editTools`, `fetchContentTool`. A tool that does not exist is not declared to the model at all; it cannot even try. | |
| 18 | +- **Does an existing tool ask before running?** `yolo`. | |
| 19 | + | |
| 20 | +So `yolo: true` with `bashTool: false` is an agent that edits files freely but has no shell, and `yolo: false` with `bashTool: true` is an agent that has a shell but needs your consent for each command. Merging the two into one tri-state (`bashTool: ask`) was considered and rejected: it would have to be repeated for every tool, while the real decision — "do I trust this agent to act alone right now?" — is a single one. | |
| 21 | + | |
| 22 | +Only the calls that change something ask: `bash`, `write_file`, `edit_file`, `fetch_content` when it saves, `lsp` `rename`, and the `plan` actions that change a plan. Reads (`read_file`, `read_skill`, the `lsp` navigation actions, `fetch_content` without `output`) never ask: a dialog on every read teaches people to click "allow" without reading. | |
| 23 | + | |
| 24 | +## Four answers, one memory | |
| 25 | + | |
| 26 | +When a call asks, the answer can be *once* or *for the session*, and *yes* or *no*: allow once, allow this tool for the session, reject this call, never allow this tool in this session. "Never" exists because a refusal that only lasts one call leaves you rejecting the same retry again and again; "reject once" exists because without it the only way to say no would be final. A session's remembered answers are kept per tool name, survive `/new` (the editor still sees the same session), and disappear when mm exits. | |
| 27 | + | |
| 28 | +The rules live in one package, `internal/consent`: the `yolo` check (`Gate`), the four answers (`Answer`), what a session remembers (`Policy`) and the line mode's prompt hook. Each front end only draws the question its own way — a `🔐` line, the window's box, the editor's dialog. Before, each front end had its own map of "allowed for this session", and the three had already started to differ. | |
| 29 | + | |
| 30 | +## Why the asking agent decides | |
| 31 | + | |
| 32 | +With `mmConfigs`, one agent can ask another (`/ask-jan`, `mm --ask jan`). Two configuration files, two `yolo` values: which one wins? The one of the agent the **user started**. It is the only agent with a user in front of it, and the user chose its configuration for this session; the asked agent's file was written for a different situation — perhaps for running alone in yolo mode. | |
| 33 | + | |
| 34 | +Technically, the asked agent is started with `AGENT_YOLO=false`, so it submits every call it would gate through the ACP permission request — the same channel an editor uses. The asking agent receives it and runs it through its own `consent.Gate`: in yolo mode it allows at once; otherwise it asks its own user, in its own front end, and remembers the answer under `jan/bash` so that allowing Jan's shell does not allow yours. The asked agent itself never remembers anything: it is asked again each time, and the asking agent's memory answers. | |
| 35 | + | |
| 36 | +## Modes that cannot ask | |
| 37 | + | |
| 38 | +`mm -p` and `mm --ask` have nobody to ask: the first is a one-shot run, the second reads its question from stdin. With `yolo: false` they reject the calls that would ask and say so once on stderr. Allowing them silently would turn `yolo: false` into a setting that depends on how you launched mm, which is exactly the inconsistency this design removes; hanging on an invisible question would block scripts forever. | |
| 39 | + | |
| 40 | +## Rejected alternatives | |
| 41 | + | |
| 42 | +- **A `--yolo` command-line flag only.** It would not reach the sandbox kits or an editor's launch settings, which set environment variables and files, not flags. `AGENT_YOLO` covers the one-run case instead. | |
| 43 | +- **Keeping `permissions: ask | auto`.** It was read by the window only and appeared in no shipped configuration. It still works — `ask` means `yolo: false` — with a deprecation warning, so old files keep their protection. | |
| 44 | +- **Letting yolo override MCP servers marked `ask`.** An MCP server is third-party code the user explicitly marked as untrusted; `yolo: true` keeps that per-server choice. `yolo: false`, on the other hand, makes every MCP tool ask: turning protection on should never leave a hole. | |
| 45 | + | |
| 46 | +## How it relates to the rest | |
| 47 | + | |
| 48 | +- Configuration keys: [configuration reference](../reference/configuration.md). | |
| 49 | +- How to turn it on: [How to confirm tool calls](../how-to/confirm-tool-calls.md). | |
| 50 | +- The event path the front ends share (`ui.Sink`, `Allow`): [Architecture](architecture.md). | |
| 51 | +- Asking another agent: [Asking another agent](asking-another-agent.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | +# Yolo mode and permissions — explanation | ||
| 2 | + | ||
| 3 | +## What is this about? | ||
| 4 | + | ||
| 5 | +A coding agent runs commands and writes files on your machine. Somebody has to decide whether each of those calls may run: either the user, call by call, or nobody, because the user decided once and for all to let the agent act. mini-me calls the second choice **yolo mode** and makes it one setting, `yolo`, read the same way by the three front ends — the line mode, the window and the editor (ACP) — and by an agent asked by another one. | ||
| 6 | + | ||
| 7 | +## Why yolo is the default | ||
| 8 | + | ||
| 9 | +mini-me is built to drive a small local model through many short steps. Asking before each `ls` makes that loop unusable in practice, and the line mode — the first front end — never asked at all. Before this setting existed, the behaviour depended on where you ran mm: the line mode never asked, the window asked only with `permissions: ask`, and the editor always asked, whatever you wanted. The same agent file did not mean the same thing in three places. | ||
| 10 | + | ||
| 11 | +The default was therefore set to what most runs already did — run without asking — and the protection became a deliberate, visible choice: `yolo: false`. The banner says which way it is set (`yolo: on` / `yolo: off (tools ask)`), so nobody finds out by surprise, in either direction. The consequence worth knowing: in an editor, the permission dialog only appears with `yolo: false`; it used to appear always. | ||
| 12 | + | ||
| 13 | +## Existing versus asking | ||
| 14 | + | ||
| 15 | +Two different questions are kept apart on purpose: | ||
| 16 | + | ||
| 17 | +- **Does the tool exist?** `bashTool`, `editTools`, `fetchContentTool`. A tool that does not exist is not declared to the model at all; it cannot even try. | ||
| 18 | +- **Does an existing tool ask before running?** `yolo`. | ||
| 19 | + | ||
| 20 | +So `yolo: true` with `bashTool: false` is an agent that edits files freely but has no shell, and `yolo: false` with `bashTool: true` is an agent that has a shell but needs your consent for each command. Merging the two into one tri-state (`bashTool: ask`) was considered and rejected: it would have to be repeated for every tool, while the real decision — "do I trust this agent to act alone right now?" — is a single one. | ||
| 21 | + | ||
| 22 | +Only the calls that change something ask: `bash`, `write_file`, `edit_file`, `fetch_content` when it saves, `lsp` `rename`, and the `plan` actions that change a plan. Reads (`read_file`, `read_skill`, the `lsp` navigation actions, `fetch_content` without `output`) never ask: a dialog on every read teaches people to click "allow" without reading. | ||
| 23 | + | ||
| 24 | +## Four answers, one memory | ||
| 25 | + | ||
| 26 | +When a call asks, the answer can be *once* or *for the session*, and *yes* or *no*: allow once, allow this tool for the session, reject this call, never allow this tool in this session. "Never" exists because a refusal that only lasts one call leaves you rejecting the same retry again and again; "reject once" exists because without it the only way to say no would be final. A session's remembered answers are kept per tool name, survive `/new` (the editor still sees the same session), and disappear when mm exits. | ||
| 27 | + | ||
| 28 | +The rules live in one package, `internal/consent`: the `yolo` check (`Gate`), the four answers (`Answer`), what a session remembers (`Policy`) and the line mode's prompt hook. Each front end only draws the question its own way — a `🔐` line, the window's box, the editor's dialog. Before, each front end had its own map of "allowed for this session", and the three had already started to differ. | ||
| 29 | + | ||
| 30 | +## Why the asking agent decides | ||
| 31 | + | ||
| 32 | +With `mmConfigs`, one agent can ask another (`/ask-jan`, `mm --ask jan`). Two configuration files, two `yolo` values: which one wins? The one of the agent the **user started**. It is the only agent with a user in front of it, and the user chose its configuration for this session; the asked agent's file was written for a different situation — perhaps for running alone in yolo mode. | ||
| 33 | + | ||
| 34 | +Technically, the asked agent is started with `AGENT_YOLO=false`, so it submits every call it would gate through the ACP permission request — the same channel an editor uses. The asking agent receives it and runs it through its own `consent.Gate`: in yolo mode it allows at once; otherwise it asks its own user, in its own front end, and remembers the answer under `jan/bash` so that allowing Jan's shell does not allow yours. The asked agent itself never remembers anything: it is asked again each time, and the asking agent's memory answers. | ||
| 35 | + | ||
| 36 | +## Modes that cannot ask | ||
| 37 | + | ||
| 38 | +`mm -p` and `mm --ask` have nobody to ask: the first is a one-shot run, the second reads its question from stdin. With `yolo: false` they reject the calls that would ask and say so once on stderr. Allowing them silently would turn `yolo: false` into a setting that depends on how you launched mm, which is exactly the inconsistency this design removes; hanging on an invisible question would block scripts forever. | ||
| 39 | + | ||
| 40 | +## Rejected alternatives | ||
| 41 | + | ||
| 42 | +- **A `--yolo` command-line flag only.** It would not reach the sandbox kits or an editor's launch settings, which set environment variables and files, not flags. `AGENT_YOLO` covers the one-run case instead. | ||
| 43 | +- **Keeping `permissions: ask | auto`.** It was read by the window only and appeared in no shipped configuration. It still works — `ask` means `yolo: false` — with a deprecation warning, so old files keep their protection. | ||
| 44 | +- **Letting yolo override MCP servers marked `ask`.** An MCP server is third-party code the user explicitly marked as untrusted; `yolo: true` keeps that per-server choice. `yolo: false`, on the other hand, makes every MCP tool ask: turning protection on should never leave a hole. | ||
| 45 | + | ||
| 46 | +## How it relates to the rest | ||
| 47 | + | ||
| 48 | +- Configuration keys: [configuration reference](../reference/configuration.md). | ||
| 49 | +- How to turn it on: [How to confirm tool calls](../how-to/confirm-tool-calls.md). | ||
| 50 | +- The event path the front ends share (`ui.Sink`, `Allow`): [Architecture](architecture.md). | ||
| 51 | +- Asking another agent: [Asking another agent](asking-another-agent.md). | ||
modified
docs/en/how-to/ask-another-agent.md +1 -1 | @@ -50,7 +50,7 @@ This guide shows how to put a question to another mm agent — another model, an | ||
| 50 | 50 | |
| 51 | 51 | - **Let the other agent see the conversation.** Use `/ask-with-context-lucy <prompt>`: Lucy receives a transcript of the conversation (questions, answers, one line per tool call — never tool outputs) before your question. |
| 52 | 52 | - **In the terminal or the window, by name.** `/ask lucy <prompt>` and `/ask-with-context lucy <prompt>` are the same commands with the agent as an argument. |
| 53 | -- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, the editor's own permission dialog asks you. | |
| 53 | +- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, your agent's `yolo` decides: with `yolo: true` it runs, with `yolo: false` the editor's own permission dialog asks you — whatever Lucy's own file says. See [How to confirm tool calls](confirm-tool-calls.md). | |
| 54 | 54 | - **Stop a long answer.** `/abort`, `Esc` or `Ctrl+C` — nothing is kept from a cancelled ask. |
| 55 | 55 | - **Agents on other providers.** Any mm configuration works: an Ollama agent next to an llmman one is just another `mmConfigs` entry. |
| 56 | 56 | |
| @@ -50,7 +50,7 @@ This guide shows how to put a question to another mm agent — another model, an | |||
| 50 | 50 | ||
| 51 | - **Let the other agent see the conversation.** Use `/ask-with-context-lucy <prompt>`: Lucy receives a transcript of the conversation (questions, answers, one line per tool call — never tool outputs) before your question. | 51 | - **Let the other agent see the conversation.** Use `/ask-with-context-lucy <prompt>`: Lucy receives a transcript of the conversation (questions, answers, one line per tool call — never tool outputs) before your question. |
| 52 | - **In the terminal or the window, by name.** `/ask lucy <prompt>` and `/ask-with-context lucy <prompt>` are the same commands with the agent as an argument. | 52 | - **In the terminal or the window, by name.** `/ask lucy <prompt>` and `/ask-with-context lucy <prompt>` are the same commands with the agent as an argument. |
| 53 | -- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, the editor's own permission dialog asks you. | 53 | +- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, your agent's `yolo` decides: with `yolo: true` it runs, with `yolo: false` the editor's own permission dialog asks you — whatever Lucy's own file says. See [How to confirm tool calls](confirm-tool-calls.md). |
| 54 | - **Stop a long answer.** `/abort`, `Esc` or `Ctrl+C` — nothing is kept from a cancelled ask. | 54 | - **Stop a long answer.** `/abort`, `Esc` or `Ctrl+C` — nothing is kept from a cancelled ask. |
| 55 | - **Agents on other providers.** Any mm configuration works: an Ollama agent next to an llmman one is just another `mmConfigs` entry. | 55 | - **Agents on other providers.** Any mm configuration works: an Ollama agent next to an llmman one is just another `mmConfigs` entry. |
| 56 | 56 | ||
added
docs/en/how-to/confirm-tool-calls.md +57 -0 | new file mode 100644 | ||
| @@ -0,0 +1,57 @@ | ||
| 1 | +# How to confirm tool calls before they run | |
| 2 | + | |
| 3 | +By default mini-me runs in **yolo mode**: `bash`, file writes and edits run as soon as the model asks for them. This guide shows how to make the agent ask you first, in the terminal, in the window and in an editor. It assumes you already run `mm` with a YAML configuration file. | |
| 4 | + | |
| 5 | +## Steps | |
| 6 | + | |
| 7 | +1. Turn yolo off in your configuration file: | |
| 8 | + | |
| 9 | + ```yaml | |
| 10 | + yolo: false | |
| 11 | + bashTool: true | |
| 12 | + ``` | |
| 13 | + | |
| 14 | + `bashTool` (and `editTools`, `fetchContentTool`) still decide which tools **exist**; `yolo` decides whether the ones that exist ask before running. | |
| 15 | + | |
| 16 | +2. Start `mm` as usual. The banner ends with `yolo: off (tools ask)`: | |
| 17 | + | |
| 18 | + ``` | |
| 19 | + mm v0.2.6 | config: agent.yaml | provider: dmr | model: … | tools: bash, read_file, write_file, edit_file | skills: 3 | yolo: off (tools ask) | |
| 20 | + ``` | |
| 21 | + | |
| 22 | +3. Ask for something that runs a command. In the line mode, the tool's line is followed by a question: | |
| 23 | + | |
| 24 | + ``` | |
| 25 | + 🛠️ bash: ls -la | |
| 26 | + | |
| 27 | + 🔐 bash: ls -la | |
| 28 | + [a] allow once · [s] allow bash for this session · [r] reject · [n] never bash | |
| 29 | + ? | |
| 30 | + ``` | |
| 31 | + | |
| 32 | +4. Type one letter and press Enter: | |
| 33 | + | |
| 34 | + | Key | Effect | | |
| 35 | + |-----|--------| | |
| 36 | + | `a` | Run this call. The next `bash` call asks again. | | |
| 37 | + | `s` | Run this call and every later `bash` call of this session without asking. | | |
| 38 | + | `r` | Do not run this call. The model receives `[command rejected by the user]` and can propose something else. The next call asks again. | | |
| 39 | + | `n` | Do not run this call, and refuse every later `bash` call of this session without asking. | | |
| 40 | + | |
| 41 | + Anything else shows the legend again. `/abort` or **Ctrl+C** rejects the call and stops the turn. | |
| 42 | + | |
| 43 | +## Variants | |
| 44 | + | |
| 45 | +- **In the window (`mm -tui`).** The input box is replaced by `? bash: ls -la` and the same four keys answer it; **Esc** is the same as `r`. | |
| 46 | +- **In an editor (`mm -acp`, Zed).** The editor's permission dialog offers *Allow once*, *Allow bash for this session*, *Reject* and *Never allow bash in this session*. With `yolo: true`, no dialog is shown at all. | |
| 47 | +- **For one run only, without editing the file.** `AGENT_YOLO=false mm agent.yaml` (or `AGENT_YOLO=true` to skip the questions once). In an editor, put the variable in the agent's `env` settings next to `AGENT_CONFIG`. | |
| 48 | +- **With other agents (`mmConfigs`).** The agent you started decides for the agents it asks. With `yolo: false` in your main configuration, a `bash` call made by `/ask-jan` asks *you*, titled `jan · bash: …`, and `s` remembers `jan/bash`, not your own `bash`. The `yolo` written in Jan's own file is ignored while it answers an `/ask`. | |
| 49 | +- **In a script (`mm -p`, `mm --ask`).** Nobody can answer there, so with `yolo: false` the calls that would ask are rejected, and stderr says so once: `[yolo is off and this mode cannot ask: …]`. Keep `yolo: true` for scripted runs, or remove the tools you do not want with `bashTool: false` / `editTools: false`. | |
| 50 | +- **MCP tools.** With `yolo: false` every MCP tool asks too. With `yolo: true`, each MCP server keeps its own `permissions` (`ask` by default, `auto` to run unasked). | |
| 51 | +- **An old configuration with `permissions: ask`.** It still works and means `yolo: false`; the banner warns that the key is deprecated. Replace it with `yolo: false`. | |
| 52 | + | |
| 53 | +## See also | |
| 54 | + | |
| 55 | +- Every key and its default: [configuration reference](../reference/configuration.md) (`yolo`, `permissions`). | |
| 56 | +- What each front end shows: [CLI reference](../reference/cli.md), [ACP reference](../reference/acp.md#sessionrequest_permission-requests-sent). | |
| 57 | +- Why yolo is the default and why the asking agent wins: [Yolo mode and permissions](../explanation/yolo-and-permissions.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | +# How to confirm tool calls before they run | ||
| 2 | + | ||
| 3 | +By default mini-me runs in **yolo mode**: `bash`, file writes and edits run as soon as the model asks for them. This guide shows how to make the agent ask you first, in the terminal, in the window and in an editor. It assumes you already run `mm` with a YAML configuration file. | ||
| 4 | + | ||
| 5 | +## Steps | ||
| 6 | + | ||
| 7 | +1. Turn yolo off in your configuration file: | ||
| 8 | + | ||
| 9 | + ```yaml | ||
| 10 | + yolo: false | ||
| 11 | + bashTool: true | ||
| 12 | + ``` | ||
| 13 | + | ||
| 14 | + `bashTool` (and `editTools`, `fetchContentTool`) still decide which tools **exist**; `yolo` decides whether the ones that exist ask before running. | ||
| 15 | + | ||
| 16 | +2. Start `mm` as usual. The banner ends with `yolo: off (tools ask)`: | ||
| 17 | + | ||
| 18 | + ``` | ||
| 19 | + mm v0.2.6 | config: agent.yaml | provider: dmr | model: … | tools: bash, read_file, write_file, edit_file | skills: 3 | yolo: off (tools ask) | ||
| 20 | + ``` | ||
| 21 | + | ||
| 22 | +3. Ask for something that runs a command. In the line mode, the tool's line is followed by a question: | ||
| 23 | + | ||
| 24 | + ``` | ||
| 25 | + 🛠️ bash: ls -la | ||
| 26 | + | ||
| 27 | + 🔐 bash: ls -la | ||
| 28 | + [a] allow once · [s] allow bash for this session · [r] reject · [n] never bash | ||
| 29 | + ? | ||
| 30 | + ``` | ||
| 31 | + | ||
| 32 | +4. Type one letter and press Enter: | ||
| 33 | + | ||
| 34 | + | Key | Effect | | ||
| 35 | + |-----|--------| | ||
| 36 | + | `a` | Run this call. The next `bash` call asks again. | | ||
| 37 | + | `s` | Run this call and every later `bash` call of this session without asking. | | ||
| 38 | + | `r` | Do not run this call. The model receives `[command rejected by the user]` and can propose something else. The next call asks again. | | ||
| 39 | + | `n` | Do not run this call, and refuse every later `bash` call of this session without asking. | | ||
| 40 | + | ||
| 41 | + Anything else shows the legend again. `/abort` or **Ctrl+C** rejects the call and stops the turn. | ||
| 42 | + | ||
| 43 | +## Variants | ||
| 44 | + | ||
| 45 | +- **In the window (`mm -tui`).** The input box is replaced by `? bash: ls -la` and the same four keys answer it; **Esc** is the same as `r`. | ||
| 46 | +- **In an editor (`mm -acp`, Zed).** The editor's permission dialog offers *Allow once*, *Allow bash for this session*, *Reject* and *Never allow bash in this session*. With `yolo: true`, no dialog is shown at all. | ||
| 47 | +- **For one run only, without editing the file.** `AGENT_YOLO=false mm agent.yaml` (or `AGENT_YOLO=true` to skip the questions once). In an editor, put the variable in the agent's `env` settings next to `AGENT_CONFIG`. | ||
| 48 | +- **With other agents (`mmConfigs`).** The agent you started decides for the agents it asks. With `yolo: false` in your main configuration, a `bash` call made by `/ask-jan` asks *you*, titled `jan · bash: …`, and `s` remembers `jan/bash`, not your own `bash`. The `yolo` written in Jan's own file is ignored while it answers an `/ask`. | ||
| 49 | +- **In a script (`mm -p`, `mm --ask`).** Nobody can answer there, so with `yolo: false` the calls that would ask are rejected, and stderr says so once: `[yolo is off and this mode cannot ask: …]`. Keep `yolo: true` for scripted runs, or remove the tools you do not want with `bashTool: false` / `editTools: false`. | ||
| 50 | +- **MCP tools.** With `yolo: false` every MCP tool asks too. With `yolo: true`, each MCP server keeps its own `permissions` (`ask` by default, `auto` to run unasked). | ||
| 51 | +- **An old configuration with `permissions: ask`.** It still works and means `yolo: false`; the banner warns that the key is deprecated. Replace it with `yolo: false`. | ||
| 52 | + | ||
| 53 | +## See also | ||
| 54 | + | ||
| 55 | +- Every key and its default: [configuration reference](../reference/configuration.md) (`yolo`, `permissions`). | ||
| 56 | +- What each front end shows: [CLI reference](../reference/cli.md), [ACP reference](../reference/acp.md#sessionrequest_permission-requests-sent). | ||
| 57 | +- Why yolo is the default and why the asking agent wins: [Yolo mode and permissions](../explanation/yolo-and-permissions.md). | ||
modified
docs/en/how-to/fetch-a-web-page.md +1 -1 | @@ -46,7 +46,7 @@ The page does not enter the conversation, so a long page does not use up the con | ||
| 46 | 46 | - **Complex tables.** Tables with merged cells are kept as raw HTML by default. Ask for flattened tables ("…as Markdown tables, flatten them") and the model passes `tables: flatten`. |
| 47 | 47 | - **Strikethrough and task lists.** Ask for them explicitly ("keep the struck-through text", "keep the checkboxes") and the model passes `strikethrough: true` or `taskList: true`. |
| 48 | 48 | - **Links relative to another site.** If the page is a mirror, ask to resolve links against the original; the model passes `baseUrl`. |
| 49 | -- **In an editor (ACP) or with `permissions: ask`.** Reading a page needs no permission; saving it to a file asks first, like `write_file`. | |
| 49 | +- **With `yolo: false`.** Reading a page needs no permission; saving it to a file asks first, like `write_file`. | |
| 50 | 50 | |
| 51 | 51 | ## When it fails |
| 52 | 52 | |
| @@ -46,7 +46,7 @@ The page does not enter the conversation, so a long page does not use up the con | |||
| 46 | - **Complex tables.** Tables with merged cells are kept as raw HTML by default. Ask for flattened tables ("…as Markdown tables, flatten them") and the model passes `tables: flatten`. | 46 | - **Complex tables.** Tables with merged cells are kept as raw HTML by default. Ask for flattened tables ("…as Markdown tables, flatten them") and the model passes `tables: flatten`. |
| 47 | - **Strikethrough and task lists.** Ask for them explicitly ("keep the struck-through text", "keep the checkboxes") and the model passes `strikethrough: true` or `taskList: true`. | 47 | - **Strikethrough and task lists.** Ask for them explicitly ("keep the struck-through text", "keep the checkboxes") and the model passes `strikethrough: true` or `taskList: true`. |
| 48 | - **Links relative to another site.** If the page is a mirror, ask to resolve links against the original; the model passes `baseUrl`. | 48 | - **Links relative to another site.** If the page is a mirror, ask to resolve links against the original; the model passes `baseUrl`. |
| 49 | -- **In an editor (ACP) or with `permissions: ask`.** Reading a page needs no permission; saving it to a file asks first, like `write_file`. | 49 | +- **With `yolo: false`.** Reading a page needs no permission; saving it to a file asks first, like `write_file`. |
| 50 | 50 | ||
| 51 | ## When it fails | 51 | ## When it fails |
| 52 | 52 | ||
modified
docs/en/how-to/use-from-an-editor.md +2 -2 | @@ -21,13 +21,13 @@ This guide shows how to host `mm` inside an editor that speaks the Agent Client | ||
| 21 | 21 | |
| 22 | 22 | 2. Point `AGENT_CONFIG` at an absolute path. The editor launches `mm` with the project's directory as working directory, so a relative `./agent.yaml` would not be found. In ACP mode a relative `skillsDir` in that file is resolved next to the file, not next to the editor's working directory. |
| 23 | 23 | |
| 24 | -3. Open the agent panel and start a conversation. Each `bash`, `read_file`, `write_file` and `edit_file` call appears as a tool call; the editor asks you to allow it before it runs. `read_skill` runs without a dialog. | |
| 24 | +3. Open the agent panel and start a conversation. Each `bash`, `read_file`, `write_file` and `edit_file` call appears as a tool call. By default (`yolo: true`) they run at once; with `yolo: false` in the configuration, the editor asks you to allow each `bash`, `write_file` and `edit_file` call before it runs. `read_file` and `read_skill` never ask. | |
| 25 | 25 | |
| 26 | 26 | 4. Read the logs when something looks wrong. In ACP mode nothing human-readable goes to stdout: the banner, the warnings and a `[acp] …` trail go to stderr, which Zed shows under `dev: open acp logs`. |
| 27 | 27 | |
| 28 | 28 | ## Variants |
| 29 | 29 | |
| 30 | -- **Allow a tool for the whole session.** Pick "Allow bash for this session" in the permission dialog; the choice is remembered per tool until the session ends. | |
| 30 | +- **Allow or refuse a tool for the whole session** (`yolo: false`). Pick "Allow bash for this session" or "Never allow bash in this session" in the permission dialog; the choice is remembered per tool until the session ends. See [How to confirm tool calls](confirm-tool-calls.md). | |
| 31 | 31 | - **Cancel a running turn.** Use the editor's stop key (Escape in Zed). The agent answers the prompt with stop reason `cancelled` and keeps the commands that already ran in the history. There is no `/abort` over ACP. |
| 32 | 32 | - **Compress or restart the conversation.** Send `/compact` or `/new`; editors that list the agent's commands offer both in the slash-command menu. See the [slash commands reference](../reference/slash-commands.md#acp-mode). |
| 33 | 33 | - **Attach a file with `@`.** The editor's picker sends a `resource_link`, passed to the model as `[attached file: <path>]`; a path typed by hand as `@path` is understood the same way (see [`@path`](attach-a-file.md)). The model reads the file with its tools. |
| @@ -21,13 +21,13 @@ This guide shows how to host `mm` inside an editor that speaks the Agent Client | |||
| 21 | 21 | ||
| 22 | 2. Point `AGENT_CONFIG` at an absolute path. The editor launches `mm` with the project's directory as working directory, so a relative `./agent.yaml` would not be found. In ACP mode a relative `skillsDir` in that file is resolved next to the file, not next to the editor's working directory. | 22 | 2. Point `AGENT_CONFIG` at an absolute path. The editor launches `mm` with the project's directory as working directory, so a relative `./agent.yaml` would not be found. In ACP mode a relative `skillsDir` in that file is resolved next to the file, not next to the editor's working directory. |
| 23 | 23 | ||
| 24 | -3. Open the agent panel and start a conversation. Each `bash`, `read_file`, `write_file` and `edit_file` call appears as a tool call; the editor asks you to allow it before it runs. `read_skill` runs without a dialog. | 24 | +3. Open the agent panel and start a conversation. Each `bash`, `read_file`, `write_file` and `edit_file` call appears as a tool call. By default (`yolo: true`) they run at once; with `yolo: false` in the configuration, the editor asks you to allow each `bash`, `write_file` and `edit_file` call before it runs. `read_file` and `read_skill` never ask. |
| 25 | 25 | ||
| 26 | 4. Read the logs when something looks wrong. In ACP mode nothing human-readable goes to stdout: the banner, the warnings and a `[acp] …` trail go to stderr, which Zed shows under `dev: open acp logs`. | 26 | 4. Read the logs when something looks wrong. In ACP mode nothing human-readable goes to stdout: the banner, the warnings and a `[acp] …` trail go to stderr, which Zed shows under `dev: open acp logs`. |
| 27 | 27 | ||
| 28 | ## Variants | 28 | ## Variants |
| 29 | 29 | ||
| 30 | -- **Allow a tool for the whole session.** Pick "Allow bash for this session" in the permission dialog; the choice is remembered per tool until the session ends. | 30 | +- **Allow or refuse a tool for the whole session** (`yolo: false`). Pick "Allow bash for this session" or "Never allow bash in this session" in the permission dialog; the choice is remembered per tool until the session ends. See [How to confirm tool calls](confirm-tool-calls.md). |
| 31 | - **Cancel a running turn.** Use the editor's stop key (Escape in Zed). The agent answers the prompt with stop reason `cancelled` and keeps the commands that already ran in the history. There is no `/abort` over ACP. | 31 | - **Cancel a running turn.** Use the editor's stop key (Escape in Zed). The agent answers the prompt with stop reason `cancelled` and keeps the commands that already ran in the history. There is no `/abort` over ACP. |
| 32 | - **Compress or restart the conversation.** Send `/compact` or `/new`; editors that list the agent's commands offer both in the slash-command menu. See the [slash commands reference](../reference/slash-commands.md#acp-mode). | 32 | - **Compress or restart the conversation.** Send `/compact` or `/new`; editors that list the agent's commands offer both in the slash-command menu. See the [slash commands reference](../reference/slash-commands.md#acp-mode). |
| 33 | - **Attach a file with `@`.** The editor's picker sends a `resource_link`, passed to the model as `[attached file: <path>]`; a path typed by hand as `@path` is understood the same way (see [`@path`](attach-a-file.md)). The model reads the file with its tools. | 33 | - **Attach a file with `@`.** The editor's picker sends a `resource_link`, passed to the model as `[attached file: <path>]`; a path typed by hand as `@path` is understood the same way (see [`@path`](attach-a-file.md)). The model reads the file with its tools. |
modified
docs/en/how-to/use-the-window-mode.md +1 -1 | @@ -26,7 +26,7 @@ This guide shows how to run mini-me as a single full-screen window — the conve | ||
| 26 | 26 | |
| 27 | 27 | ## Variants |
| 28 | 28 | |
| 29 | -- **Ask before every `bash` and file tool.** Set `permissions: ask` in the YAML. The box is replaced by the question `? bash: rm -rf build` and three keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject. A rejected tool tells the model `[command rejected by the user]`. The default, `auto`, runs tools as the line mode always has. | |
| 29 | +- **Ask before every `bash` and file tool.** Set `yolo: false` in the YAML. The box is replaced by the question `? bash: rm -rf build` and four keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject, `n` never allow this tool in this session. A rejected tool tells the model `[command rejected by the user]`. The default, `yolo: true`, runs tools without asking. See [How to confirm tool calls](confirm-tool-calls.md). | |
| 30 | 30 | - **Point the model at a file.** `@path` works exactly as in the line mode: existing paths are attached and echoed as `📎` lines. See [Point the model at a file with `@path`](attach-a-file.md). |
| 31 | 31 | - **Keep and resume sessions.** With `sessions.enabled: true`, the header shows the session id being saved; `/sessions` and `/resume` work as in the line mode. See [Keep sessions and resume them](keep-sessions.md). |
| 32 | 32 | - **Scroll the conversation.** PgUp/PgDn page; **Tab** moves the focus to the conversation so ↑/↓, Home and End scroll it; End resumes following the end. **Ctrl-L** clears the displayed conversation without touching the history. The mouse wheel scrolls only after **F2** (see below). |
| @@ -26,7 +26,7 @@ This guide shows how to run mini-me as a single full-screen window — the conve | |||
| 26 | 26 | ||
| 27 | ## Variants | 27 | ## Variants |
| 28 | 28 | ||
| 29 | -- **Ask before every `bash` and file tool.** Set `permissions: ask` in the YAML. The box is replaced by the question `? bash: rm -rf build` and three keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject. A rejected tool tells the model `[command rejected by the user]`. The default, `auto`, runs tools as the line mode always has. | 29 | +- **Ask before every `bash` and file tool.** Set `yolo: false` in the YAML. The box is replaced by the question `? bash: rm -rf build` and four keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject, `n` never allow this tool in this session. A rejected tool tells the model `[command rejected by the user]`. The default, `yolo: true`, runs tools without asking. See [How to confirm tool calls](confirm-tool-calls.md). |
| 30 | - **Point the model at a file.** `@path` works exactly as in the line mode: existing paths are attached and echoed as `📎` lines. See [Point the model at a file with `@path`](attach-a-file.md). | 30 | - **Point the model at a file.** `@path` works exactly as in the line mode: existing paths are attached and echoed as `📎` lines. See [Point the model at a file with `@path`](attach-a-file.md). |
| 31 | - **Keep and resume sessions.** With `sessions.enabled: true`, the header shows the session id being saved; `/sessions` and `/resume` work as in the line mode. See [Keep sessions and resume them](keep-sessions.md). | 31 | - **Keep and resume sessions.** With `sessions.enabled: true`, the header shows the session id being saved; `/sessions` and `/resume` work as in the line mode. See [Keep sessions and resume them](keep-sessions.md). |
| 32 | - **Scroll the conversation.** PgUp/PgDn page; **Tab** moves the focus to the conversation so ↑/↓, Home and End scroll it; End resumes following the end. **Ctrl-L** clears the displayed conversation without touching the history. The mouse wheel scrolls only after **F2** (see below). | 32 | - **Scroll the conversation.** PgUp/PgDn page; **Tab** moves the focus to the conversation so ↑/↓, Home and End scroll it; End resumes following the end. **Ctrl-L** clears the displayed conversation without touching the history. The mouse wheel scrolls only after **F2** (see below). |
modified
docs/en/reference/acp.md +5 -2 | @@ -111,13 +111,16 @@ Clients display the name with a leading slash and send it back as the text of a | ||
| 111 | 111 | |
| 112 | 112 | ## `session/request_permission` requests sent |
| 113 | 113 | |
| 114 | -Sent before `bash`, `read_file`, `write_file` and `edit_file` run; never for `read_skill`. | |
| 114 | +Sent only when `yolo: false` (default `true`: none is sent for the built-in tools), before `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename` and the `plan` actions that change a plan run; never for `read_skill`, `read_file`, `fetch_content` without `output` or the `lsp` navigation actions. MCP tools send one when `yolo: false` or when their `permissions` is `ask`. | |
| 115 | 115 | |
| 116 | 116 | | Option id | Kind | Effect | |
| 117 | 117 | |-----------|------|--------| |
| 118 | 118 | | `allow` | `allow_once` | Runs the tool. | |
| 119 | 119 | | `allow_always` | `allow_always` | Runs the tool and skips the dialog for that tool name for the rest of the session. | |
| 120 | -| `reject` | `reject_once` | Does not run; the model receives `[command rejected by the user]`. | | |
| 120 | +| `reject` | `reject_once` | Does not run; the model receives `[command rejected by the user]` (or `[<tool> rejected by the user]`). | | |
| 121 | +| `never` | `reject_always` | Does not run, and every later call of that tool name in the session is rejected without a dialog. | | |
| 122 | + | |
| 123 | +The remembered answers (`allow_always`, `never`) survive `/new`. | |
| 121 | 124 | |
| 122 | 125 | A transport error or a `cancelled` outcome counts as a rejection. |
| 123 | 126 | |
| @@ -111,13 +111,16 @@ Clients display the name with a leading slash and send it back as the text of a | |||
| 111 | 111 | ||
| 112 | ## `session/request_permission` requests sent | 112 | ## `session/request_permission` requests sent |
| 113 | 113 | ||
| 114 | -Sent before `bash`, `read_file`, `write_file` and `edit_file` run; never for `read_skill`. | 114 | +Sent only when `yolo: false` (default `true`: none is sent for the built-in tools), before `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename` and the `plan` actions that change a plan run; never for `read_skill`, `read_file`, `fetch_content` without `output` or the `lsp` navigation actions. MCP tools send one when `yolo: false` or when their `permissions` is `ask`. |
| 115 | 115 | ||
| 116 | | Option id | Kind | Effect | | 116 | | Option id | Kind | Effect | |
| 117 | |-----------|------|--------| | 117 | |-----------|------|--------| |
| 118 | | `allow` | `allow_once` | Runs the tool. | | 118 | | `allow` | `allow_once` | Runs the tool. | |
| 119 | | `allow_always` | `allow_always` | Runs the tool and skips the dialog for that tool name for the rest of the session. | | 119 | | `allow_always` | `allow_always` | Runs the tool and skips the dialog for that tool name for the rest of the session. | |
| 120 | -| `reject` | `reject_once` | Does not run; the model receives `[command rejected by the user]`. | | 120 | +| `reject` | `reject_once` | Does not run; the model receives `[command rejected by the user]` (or `[<tool> rejected by the user]`). | |
| 121 | +| `never` | `reject_always` | Does not run, and every later call of that tool name in the session is rejected without a dialog. | | ||
| 122 | + | ||
| 123 | +The remembered answers (`allow_always`, `never`) survive `/new`. | ||
| 121 | 124 | ||
| 122 | A transport error or a `cancelled` outcome counts as a rejection. | 125 | A transport error or a `cancelled` outcome counts as a rejection. |
| 123 | 126 | ||
modified
docs/en/reference/cli.md +14 -5 | @@ -60,6 +60,7 @@ The first existing source wins. | ||
| 60 | 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | |
| 61 | 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | |
| 62 | 62 | | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. | |
| 63 | +| `AGENT_YOLO` | Overrides the `yolo` key: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Set to `false` by `/ask` on the agents it starts, so the asking agent decides. | | |
| 63 | 64 | | `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. | |
| 64 | 65 | | `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. | |
| 65 | 66 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | |
| @@ -71,8 +72,8 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | ||
| 71 | 72 | |
| 72 | 73 | | Line | Content | |
| 73 | 74 | |------|---------| |
| 74 | -| `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`). | | |
| 75 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found. | | |
| 75 | +| `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | | |
| 76 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`. | | |
| 76 | 77 | | `Agent ready. Commands: …` | Terminal mode only. | |
| 77 | 78 | | `Shortcut: Ctrl+C …` | Terminal mode only. | |
| 78 | 79 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | |
| @@ -81,7 +82,7 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | ||
| 81 | 82 | |
| 82 | 83 | ``` |
| 83 | 84 | [warning: llamacpp: nothing answers at http://127.0.0.1:8080/v1 — start it: llama-server -m <model.gguf> --jinja --port 8080] |
| 84 | -mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | |
| 85 | +mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | yolo: on | |
| 85 | 86 | ``` |
| 86 | 87 | |
| 87 | 88 | ## ACP mode specifics |
| @@ -92,6 +93,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 92 | 93 | | stderr | Banner, warnings, `[acp] …` trail, SDK logs. | |
| 93 | 94 | | Spinner | Disabled. | |
| 94 | 95 | | Relative `skillsDir` | Resolved relative to the directory of the configuration file, when a file was found; otherwise from the working directory, as in terminal mode. | |
| 96 | +| Permissions | `yolo: true`: no `session/request_permission` is sent for the built-in tools. `yolo: false`: one per call, with four options — `allow` (allow once), `allow_always` (this tool for the session), `reject` (reject once), `never` (`reject_always`: this tool, for the session). MCP tools marked `ask` always send one. | | |
| 97 | + | |
| 98 | +## Line mode specifics | |
| 99 | + | |
| 100 | +| Aspect | Behaviour | | |
| 101 | +|--------|-----------| | |
| 102 | +| Permissions | With `yolo: false` (and for MCP tools marked `ask`), after the tool's `🛠️` line: `🔐 <tool title>`, the legend `[a] allow once · [s] allow <tool> for this session · [r] reject · [n] never <tool>` and ` ? `. The next typed line answers (`a`, `s`, `r`, `n` or the words `once`, `session`, `reject`, `never`); another answer prints the legend again; a line starting with `/` is a command (`/abort`, `/quit`), not an answer. `Ctrl+C` or `/abort` rejects and stops the turn. The same prompt answers the calls of an agent asked with `/ask`. | | |
| 95 | 103 | |
| 96 | 104 | ## Window mode specifics |
| 97 | 105 | |
| @@ -108,7 +116,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 108 | 116 | | Pickers | `/` completes a command, `@` a path under the working directory, `$` a skill of the catalogue — the last two anywhere in a sentence, keeping their prefix. See [Slash commands](slash-commands.md#the-three-pickers). | |
| 109 | 117 | | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. | |
| 110 | 118 | | Errors | One line `[error: <explanation>]`; a cancelled turn shows `🛑 Generation aborted.`; each turn ends with `⚙ N command(s) [· 📝 N file op(s)] [· 📖 N skill(s)]`. | |
| 111 | -| Permissions | With `permissions: ask`, the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject` until answered; a turn cancelled while asking rejects. | | |
| 119 | +| Permissions | With `yolo: false` (and for MCP tools marked `ask`), the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject [n] never <tool>` until answered (`Esc` = `r`); a turn cancelled while asking rejects. | | |
| 112 | 120 | | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). | |
| 113 | 121 | | Mouse | Not captured at start-up, so the terminal's click-and-drag selection works. `F2` captures it (wheel scrolling; `Shift`+drag selects) and releases it again. | |
| 114 | 122 | |
| @@ -122,6 +130,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 122 | 130 | | Session | Not saved; no session directory used. | |
| 123 | 131 | | Config file | Can appear before or after the `-prompt`/`-p` flag: `mm -p "prompt" config.yaml` and `mm config.yaml -p "prompt"` both work. | |
| 124 | 132 | | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. | |
| 133 | +| Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. | | |
| 125 | 134 | |
| 126 | 135 | ## Ask mode specifics |
| 127 | 136 | |
| @@ -132,7 +141,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 132 | 141 | | Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. | |
| 133 | 142 | | stdout | The agent's answer, and nothing else, followed by a newline. | |
| 134 | 143 | | stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. | |
| 135 | -| The agent's tool calls | Allowed, as in the line mode. | | |
| 144 | +| The agent's tool calls | Decided by this configuration's `yolo`: `true` = allowed; `false` = rejected, with the same stderr line as in prompt mode (stdin carried the question, nobody can answer). | | |
| 136 | 145 | | Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. | |
| 137 | 146 | | Session | None: neither this process nor the agent writes session files. | |
| 138 | 147 | |
| @@ -60,6 +60,7 @@ The first existing source wins. | |||
| 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | | 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | |
| 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | | 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | |
| 62 | | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. | | 62 | | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. | |
| 63 | +| `AGENT_YOLO` | Overrides the `yolo` key: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Set to `false` by `/ask` on the agents it starts, so the asking agent decides. | | ||
| 63 | | `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. | | 64 | | `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. | |
| 64 | | `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. | | 65 | | `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. | |
| 65 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | | 66 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | |
| @@ -71,8 +72,8 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | |||
| 71 | 72 | ||
| 72 | | Line | Content | | 73 | | Line | Content | |
| 73 | |------|---------| | 74 | |------|---------| |
| 74 | -| `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`). | | 75 | +| `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | |
| 75 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found. | | 76 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`. | |
| 76 | | `Agent ready. Commands: …` | Terminal mode only. | | 77 | | `Agent ready. Commands: …` | Terminal mode only. | |
| 77 | | `Shortcut: Ctrl+C …` | Terminal mode only. | | 78 | | `Shortcut: Ctrl+C …` | Terminal mode only. | |
| 78 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | | 79 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | |
| @@ -81,7 +82,7 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | |||
| 81 | 82 | ||
| 82 | ``` | 83 | ``` |
| 83 | [warning: llamacpp: nothing answers at http://127.0.0.1:8080/v1 — start it: llama-server -m <model.gguf> --jinja --port 8080] | 84 | [warning: llamacpp: nothing answers at http://127.0.0.1:8080/v1 — start it: llama-server -m <model.gguf> --jinja --port 8080] |
| 84 | -mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | 85 | +mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | yolo: on |
| 85 | ``` | 86 | ``` |
| 86 | 87 | ||
| 87 | ## ACP mode specifics | 88 | ## ACP mode specifics |
| @@ -92,6 +93,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 92 | | stderr | Banner, warnings, `[acp] …` trail, SDK logs. | | 93 | | stderr | Banner, warnings, `[acp] …` trail, SDK logs. | |
| 93 | | Spinner | Disabled. | | 94 | | Spinner | Disabled. | |
| 94 | | Relative `skillsDir` | Resolved relative to the directory of the configuration file, when a file was found; otherwise from the working directory, as in terminal mode. | | 95 | | Relative `skillsDir` | Resolved relative to the directory of the configuration file, when a file was found; otherwise from the working directory, as in terminal mode. | |
| 96 | +| Permissions | `yolo: true`: no `session/request_permission` is sent for the built-in tools. `yolo: false`: one per call, with four options — `allow` (allow once), `allow_always` (this tool for the session), `reject` (reject once), `never` (`reject_always`: this tool, for the session). MCP tools marked `ask` always send one. | | ||
| 97 | + | ||
| 98 | +## Line mode specifics | ||
| 99 | + | ||
| 100 | +| Aspect | Behaviour | | ||
| 101 | +|--------|-----------| | ||
| 102 | +| Permissions | With `yolo: false` (and for MCP tools marked `ask`), after the tool's `🛠️` line: `🔐 <tool title>`, the legend `[a] allow once · [s] allow <tool> for this session · [r] reject · [n] never <tool>` and ` ? `. The next typed line answers (`a`, `s`, `r`, `n` or the words `once`, `session`, `reject`, `never`); another answer prints the legend again; a line starting with `/` is a command (`/abort`, `/quit`), not an answer. `Ctrl+C` or `/abort` rejects and stops the turn. The same prompt answers the calls of an agent asked with `/ask`. | | ||
| 95 | 103 | ||
| 96 | ## Window mode specifics | 104 | ## Window mode specifics |
| 97 | 105 | ||
| @@ -108,7 +116,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 108 | | Pickers | `/` completes a command, `@` a path under the working directory, `$` a skill of the catalogue — the last two anywhere in a sentence, keeping their prefix. See [Slash commands](slash-commands.md#the-three-pickers). | | 116 | | Pickers | `/` completes a command, `@` a path under the working directory, `$` a skill of the catalogue — the last two anywhere in a sentence, keeping their prefix. See [Slash commands](slash-commands.md#the-three-pickers). | |
| 109 | | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. | | 117 | | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. | |
| 110 | | Errors | One line `[error: <explanation>]`; a cancelled turn shows `🛑 Generation aborted.`; each turn ends with `⚙ N command(s) [· 📝 N file op(s)] [· 📖 N skill(s)]`. | | 118 | | Errors | One line `[error: <explanation>]`; a cancelled turn shows `🛑 Generation aborted.`; each turn ends with `⚙ N command(s) [· 📝 N file op(s)] [· 📖 N skill(s)]`. | |
| 111 | -| Permissions | With `permissions: ask`, the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject` until answered; a turn cancelled while asking rejects. | | 119 | +| Permissions | With `yolo: false` (and for MCP tools marked `ask`), the box shows `? <tool title>` with `[a] allow once [s] allow <tool> for this session [r] reject [n] never <tool>` until answered (`Esc` = `r`); a turn cancelled while asking rejects. | |
| 112 | | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). | | 120 | | Keys | See [Slash commands — window keys](slash-commands.md#window-mode-keys). | |
| 113 | | Mouse | Not captured at start-up, so the terminal's click-and-drag selection works. `F2` captures it (wheel scrolling; `Shift`+drag selects) and releases it again. | | 121 | | Mouse | Not captured at start-up, so the terminal's click-and-drag selection works. `F2` captures it (wheel scrolling; `Shift`+drag selects) and releases it again. | |
| 114 | 122 | ||
| @@ -122,6 +130,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 122 | | Session | Not saved; no session directory used. | | 130 | | Session | Not saved; no session directory used. | |
| 123 | | Config file | Can appear before or after the `-prompt`/`-p` flag: `mm -p "prompt" config.yaml` and `mm config.yaml -p "prompt"` both work. | | 131 | | Config file | Can appear before or after the `-prompt`/`-p` flag: `mm -p "prompt" config.yaml` and `mm config.yaml -p "prompt"` both work. | |
| 124 | | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. | | 132 | | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. | |
| 133 | +| Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. | | ||
| 125 | 134 | ||
| 126 | ## Ask mode specifics | 135 | ## Ask mode specifics |
| 127 | 136 | ||
| @@ -132,7 +141,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 132 | | Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. | | 141 | | Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. | |
| 133 | | stdout | The agent's answer, and nothing else, followed by a newline. | | 142 | | stdout | The agent's answer, and nothing else, followed by a newline. | |
| 134 | | stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. | | 143 | | stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. | |
| 135 | -| The agent's tool calls | Allowed, as in the line mode. | | 144 | +| The agent's tool calls | Decided by this configuration's `yolo`: `true` = allowed; `false` = rejected, with the same stderr line as in prompt mode (stdin carried the question, nobody can answer). | |
| 136 | | Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. | | 145 | | Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. | |
| 137 | | Session | None: neither this process nor the agent writes session files. | | 146 | | Session | None: neither this process nor the agent writes session files. | |
| 138 | 147 | ||
modified
docs/en/reference/configuration.md +5 -3 | @@ -31,7 +31,8 @@ | ||
| 31 | 31 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | |
| 32 | 32 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 33 | 33 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | |
| 34 | -| `permissions` | string | `auto` | `ask`: in the window mode, `bash`, `write_file` and `edit_file` wait for the user's answer (`a`/`s`/`r`) before running. `auto`: tools run at once. The line mode and the ACP façade do not read it (ACP has its own dialog). Must be `auto` or `ask`. | | |
| 34 | +| `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | | |
| 35 | +| `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. | | |
| 35 | 36 | | `mmConfigs` | map name → path | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)). A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. | |
| 36 | 37 | |
| 37 | 38 | ## `context` keys |
| @@ -118,14 +119,15 @@ Reported as `[config error: <path>: <message>]`, exit code 1. | ||
| 118 | 119 | | `lsp.servers[N].extensions must list at least one extension` | a server without `extensions` | |
| 119 | 120 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot | |
| 120 | 121 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension | |
| 121 | -| `permissions must be "auto" or "ask"` | any other value | | |
| 122 | +| `permissions must be "auto" or "ask"` | any other value of the deprecated key | | |
| 123 | +| `AGENT_YOLO="maybe": must be true or false` | an `AGENT_YOLO` value `strconv.ParseBool` refuses (accepted: `1`, `t`, `true`, `0`, `f`, `false`, any case) | | |
| 122 | 124 | | `bashTimeout must be > 0` | zero or negative | |
| 123 | 125 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | |
| 124 | 126 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | |
| 125 | 127 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""` | |
| 126 | 128 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | |
| 127 | 129 | |
| 128 | -A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`. | |
| 130 | +A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`). | |
| 129 | 131 | |
| 130 | 132 | ## Shipped files |
| 131 | 133 | |
| @@ -31,7 +31,8 @@ | |||
| 31 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | | 31 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | |
| 32 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | | 32 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 33 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | | 33 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | |
| 34 | -| `permissions` | string | `auto` | `ask`: in the window mode, `bash`, `write_file` and `edit_file` wait for the user's answer (`a`/`s`/`r`) before running. `auto`: tools run at once. The line mode and the ACP façade do not read it (ACP has its own dialog). Must be `auto` or `ask`. | | 34 | +| `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | |
| 35 | +| `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. | | ||
| 35 | | `mmConfigs` | map name → path | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)). A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. | | 36 | | `mmConfigs` | map name → path | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)). A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. | |
| 36 | 37 | ||
| 37 | ## `context` keys | 38 | ## `context` keys |
| @@ -118,14 +119,15 @@ Reported as `[config error: <path>: <message>]`, exit code 1. | |||
| 118 | | `lsp.servers[N].extensions must list at least one extension` | a server without `extensions` | | 119 | | `lsp.servers[N].extensions must list at least one extension` | a server without `extensions` | |
| 119 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot | | 120 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot | |
| 120 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension | | 121 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension | |
| 121 | -| `permissions must be "auto" or "ask"` | any other value | | 122 | +| `permissions must be "auto" or "ask"` | any other value of the deprecated key | |
| 123 | +| `AGENT_YOLO="maybe": must be true or false` | an `AGENT_YOLO` value `strconv.ParseBool` refuses (accepted: `1`, `t`, `true`, `0`, `f`, `false`, any case) | | ||
| 122 | | `bashTimeout must be > 0` | zero or negative | | 124 | | `bashTimeout must be > 0` | zero or negative | |
| 123 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | | 125 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | |
| 124 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | | 126 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | |
| 125 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""` | | 127 | | `mmConfigs.<name>: the path must not be empty` | `<name>: ""` | |
| 126 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | | 128 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | |
| 127 | 129 | ||
| 128 | -A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`. | 130 | +A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`). |
| 129 | 131 | ||
| 130 | ## Shipped files | 132 | ## Shipped files |
| 131 | 133 | ||
modified
docs/en/reference/mcp-configuration.md +2 -1 | @@ -27,7 +27,8 @@ | ||
| 27 | 27 | | `sessions` | map | see below | Persistent, per-project sessions. | |
| 28 | 28 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 29 | 29 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | |
| 30 | -| `permissions` | string | `auto` | `ask`: in the window mode, `bash`, `write_file` and `edit_file` wait for the user's answer (`a`/`s`/`r`) before running. `auto`: tools run at once. The line mode and the ACP façade do not read it (ACP has its own dialog). Must be `auto` or `ask`. | | |
| 30 | +| `yolo` | bool | `true` | `false`: every built-in tool call that changes something, and every MCP tool call — even on a server set to `permissions: auto` —, waits for the user's answer. `true`: built-in tools run unasked; each MCP server keeps its own `permissions`. See [configuration](configuration.md). | | |
| 31 | +| `permissions` | string | absent | Deprecated top-level key, replaced by `yolo` (`ask` = `yolo: false`). Not the same as `mcp.permissions`. | | |
| 31 | 32 | |
| 32 | 33 | ## `context` keys |
| 33 | 34 | |
| @@ -27,7 +27,8 @@ | |||
| 27 | | `sessions` | map | see below | Persistent, per-project sessions. | | 27 | | `sessions` | map | see below | Persistent, per-project sessions. | |
| 28 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | | 28 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 29 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | | 29 | | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. | |
| 30 | -| `permissions` | string | `auto` | `ask`: in the window mode, `bash`, `write_file` and `edit_file` wait for the user's answer (`a`/`s`/`r`) before running. `auto`: tools run at once. The line mode and the ACP façade do not read it (ACP has its own dialog). Must be `auto` or `ask`. | | 30 | +| `yolo` | bool | `true` | `false`: every built-in tool call that changes something, and every MCP tool call — even on a server set to `permissions: auto` —, waits for the user's answer. `true`: built-in tools run unasked; each MCP server keeps its own `permissions`. See [configuration](configuration.md). | |
| 31 | +| `permissions` | string | absent | Deprecated top-level key, replaced by `yolo` (`ask` = `yolo: false`). Not the same as `mcp.permissions`. | | ||
| 31 | 32 | ||
| 32 | ## `context` keys | 33 | ## `context` keys |
| 33 | 34 | ||
modified
docs/en/reference/slash-commands.md +1 -1 | @@ -104,7 +104,7 @@ The keys: | ||
| 104 | 104 | | `F1` | Same as `/help`. | |
| 105 | 105 | | `F2` | Toggles mouse capture. Off (default): the terminal's own click-and-drag selection works; the status bar says `F2 mouse: off`. On: the wheel scrolls, `Shift`+drag selects, the status bar says `mouse: on (Shift+drag selects)`. | |
| 106 | 106 | | `Ctrl-Y` | Same as `/copy`: the last answer to the clipboard. | |
| 107 | -| `a` `s` `r` | Answer a pending permission: allow once, allow this tool for the session, reject. | | |
| 107 | +| `a` `s` `r` `n` | Answer a pending permission (`yolo: false`, or an MCP tool marked `ask`): allow once, allow this tool for the session, reject this call (`Esc` too), never allow this tool in this session. | | |
| 108 | 108 | | Mouse wheel | Scrolls the conversation — only while the mouse is captured (F2). | |
| 109 | 109 | | Bracketed paste | Inserted as one block; its line breaks become newlines, never a send. Longer than 3 lines or 200 characters: collapsed into one `[Pasted #n · L lines · C chars]` token that edits as a single character; Enter sends the full text, the conversation keeps the token. `@path` inside a paste is not looked up. | |
| 110 | 110 | |
| @@ -104,7 +104,7 @@ The keys: | |||
| 104 | | `F1` | Same as `/help`. | | 104 | | `F1` | Same as `/help`. | |
| 105 | | `F2` | Toggles mouse capture. Off (default): the terminal's own click-and-drag selection works; the status bar says `F2 mouse: off`. On: the wheel scrolls, `Shift`+drag selects, the status bar says `mouse: on (Shift+drag selects)`. | | 105 | | `F2` | Toggles mouse capture. Off (default): the terminal's own click-and-drag selection works; the status bar says `F2 mouse: off`. On: the wheel scrolls, `Shift`+drag selects, the status bar says `mouse: on (Shift+drag selects)`. | |
| 106 | | `Ctrl-Y` | Same as `/copy`: the last answer to the clipboard. | | 106 | | `Ctrl-Y` | Same as `/copy`: the last answer to the clipboard. | |
| 107 | -| `a` `s` `r` | Answer a pending permission: allow once, allow this tool for the session, reject. | | 107 | +| `a` `s` `r` `n` | Answer a pending permission (`yolo: false`, or an MCP tool marked `ask`): allow once, allow this tool for the session, reject this call (`Esc` too), never allow this tool in this session. | |
| 108 | | Mouse wheel | Scrolls the conversation — only while the mouse is captured (F2). | | 108 | | Mouse wheel | Scrolls the conversation — only while the mouse is captured (F2). | |
| 109 | | Bracketed paste | Inserted as one block; its line breaks become newlines, never a send. Longer than 3 lines or 200 characters: collapsed into one `[Pasted #n · L lines · C chars]` token that edits as a single character; Enter sends the full text, the conversation keeps the token. `@path` inside a paste is not looked up. | | 109 | | Bracketed paste | Inserted as one block; its line breaks become newlines, never a send. Longer than 3 lines or 200 characters: collapsed into one `[Pasted #n · L lines · C chars]` token that edits as a single character; Enter sends the full text, the conversation keeps the token. `@path` inside a paste is not looked up. | |
| 110 | 110 | ||
modified
docs/en/reference/tools.md +4 -2 | @@ -2,6 +2,8 @@ | ||
| 2 | 2 | |
| 3 | 3 | > Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends `[LOOP_DETECTED: …]` to the output. |
| 4 | 4 | |
| 5 | +> **Permissions.** "Permission requested" below applies when `yolo: false` (see [configuration](configuration.md)), in every front end: the line mode's `🔐` prompt, the window's box, the editor's dialog. With `yolo: true` (the default) the same calls run without asking. In `-p` and `mm --ask` a call that would ask is rejected. A refusal returns `[<tool> rejected by the user]` (`[command rejected by the user]` for `bash`) to the model. | |
| 6 | + | |
| 5 | 7 | ## `bash` |
| 6 | 8 | |
| 7 | 9 | Declared when `bashTool: true` (the default). |
| @@ -20,9 +22,9 @@ Declared when `bashTool: true` (the default). | ||
| 20 | 22 | | Very large output | Read back from both ends (1 MiB each) before `maxOutput` truncation, with `[... output too large to read back in full ...]` between them. | |
| 21 | 23 | | Output | stdout and stderr merged, truncated to `maxOutput` characters. | |
| 22 | 24 | | Non-zero exit | Output ends with `[exit code: exit status N]`. | |
| 23 | -| Terminal display | `🛠️ bash: <command>` before running, then up to `previewLines` lines dimmed and indented, or `(no output)`. | | |
| 25 | +| Terminal display | `🛠️ bash: <command>` before running (then the `🔐` prompt when permission is requested), then up to `previewLines` lines dimmed and indented, or `(no output)`. | | |
| 24 | 26 | | ACP | Tool call of kind `execute`; permission requested before running; a refusal returns `[command rejected by the user]` with status `failed`. | |
| 25 | -| Window mode | One `‣ bash <command>` line with status, time and folded output. With `permissions: ask`, waits for `a`/`s`/`r`; a refusal returns the same text as in ACP. | | |
| 27 | +| Window mode | One `‣ bash <command>` line with status, time and folded output. Permission requested: waits for `a`/`s`/`r`/`n`; a refusal returns the same text as in ACP. | | |
| 26 | 28 | |
| 27 | 29 | ## `read_skill` |
| 28 | 30 | |
| @@ -2,6 +2,8 @@ | |||
| 2 | 2 | ||
| 3 | > Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends `[LOOP_DETECTED: …]` to the output. | 3 | > Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends `[LOOP_DETECTED: …]` to the output. |
| 4 | 4 | ||
| 5 | +> **Permissions.** "Permission requested" below applies when `yolo: false` (see [configuration](configuration.md)), in every front end: the line mode's `🔐` prompt, the window's box, the editor's dialog. With `yolo: true` (the default) the same calls run without asking. In `-p` and `mm --ask` a call that would ask is rejected. A refusal returns `[<tool> rejected by the user]` (`[command rejected by the user]` for `bash`) to the model. | ||
| 6 | + | ||
| 5 | ## `bash` | 7 | ## `bash` |
| 6 | 8 | ||
| 7 | Declared when `bashTool: true` (the default). | 9 | Declared when `bashTool: true` (the default). |
| @@ -20,9 +22,9 @@ Declared when `bashTool: true` (the default). | |||
| 20 | | Very large output | Read back from both ends (1 MiB each) before `maxOutput` truncation, with `[... output too large to read back in full ...]` between them. | | 22 | | Very large output | Read back from both ends (1 MiB each) before `maxOutput` truncation, with `[... output too large to read back in full ...]` between them. | |
| 21 | | Output | stdout and stderr merged, truncated to `maxOutput` characters. | | 23 | | Output | stdout and stderr merged, truncated to `maxOutput` characters. | |
| 22 | | Non-zero exit | Output ends with `[exit code: exit status N]`. | | 24 | | Non-zero exit | Output ends with `[exit code: exit status N]`. | |
| 23 | -| Terminal display | `🛠️ bash: <command>` before running, then up to `previewLines` lines dimmed and indented, or `(no output)`. | | 25 | +| Terminal display | `🛠️ bash: <command>` before running (then the `🔐` prompt when permission is requested), then up to `previewLines` lines dimmed and indented, or `(no output)`. | |
| 24 | | ACP | Tool call of kind `execute`; permission requested before running; a refusal returns `[command rejected by the user]` with status `failed`. | | 26 | | ACP | Tool call of kind `execute`; permission requested before running; a refusal returns `[command rejected by the user]` with status `failed`. | |
| 25 | -| Window mode | One `‣ bash <command>` line with status, time and folded output. With `permissions: ask`, waits for `a`/`s`/`r`; a refusal returns the same text as in ACP. | | 27 | +| Window mode | One `‣ bash <command>` line with status, time and folded output. Permission requested: waits for `a`/`s`/`r`/`n`; a refusal returns the same text as in ACP. | |
| 26 | 28 | ||
| 27 | ## `read_skill` | 29 | ## `read_skill` |
| 28 | 30 | ||
modified
docs/fr/README.md +2 -0 | @@ -16,6 +16,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 16 | 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` |
| 17 | 17 | - [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo |
| 18 | 18 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) |
| 19 | +- [Confirmer les appels d'outils avant leur exécution](how-to/confirm-tool-calls.md) — `yolo: false`, les quatre réponses, `AGENT_YOLO`, qui décide avec `mmConfigs` | |
| 19 | 20 | - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le template et les deux kits sous `docker-sbx/`, mode fenêtre et éditeur |
| 20 | 21 | - [Faire tourner mini-me avec les kits sandbox v3](how-to/run-with-sandbox-kits-v3.md) — les deux mêmes kits pour la spécification de kit v3, sous `docker-sbx-v3/` |
| 21 | 22 | - [Ajouter un skill](how-to/add-a-skill.md) |
| @@ -58,6 +59,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 58 | 59 | - [L'outil `lsp`](explanation/lsp-tool.md) — un seul outil avec un champ `action`, un champ `symbol` plutôt que des colonnes, des fichiers synchronisés depuis le disque, un renommage soumis à permission |
| 59 | 60 | - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues |
| 60 | 61 | - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie |
| 62 | +- [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide | |
| 61 | 63 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, la limite de profondeur |
| 62 | 64 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 63 | 65 | |
| @@ -16,6 +16,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` | 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` |
| 17 | - [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo | 17 | - [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo |
| 18 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) | 18 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) |
| 19 | +- [Confirmer les appels d'outils avant leur exécution](how-to/confirm-tool-calls.md) — `yolo: false`, les quatre réponses, `AGENT_YOLO`, qui décide avec `mmConfigs` | ||
| 19 | - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le template et les deux kits sous `docker-sbx/`, mode fenêtre et éditeur | 20 | - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le template et les deux kits sous `docker-sbx/`, mode fenêtre et éditeur |
| 20 | - [Faire tourner mini-me avec les kits sandbox v3](how-to/run-with-sandbox-kits-v3.md) — les deux mêmes kits pour la spécification de kit v3, sous `docker-sbx-v3/` | 21 | - [Faire tourner mini-me avec les kits sandbox v3](how-to/run-with-sandbox-kits-v3.md) — les deux mêmes kits pour la spécification de kit v3, sous `docker-sbx-v3/` |
| 21 | - [Ajouter un skill](how-to/add-a-skill.md) | 22 | - [Ajouter un skill](how-to/add-a-skill.md) |
| @@ -58,6 +59,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 58 | - [L'outil `lsp`](explanation/lsp-tool.md) — un seul outil avec un champ `action`, un champ `symbol` plutôt que des colonnes, des fichiers synchronisés depuis le disque, un renommage soumis à permission | 59 | - [L'outil `lsp`](explanation/lsp-tool.md) — un seul outil avec un champ `action`, un champ `symbol` plutôt que des colonnes, des fichiers synchronisés depuis le disque, un renommage soumis à permission |
| 59 | - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues | 60 | - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues |
| 60 | - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie | 61 | - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie |
| 62 | +- [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide | ||
| 61 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, la limite de profondeur | 63 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, la limite de profondeur |
| 62 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) | 64 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 63 | 65 | ||
modified
docs/fr/explanation/architecture.md +2 -1 | @@ -24,6 +24,7 @@ Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages | ||
| 24 | 24 | | `version` | Quel `mm` c'est : le tag estampillé à la construction, ou le commit que Go embarque sinon. Lu par `--version`, par la bannière de démarrage et par la barre d'état de la fenêtre. | |
| 25 | 25 | | `spinner` | L'indicateur « toujours en cours » sur un terminal. | |
| 26 | 26 | | `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. | |
| 27 | +| `consent` | Si un appel d'outil peut s'exécuter : l'interrupteur `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient par outil (`Policy`), et le point d'accroche par lequel le mode ligne demande (`SetPrompter`). La fenêtre, la façade éditeur et `/ask` dessinent la question à leur manière et partagent ces règles. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). | | |
| 27 | 28 | | `agent` | Le REPL du terminal. | |
| 28 | 29 | | `acp` | La façade Agent Client Protocol. | |
| 29 | 30 | | `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. | |
| @@ -38,7 +39,7 @@ Un diagramme draw.io de ces paquets et de leurs dépendances est conservé dans | ||
| 38 | 39 | |
| 39 | 40 | **La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien. |
| 40 | 41 | |
| 41 | -**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️` et personne ne demande de permission. En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `permissions: ask` est la version terminal du dialogue de l'éditeur. | |
| 42 | +**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️`, et ne demandent la permission, par une invite `🔐` d'une ligne, que quand `yolo` est désactivé (voir [Le mode yolo et les permissions](yolo-and-permissions.md)). En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `yolo: false` est la version fenêtre du dialogue de l'éditeur. | |
| 42 | 43 | |
| 43 | 44 | **Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois. |
| 44 | 45 | |
| @@ -24,6 +24,7 @@ Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages | |||
| 24 | | `version` | Quel `mm` c'est : le tag estampillé à la construction, ou le commit que Go embarque sinon. Lu par `--version`, par la bannière de démarrage et par la barre d'état de la fenêtre. | | 24 | | `version` | Quel `mm` c'est : le tag estampillé à la construction, ou le commit que Go embarque sinon. Lu par `--version`, par la bannière de démarrage et par la barre d'état de la fenêtre. | |
| 25 | | `spinner` | L'indicateur « toujours en cours » sur un terminal. | | 25 | | `spinner` | L'indicateur « toujours en cours » sur un terminal. | |
| 26 | | `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. | | 26 | | `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. | |
| 27 | +| `consent` | Si un appel d'outil peut s'exécuter : l'interrupteur `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient par outil (`Policy`), et le point d'accroche par lequel le mode ligne demande (`SetPrompter`). La fenêtre, la façade éditeur et `/ask` dessinent la question à leur manière et partagent ces règles. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). | | ||
| 27 | | `agent` | Le REPL du terminal. | | 28 | | `agent` | Le REPL du terminal. | |
| 28 | | `acp` | La façade Agent Client Protocol. | | 29 | | `acp` | La façade Agent Client Protocol. | |
| 29 | | `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. | | 30 | | `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. | |
| @@ -38,7 +39,7 @@ Un diagramme draw.io de ces paquets et de leurs dépendances est conservé dans | |||
| 38 | 39 | ||
| 39 | **La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien. | 40 | **La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien. |
| 40 | 41 | ||
| 41 | -**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️` et personne ne demande de permission. En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `permissions: ask` est la version terminal du dialogue de l'éditeur. | 42 | +**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️`, et ne demandent la permission, par une invite `🔐` d'une ligne, que quand `yolo` est désactivé (voir [Le mode yolo et les permissions](yolo-and-permissions.md)). En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `yolo: false` est la version fenêtre du dialogue de l'éditeur. |
| 42 | 43 | ||
| 43 | **Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois. | 44 | **Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois. |
| 44 | 45 | ||
modified
docs/fr/explanation/asking-another-agent.md +2 -2 | @@ -10,7 +10,7 @@ | ||
| 10 | 10 | |
| 11 | 11 | **Les deux parlent ACP.** mm parle déjà l'Agent Client Protocol comme serveur, pour les éditeurs. Interroger bob fait de l'agent qui pose la question un *client* ACP de son enfant. Le protocole apporte gratuitement ce que lire un terminal n'aurait jamais donné : la réponse de bob sous forme de texte, ses appels d'outils comme des événements structurés, ses demandes de permission comme des requêtes auxquelles on peut répondre, et une façon standard d'annuler. Le serveur ACP de mm n'appelle que deux méthodes du client — les mises à jour de session et les demandes de permission — donc le côté client est petit ; les requêtes de fichiers et de terminal sont refusées comme absentes. |
| 12 | 12 | |
| 13 | -**Les permissions de bob, c'est vous qui les donnez.** Quand bob veut lancer une commande, la demande est transmise à la façade que vous utilisez : dans un éditeur, sa propre boîte de dialogue ; dans la fenêtre, le réglage `permissions` et sa question dans la zone de saisie ; en mode ligne, rien — exactement comme pour les outils de votre propre agent à cet endroit. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. | |
| 13 | +**Les permissions de bob, c'est vous qui les donnez.** Bob est lancé avec `AGENT_YOLO=false` : chaque appel qu'il filtrerait devient une demande de permission adressée à votre agent, et c'est VOTRE `yolo` qui décide : activé, l'appel s'exécute ; désactivé, la question va à la façade que vous utilisez — le dialogue de l'éditeur, la zone de la fenêtre, l'invite `🔐` du mode ligne — exactement comme pour les outils de votre propre agent à cet endroit. Le `yolo` du fichier de bob ne compte pas tant qu'il vous répond. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). | |
| 14 | 14 | |
| 15 | 15 | **La réponse est une paire de messages.** Elle entre dans votre historique comme un message utilisateur (`[asked bob (fournisseur/modèle): …]`) suivi d'un message de l'assistant (`[bob's answer]` et le texte). Un seul message aurait été plus simple, mais votre question suivante est aussi un message utilisateur, et plusieurs templates de chat (ceux de Gemma, de Mistral) refusent deux messages utilisateur à la suite. Les marques disent qui a dit quoi, pour que votre modèle ne prenne pas la réponse de bob pour la sienne. La réponse est plafonnée comme la sortie d'un outil, à `maxOutput` caractères : elle atterrit dans la fenêtre de votre modèle. |
| 16 | 16 | |
| @@ -22,7 +22,7 @@ | ||
| 22 | 22 | |
| 23 | 23 | **Les chaînes s'arrêtent.** `/ask` est une commande tapée par une personne ; aucun modèle ne peut l'appeler, donc aucun agent ne boucle tout seul. Une personne pourrait quand même taper `/ask` dans un agent lui-même interrogé, alors chaque enfant connaît sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne. |
| 24 | 24 | |
| 25 | -**La même mécanique répond aux scripts.** `mm --ask <agent>` et `mm --ask-with-context <agent>` sont les commandes slash de ce qui ne sait que lancer une commande — une skill, un script. Ils démarrent le même enfant en ACP, avec la même protection de l'environnement et la même limite de profondeur, mais la réponse va sur stdout et tout le reste sur stderr, pour que l'appelant récupère la réponse seule. mm exporte `MM_BIN` et `MM_CONFIG` vers les commandes de son outil `bash`, donc une skill appelle « ce mm, avec cette configuration » sans connaître aucun des deux chemins. Deux choses diffèrent des commandes slash, parce que l'appelant est une commande et non une personne. Les appels d'outils de l'agent sont autorisés, comme en mode ligne : la personne a déjà validé la commande `mm --ask` elle-même. Et `mm --ask` surveille son processus parent : l'outil `bash` tue `bash` quand son budget est écoulé, pas les enfants de bash, et une demande orpheline laissait son agent tourner — et écrire des fichiers — alors que plus personne n'attendait. | |
| 25 | +**La même mécanique répond aux scripts.** `mm --ask <agent>` et `mm --ask-with-context <agent>` sont les commandes slash de ce qui ne sait que lancer une commande — une skill, un script. Ils démarrent le même enfant en ACP, avec la même protection de l'environnement et la même limite de profondeur, mais la réponse va sur stdout et tout le reste sur stderr, pour que l'appelant récupère la réponse seule. mm exporte `MM_BIN` et `MM_CONFIG` vers les commandes de son outil `bash`, donc une skill appelle « ce mm, avec cette configuration » sans connaître aucun des deux chemins. Deux choses diffèrent des commandes slash, parce que l'appelant est une commande et non une personne. Les appels d'outils de l'agent suivent le `yolo` de la configuration appelante : autorisés quand il est activé — la personne a déjà validé la commande `mm --ask` elle-même —, refusés quand il est désactivé, puisque stdin portait la question et que personne n'est là pour répondre. Et `mm --ask` surveille son processus parent : l'outil `bash` tue `bash` quand son budget est écoulé, pas les enfants de bash, et une demande orpheline laissait son agent tourner — et écrire des fichiers — alors que plus personne n'attendait. | |
| 26 | 26 | |
| 27 | 27 | **Le temps est le point faible.** Un modèle local peut mettre une minute à répondre, et une commande `bash` disposait de 30 secondes ; `bashTimeout` règle désormais ce budget. Le watchdog de l'agent qui pose la question continue aussi de compter pendant qu'une commande tourne (ticket 57), il faut donc le relever aussi tant que ce ticket n'est pas corrigé. |
| 28 | 28 | |
| @@ -10,7 +10,7 @@ | |||
| 10 | 10 | ||
| 11 | **Les deux parlent ACP.** mm parle déjà l'Agent Client Protocol comme serveur, pour les éditeurs. Interroger bob fait de l'agent qui pose la question un *client* ACP de son enfant. Le protocole apporte gratuitement ce que lire un terminal n'aurait jamais donné : la réponse de bob sous forme de texte, ses appels d'outils comme des événements structurés, ses demandes de permission comme des requêtes auxquelles on peut répondre, et une façon standard d'annuler. Le serveur ACP de mm n'appelle que deux méthodes du client — les mises à jour de session et les demandes de permission — donc le côté client est petit ; les requêtes de fichiers et de terminal sont refusées comme absentes. | 11 | **Les deux parlent ACP.** mm parle déjà l'Agent Client Protocol comme serveur, pour les éditeurs. Interroger bob fait de l'agent qui pose la question un *client* ACP de son enfant. Le protocole apporte gratuitement ce que lire un terminal n'aurait jamais donné : la réponse de bob sous forme de texte, ses appels d'outils comme des événements structurés, ses demandes de permission comme des requêtes auxquelles on peut répondre, et une façon standard d'annuler. Le serveur ACP de mm n'appelle que deux méthodes du client — les mises à jour de session et les demandes de permission — donc le côté client est petit ; les requêtes de fichiers et de terminal sont refusées comme absentes. |
| 12 | 12 | ||
| 13 | -**Les permissions de bob, c'est vous qui les donnez.** Quand bob veut lancer une commande, la demande est transmise à la façade que vous utilisez : dans un éditeur, sa propre boîte de dialogue ; dans la fenêtre, le réglage `permissions` et sa question dans la zone de saisie ; en mode ligne, rien — exactement comme pour les outils de votre propre agent à cet endroit. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. | 13 | +**Les permissions de bob, c'est vous qui les donnez.** Bob est lancé avec `AGENT_YOLO=false` : chaque appel qu'il filtrerait devient une demande de permission adressée à votre agent, et c'est VOTRE `yolo` qui décide : activé, l'appel s'exécute ; désactivé, la question va à la façade que vous utilisez — le dialogue de l'éditeur, la zone de la fenêtre, l'invite `🔐` du mode ligne — exactement comme pour les outils de votre propre agent à cet endroit. Le `yolo` du fichier de bob ne compte pas tant qu'il vous répond. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. Voir [Le mode yolo et les permissions](yolo-and-permissions.md). |
| 14 | 14 | ||
| 15 | **La réponse est une paire de messages.** Elle entre dans votre historique comme un message utilisateur (`[asked bob (fournisseur/modèle): …]`) suivi d'un message de l'assistant (`[bob's answer]` et le texte). Un seul message aurait été plus simple, mais votre question suivante est aussi un message utilisateur, et plusieurs templates de chat (ceux de Gemma, de Mistral) refusent deux messages utilisateur à la suite. Les marques disent qui a dit quoi, pour que votre modèle ne prenne pas la réponse de bob pour la sienne. La réponse est plafonnée comme la sortie d'un outil, à `maxOutput` caractères : elle atterrit dans la fenêtre de votre modèle. | 15 | **La réponse est une paire de messages.** Elle entre dans votre historique comme un message utilisateur (`[asked bob (fournisseur/modèle): …]`) suivi d'un message de l'assistant (`[bob's answer]` et le texte). Un seul message aurait été plus simple, mais votre question suivante est aussi un message utilisateur, et plusieurs templates de chat (ceux de Gemma, de Mistral) refusent deux messages utilisateur à la suite. Les marques disent qui a dit quoi, pour que votre modèle ne prenne pas la réponse de bob pour la sienne. La réponse est plafonnée comme la sortie d'un outil, à `maxOutput` caractères : elle atterrit dans la fenêtre de votre modèle. |
| 16 | 16 | ||
| @@ -22,7 +22,7 @@ | |||
| 22 | 22 | ||
| 23 | **Les chaînes s'arrêtent.** `/ask` est une commande tapée par une personne ; aucun modèle ne peut l'appeler, donc aucun agent ne boucle tout seul. Une personne pourrait quand même taper `/ask` dans un agent lui-même interrogé, alors chaque enfant connaît sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne. | 23 | **Les chaînes s'arrêtent.** `/ask` est une commande tapée par une personne ; aucun modèle ne peut l'appeler, donc aucun agent ne boucle tout seul. Une personne pourrait quand même taper `/ask` dans un agent lui-même interrogé, alors chaque enfant connaît sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne. |
| 24 | 24 | ||
| 25 | -**La même mécanique répond aux scripts.** `mm --ask <agent>` et `mm --ask-with-context <agent>` sont les commandes slash de ce qui ne sait que lancer une commande — une skill, un script. Ils démarrent le même enfant en ACP, avec la même protection de l'environnement et la même limite de profondeur, mais la réponse va sur stdout et tout le reste sur stderr, pour que l'appelant récupère la réponse seule. mm exporte `MM_BIN` et `MM_CONFIG` vers les commandes de son outil `bash`, donc une skill appelle « ce mm, avec cette configuration » sans connaître aucun des deux chemins. Deux choses diffèrent des commandes slash, parce que l'appelant est une commande et non une personne. Les appels d'outils de l'agent sont autorisés, comme en mode ligne : la personne a déjà validé la commande `mm --ask` elle-même. Et `mm --ask` surveille son processus parent : l'outil `bash` tue `bash` quand son budget est écoulé, pas les enfants de bash, et une demande orpheline laissait son agent tourner — et écrire des fichiers — alors que plus personne n'attendait. | 25 | +**La même mécanique répond aux scripts.** `mm --ask <agent>` et `mm --ask-with-context <agent>` sont les commandes slash de ce qui ne sait que lancer une commande — une skill, un script. Ils démarrent le même enfant en ACP, avec la même protection de l'environnement et la même limite de profondeur, mais la réponse va sur stdout et tout le reste sur stderr, pour que l'appelant récupère la réponse seule. mm exporte `MM_BIN` et `MM_CONFIG` vers les commandes de son outil `bash`, donc une skill appelle « ce mm, avec cette configuration » sans connaître aucun des deux chemins. Deux choses diffèrent des commandes slash, parce que l'appelant est une commande et non une personne. Les appels d'outils de l'agent suivent le `yolo` de la configuration appelante : autorisés quand il est activé — la personne a déjà validé la commande `mm --ask` elle-même —, refusés quand il est désactivé, puisque stdin portait la question et que personne n'est là pour répondre. Et `mm --ask` surveille son processus parent : l'outil `bash` tue `bash` quand son budget est écoulé, pas les enfants de bash, et une demande orpheline laissait son agent tourner — et écrire des fichiers — alors que plus personne n'attendait. |
| 26 | 26 | ||
| 27 | **Le temps est le point faible.** Un modèle local peut mettre une minute à répondre, et une commande `bash` disposait de 30 secondes ; `bashTimeout` règle désormais ce budget. Le watchdog de l'agent qui pose la question continue aussi de compter pendant qu'une commande tourne (ticket 57), il faut donc le relever aussi tant que ce ticket n'est pas corrigé. | 27 | **Le temps est le point faible.** Un modèle local peut mettre une minute à répondre, et une commande `bash` disposait de 30 secondes ; `bashTimeout` règle désormais ce budget. Le watchdog de l'agent qui pose la question continue aussi de compter pendant qu'une commande tourne (ticket 57), il faut donc le relever aussi tant que ce ticket n'est pas corrigé. |
| 28 | 28 | ||
modified
docs/fr/explanation/lsp-tool.md +1 -1 | @@ -20,7 +20,7 @@ Un éditeur informe son serveur de chaque frappe. `mm` n'a pas de frappes : le m | ||
| 20 | 20 | |
| 21 | 21 | ## Pourquoi `rename` écrit lui-même les fichiers, et demande d'abord |
| 22 | 22 | |
| 23 | -Un renommage est un `WorkspaceEdit` : des plages à remplacer dans plusieurs fichiers. Le client les applique sur disque — de la fin de chaque fichier vers le début pour que les positions antérieures restent valides, les colonnes UTF-16 converties en octets — puis resynchronise les fichiers changés pour que la question suivante porte sur le code renommé. Dans le terminal il s'exécute comme `edit_file` ; là où une façade a une boîte de permission (un éditeur en ACP, le mode fenêtre avec `permissions: ask`) il demande, parce qu'il écrit, alors que les cinq actions de navigation ne demandent pas : une boîte à chaque lecture apprend aux gens à cliquer « autoriser » sans lire. | |
| 23 | +Un renommage est un `WorkspaceEdit` : des plages à remplacer dans plusieurs fichiers. Le client les applique sur disque — de la fin de chaque fichier vers le début pour que les positions antérieures restent valides, les colonnes UTF-16 converties en octets — puis resynchronise les fichiers changés pour que la question suivante porte sur le code renommé. Dans le terminal il s'exécute comme `edit_file` ; là où une façade a une boîte de permission (avec `yolo: false` : le mode ligne, la fenêtre, un éditeur en ACP) il demande, parce qu'il écrit, alors que les cinq actions de navigation ne demandent pas : une boîte à chaque lecture apprend aux gens à cliquer « autoriser » sans lire. | |
| 24 | 24 | |
| 25 | 25 | ## Pourquoi un serveur manquant est un avertissement |
| 26 | 26 | |
| @@ -20,7 +20,7 @@ Un éditeur informe son serveur de chaque frappe. `mm` n'a pas de frappes : le m | |||
| 20 | 20 | ||
| 21 | ## Pourquoi `rename` écrit lui-même les fichiers, et demande d'abord | 21 | ## Pourquoi `rename` écrit lui-même les fichiers, et demande d'abord |
| 22 | 22 | ||
| 23 | -Un renommage est un `WorkspaceEdit` : des plages à remplacer dans plusieurs fichiers. Le client les applique sur disque — de la fin de chaque fichier vers le début pour que les positions antérieures restent valides, les colonnes UTF-16 converties en octets — puis resynchronise les fichiers changés pour que la question suivante porte sur le code renommé. Dans le terminal il s'exécute comme `edit_file` ; là où une façade a une boîte de permission (un éditeur en ACP, le mode fenêtre avec `permissions: ask`) il demande, parce qu'il écrit, alors que les cinq actions de navigation ne demandent pas : une boîte à chaque lecture apprend aux gens à cliquer « autoriser » sans lire. | 23 | +Un renommage est un `WorkspaceEdit` : des plages à remplacer dans plusieurs fichiers. Le client les applique sur disque — de la fin de chaque fichier vers le début pour que les positions antérieures restent valides, les colonnes UTF-16 converties en octets — puis resynchronise les fichiers changés pour que la question suivante porte sur le code renommé. Dans le terminal il s'exécute comme `edit_file` ; là où une façade a une boîte de permission (avec `yolo: false` : le mode ligne, la fenêtre, un éditeur en ACP) il demande, parce qu'il écrit, alors que les cinq actions de navigation ne demandent pas : une boîte à chaque lecture apprend aux gens à cliquer « autoriser » sans lire. |
| 24 | 24 | ||
| 25 | ## Pourquoi un serveur manquant est un avertissement | 25 | ## Pourquoi un serveur manquant est un avertissement |
| 26 | 26 | ||
modified
docs/fr/explanation/mcp-architecture.md +1 -1 | @@ -37,7 +37,7 @@ Un diagramme draw.io de ces paquets et de leurs dépendances est conservé dans | ||
| 37 | 37 | |
| 38 | 38 | **La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien. |
| 39 | 39 | |
| 40 | -**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️` et personne ne demande de permission. En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `permissions: ask` est la version terminal du dialogue de l'éditeur. | |
| 40 | +**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️`, et ne demandent la permission, par une invite `🔐` d'une ligne, que quand `yolo` est désactivé (voir [Le mode yolo et les permissions](yolo-and-permissions.md)). En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `yolo: false` est la version fenêtre du dialogue de l'éditeur. | |
| 41 | 41 | |
| 42 | 42 | **Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois. |
| 43 | 43 | |
| @@ -37,7 +37,7 @@ Un diagramme draw.io de ces paquets et de leurs dépendances est conservé dans | |||
| 37 | 37 | ||
| 38 | **La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien. | 38 | **La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien. |
| 39 | 39 | ||
| 40 | -**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️` et personne ne demande de permission. En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `permissions: ask` est la version terminal du dialogue de l'éditeur. | 40 | +**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️`, et ne demandent la permission, par une invite `🔐` d'une ligne, que quand `yolo` est désactivé (voir [Le mode yolo et les permissions](yolo-and-permissions.md)). En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `yolo: false` est la version fenêtre du dialogue de l'éditeur. |
| 41 | 41 | ||
| 42 | **Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois. | 42 | **Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois. |
| 43 | 43 | ||
added
docs/fr/explanation/yolo-and-permissions.md +51 -0 | new file mode 100644 | ||
| @@ -0,0 +1,51 @@ | ||
| 1 | +# Le mode yolo et les permissions — explication | |
| 2 | + | |
| 3 | +## De quoi s'agit-il ? | |
| 4 | + | |
| 5 | +Un agent de code exécute des commandes et écrit des fichiers sur votre machine. Quelqu'un doit décider si chacun de ces appels peut s'exécuter : soit l'utilisateur, appel par appel, soit personne, parce que l'utilisateur a décidé une fois pour toutes de laisser l'agent agir. mini-me appelle ce second choix le **mode yolo** et en fait un seul réglage, `yolo`, lu de la même façon par les trois façades — le mode ligne, la fenêtre et l'éditeur (ACP) — et par un agent interrogé par un autre. | |
| 6 | + | |
| 7 | +## Pourquoi yolo est la valeur par défaut | |
| 8 | + | |
| 9 | +mini-me est conçu pour piloter un petit modèle local à travers de nombreuses étapes courtes. Demander avant chaque `ls` rend cette boucle inutilisable en pratique, et le mode ligne — la première façade — ne demandait jamais. Avant ce réglage, le comportement dépendait de l'endroit où l'on lançait mm : le mode ligne ne demandait jamais, la fenêtre demandait seulement avec `permissions: ask`, et l'éditeur demandait toujours, qu'on le veuille ou non. Le même fichier d'agent ne voulait pas dire la même chose à trois endroits. | |
| 10 | + | |
| 11 | +La valeur par défaut a donc été fixée sur ce que faisaient déjà la plupart des exécutions — agir sans demander — et la protection est devenue un choix délibéré et visible : `yolo: false`. La bannière dit dans quel sens le réglage est posé (`yolo: on` / `yolo: off (tools ask)`), pour que personne ne le découvre par surprise, dans un sens comme dans l'autre. La conséquence à connaître : dans un éditeur, le dialogue de permission n'apparaît qu'avec `yolo: false` ; avant, il apparaissait toujours. | |
| 12 | + | |
| 13 | +## Exister ou demander | |
| 14 | + | |
| 15 | +Deux questions différentes sont volontairement séparées : | |
| 16 | + | |
| 17 | +- **L'outil existe-t-il ?** `bashTool`, `editTools`, `fetchContentTool`. Un outil qui n'existe pas n'est pas du tout déclaré au modèle ; il ne peut même pas essayer. | |
| 18 | +- **Un outil qui existe demande-t-il avant de s'exécuter ?** `yolo`. | |
| 19 | + | |
| 20 | +Ainsi, `yolo: true` avec `bashTool: false` est un agent qui modifie librement les fichiers mais n'a pas de shell, et `yolo: false` avec `bashTool: true` est un agent qui a un shell mais a besoin de votre accord pour chaque commande. Fusionner les deux en une valeur à trois états (`bashTool: ask`) a été envisagé puis écarté : il aurait fallu la répéter pour chaque outil, alors que la vraie décision — « est-ce que je fais confiance à cet agent pour agir seul, maintenant ? » — est unique. | |
| 21 | + | |
| 22 | +Seuls les appels qui modifient quelque chose demandent : `bash`, `write_file`, `edit_file`, `fetch_content` quand il enregistre, `rename` de `lsp`, et les actions de `plan` qui modifient un plan. Les lectures (`read_file`, `read_skill`, les actions de navigation de `lsp`, `fetch_content` sans `output`) ne demandent jamais : une question à chaque lecture apprend aux gens à cliquer « autoriser » sans lire. | |
| 23 | + | |
| 24 | +## Quatre réponses, une mémoire | |
| 25 | + | |
| 26 | +Quand un appel demande, la réponse peut valoir *une fois* ou *pour la session*, et *oui* ou *non* : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, ne jamais autoriser cet outil dans cette session. « Jamais » existe parce qu'un refus qui ne dure qu'un appel oblige à refuser la même nouvelle tentative encore et encore ; « refuser une fois » existe parce que sans lui, la seule façon de dire non serait définitive. Les réponses mémorisées d'une session le sont par nom d'outil, survivent à `/new` (l'éditeur voit toujours la même session) et disparaissent quand mm s'arrête. | |
| 27 | + | |
| 28 | +Les règles vivent dans un seul paquet, `internal/consent` : la vérification de `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient (`Policy`) et le point d'accroche de l'invite du mode ligne. Chaque façade se contente de dessiner la question à sa manière — une ligne `🔐`, la zone de la fenêtre, le dialogue de l'éditeur. Auparavant, chaque façade avait sa propre table « autorisé pour la session », et les trois avaient déjà commencé à diverger. | |
| 29 | + | |
| 30 | +## Pourquoi l'agent qui interroge décide | |
| 31 | + | |
| 32 | +Avec `mmConfigs`, un agent peut en interroger un autre (`/ask-jan`, `mm --ask jan`). Deux fichiers de configuration, deux valeurs de `yolo` : laquelle l'emporte ? Celle de l'agent que l'**utilisateur a lancé**. C'est le seul agent qui a un utilisateur en face de lui, et l'utilisateur a choisi sa configuration pour cette session ; le fichier de l'agent interrogé a été écrit pour une autre situation — peut-être pour tourner seul en mode yolo. | |
| 33 | + | |
| 34 | +Techniquement, l'agent interrogé est lancé avec `AGENT_YOLO=false` : il soumet donc chaque appel qu'il filtrerait par la requête de permission ACP — le même canal qu'utilise un éditeur. L'agent qui interroge la reçoit et la fait passer par son propre `consent.Gate` : en mode yolo, il autorise aussitôt ; sinon, il demande à son propre utilisateur, dans sa propre façade, et mémorise la réponse sous `jan/bash`, pour qu'autoriser le shell de Jan n'autorise pas le vôtre. L'agent interrogé, lui, ne retient rien : on lui redemande à chaque fois, et c'est la mémoire de l'agent qui interroge qui répond. | |
| 35 | + | |
| 36 | +## Les modes qui ne peuvent pas demander | |
| 37 | + | |
| 38 | +`mm -p` et `mm --ask` n'ont personne à qui demander : le premier est une exécution unique, le second lit sa question sur stdin. Avec `yolo: false`, ils refusent les appels qui demanderaient et le disent une fois sur stderr. Les autoriser en silence ferait de `yolo: false` un réglage qui dépend de la façon dont on a lancé mm, exactement l'incohérence que cette conception supprime ; rester bloqué sur une question invisible bloquerait les scripts pour toujours. | |
| 39 | + | |
| 40 | +## Alternatives écartées | |
| 41 | + | |
| 42 | +- **Un simple flag `--yolo` en ligne de commande.** Il n'atteindrait pas les kits de sandbox ni les réglages de lancement d'un éditeur, qui posent des variables d'environnement et des fichiers, pas des flags. `AGENT_YOLO` couvre le cas d'une seule exécution. | |
| 43 | +- **Garder `permissions: ask | auto`.** Seule la fenêtre la lisait, et elle n'apparaissait dans aucune configuration livrée. Elle fonctionne encore — `ask` signifie `yolo: false` — avec un avertissement d'obsolescence, pour que les anciens fichiers gardent leur protection. | |
| 44 | +- **Laisser yolo passer outre les serveurs MCP marqués `ask`.** Un serveur MCP est du code tiers que l'utilisateur a explicitement marqué comme non fiable ; `yolo: true` respecte ce choix par serveur. `yolo: false`, en revanche, fait demander chaque outil MCP : activer la protection ne doit jamais laisser de trou. | |
| 45 | + | |
| 46 | +## Liens avec le reste | |
| 47 | + | |
| 48 | +- Les clés de configuration : [référence de la configuration](../reference/configuration.md). | |
| 49 | +- Comment l'activer : [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | |
| 50 | +- Le chemin d'événements que partagent les façades (`ui.Sink`, `Allow`) : [Architecture](architecture.md). | |
| 51 | +- Interroger un autre agent : [Interroger un autre agent](asking-another-agent.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,51 @@ | |||
| 1 | +# Le mode yolo et les permissions — explication | ||
| 2 | + | ||
| 3 | +## De quoi s'agit-il ? | ||
| 4 | + | ||
| 5 | +Un agent de code exécute des commandes et écrit des fichiers sur votre machine. Quelqu'un doit décider si chacun de ces appels peut s'exécuter : soit l'utilisateur, appel par appel, soit personne, parce que l'utilisateur a décidé une fois pour toutes de laisser l'agent agir. mini-me appelle ce second choix le **mode yolo** et en fait un seul réglage, `yolo`, lu de la même façon par les trois façades — le mode ligne, la fenêtre et l'éditeur (ACP) — et par un agent interrogé par un autre. | ||
| 6 | + | ||
| 7 | +## Pourquoi yolo est la valeur par défaut | ||
| 8 | + | ||
| 9 | +mini-me est conçu pour piloter un petit modèle local à travers de nombreuses étapes courtes. Demander avant chaque `ls` rend cette boucle inutilisable en pratique, et le mode ligne — la première façade — ne demandait jamais. Avant ce réglage, le comportement dépendait de l'endroit où l'on lançait mm : le mode ligne ne demandait jamais, la fenêtre demandait seulement avec `permissions: ask`, et l'éditeur demandait toujours, qu'on le veuille ou non. Le même fichier d'agent ne voulait pas dire la même chose à trois endroits. | ||
| 10 | + | ||
| 11 | +La valeur par défaut a donc été fixée sur ce que faisaient déjà la plupart des exécutions — agir sans demander — et la protection est devenue un choix délibéré et visible : `yolo: false`. La bannière dit dans quel sens le réglage est posé (`yolo: on` / `yolo: off (tools ask)`), pour que personne ne le découvre par surprise, dans un sens comme dans l'autre. La conséquence à connaître : dans un éditeur, le dialogue de permission n'apparaît qu'avec `yolo: false` ; avant, il apparaissait toujours. | ||
| 12 | + | ||
| 13 | +## Exister ou demander | ||
| 14 | + | ||
| 15 | +Deux questions différentes sont volontairement séparées : | ||
| 16 | + | ||
| 17 | +- **L'outil existe-t-il ?** `bashTool`, `editTools`, `fetchContentTool`. Un outil qui n'existe pas n'est pas du tout déclaré au modèle ; il ne peut même pas essayer. | ||
| 18 | +- **Un outil qui existe demande-t-il avant de s'exécuter ?** `yolo`. | ||
| 19 | + | ||
| 20 | +Ainsi, `yolo: true` avec `bashTool: false` est un agent qui modifie librement les fichiers mais n'a pas de shell, et `yolo: false` avec `bashTool: true` est un agent qui a un shell mais a besoin de votre accord pour chaque commande. Fusionner les deux en une valeur à trois états (`bashTool: ask`) a été envisagé puis écarté : il aurait fallu la répéter pour chaque outil, alors que la vraie décision — « est-ce que je fais confiance à cet agent pour agir seul, maintenant ? » — est unique. | ||
| 21 | + | ||
| 22 | +Seuls les appels qui modifient quelque chose demandent : `bash`, `write_file`, `edit_file`, `fetch_content` quand il enregistre, `rename` de `lsp`, et les actions de `plan` qui modifient un plan. Les lectures (`read_file`, `read_skill`, les actions de navigation de `lsp`, `fetch_content` sans `output`) ne demandent jamais : une question à chaque lecture apprend aux gens à cliquer « autoriser » sans lire. | ||
| 23 | + | ||
| 24 | +## Quatre réponses, une mémoire | ||
| 25 | + | ||
| 26 | +Quand un appel demande, la réponse peut valoir *une fois* ou *pour la session*, et *oui* ou *non* : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, ne jamais autoriser cet outil dans cette session. « Jamais » existe parce qu'un refus qui ne dure qu'un appel oblige à refuser la même nouvelle tentative encore et encore ; « refuser une fois » existe parce que sans lui, la seule façon de dire non serait définitive. Les réponses mémorisées d'une session le sont par nom d'outil, survivent à `/new` (l'éditeur voit toujours la même session) et disparaissent quand mm s'arrête. | ||
| 27 | + | ||
| 28 | +Les règles vivent dans un seul paquet, `internal/consent` : la vérification de `yolo` (`Gate`), les quatre réponses (`Answer`), ce qu'une session retient (`Policy`) et le point d'accroche de l'invite du mode ligne. Chaque façade se contente de dessiner la question à sa manière — une ligne `🔐`, la zone de la fenêtre, le dialogue de l'éditeur. Auparavant, chaque façade avait sa propre table « autorisé pour la session », et les trois avaient déjà commencé à diverger. | ||
| 29 | + | ||
| 30 | +## Pourquoi l'agent qui interroge décide | ||
| 31 | + | ||
| 32 | +Avec `mmConfigs`, un agent peut en interroger un autre (`/ask-jan`, `mm --ask jan`). Deux fichiers de configuration, deux valeurs de `yolo` : laquelle l'emporte ? Celle de l'agent que l'**utilisateur a lancé**. C'est le seul agent qui a un utilisateur en face de lui, et l'utilisateur a choisi sa configuration pour cette session ; le fichier de l'agent interrogé a été écrit pour une autre situation — peut-être pour tourner seul en mode yolo. | ||
| 33 | + | ||
| 34 | +Techniquement, l'agent interrogé est lancé avec `AGENT_YOLO=false` : il soumet donc chaque appel qu'il filtrerait par la requête de permission ACP — le même canal qu'utilise un éditeur. L'agent qui interroge la reçoit et la fait passer par son propre `consent.Gate` : en mode yolo, il autorise aussitôt ; sinon, il demande à son propre utilisateur, dans sa propre façade, et mémorise la réponse sous `jan/bash`, pour qu'autoriser le shell de Jan n'autorise pas le vôtre. L'agent interrogé, lui, ne retient rien : on lui redemande à chaque fois, et c'est la mémoire de l'agent qui interroge qui répond. | ||
| 35 | + | ||
| 36 | +## Les modes qui ne peuvent pas demander | ||
| 37 | + | ||
| 38 | +`mm -p` et `mm --ask` n'ont personne à qui demander : le premier est une exécution unique, le second lit sa question sur stdin. Avec `yolo: false`, ils refusent les appels qui demanderaient et le disent une fois sur stderr. Les autoriser en silence ferait de `yolo: false` un réglage qui dépend de la façon dont on a lancé mm, exactement l'incohérence que cette conception supprime ; rester bloqué sur une question invisible bloquerait les scripts pour toujours. | ||
| 39 | + | ||
| 40 | +## Alternatives écartées | ||
| 41 | + | ||
| 42 | +- **Un simple flag `--yolo` en ligne de commande.** Il n'atteindrait pas les kits de sandbox ni les réglages de lancement d'un éditeur, qui posent des variables d'environnement et des fichiers, pas des flags. `AGENT_YOLO` couvre le cas d'une seule exécution. | ||
| 43 | +- **Garder `permissions: ask | auto`.** Seule la fenêtre la lisait, et elle n'apparaissait dans aucune configuration livrée. Elle fonctionne encore — `ask` signifie `yolo: false` — avec un avertissement d'obsolescence, pour que les anciens fichiers gardent leur protection. | ||
| 44 | +- **Laisser yolo passer outre les serveurs MCP marqués `ask`.** Un serveur MCP est du code tiers que l'utilisateur a explicitement marqué comme non fiable ; `yolo: true` respecte ce choix par serveur. `yolo: false`, en revanche, fait demander chaque outil MCP : activer la protection ne doit jamais laisser de trou. | ||
| 45 | + | ||
| 46 | +## Liens avec le reste | ||
| 47 | + | ||
| 48 | +- Les clés de configuration : [référence de la configuration](../reference/configuration.md). | ||
| 49 | +- Comment l'activer : [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | ||
| 50 | +- Le chemin d'événements que partagent les façades (`ui.Sink`, `Allow`) : [Architecture](architecture.md). | ||
| 51 | +- Interroger un autre agent : [Interroger un autre agent](asking-another-agent.md). | ||
modified
docs/fr/how-to/ask-another-agent.md +1 -1 | @@ -50,7 +50,7 @@ Ce guide montre comment poser une question à un autre agent mm — un autre mod | ||
| 50 | 50 | |
| 51 | 51 | - **Laisser l'autre agent voir la conversation.** Utilisez `/ask-with-context-lucy <prompt>` : Lucy reçoit une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils) avant votre question. |
| 52 | 52 | - **Dans le terminal ou la fenêtre, par nom.** `/ask lucy <prompt>` et `/ask-with-context lucy <prompt>` sont les mêmes commandes, avec l'agent en argument. |
| 53 | -- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est la boîte de dialogue de permission de l'éditeur qui vous le demande. | |
| 53 | +- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est le `yolo` de votre agent qui décide : avec `yolo: true` elle s'exécute, avec `yolo: false` c'est la boîte de dialogue de permission de l'éditeur qui vous le demande — quoi que dise le fichier de Lucy. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | |
| 54 | 54 | - **Arrêter une longue réponse.** `/abort`, `Échap` ou `Ctrl+C` — rien n'est gardé d'une demande annulée. |
| 55 | 55 | - **Des agents sur d'autres fournisseurs.** N'importe quelle configuration mm convient : un agent Ollama à côté d'un agent llmman n'est qu'une entrée de plus dans `mmConfigs`. |
| 56 | 56 | |
| @@ -50,7 +50,7 @@ Ce guide montre comment poser une question à un autre agent mm — un autre mod | |||
| 50 | 50 | ||
| 51 | - **Laisser l'autre agent voir la conversation.** Utilisez `/ask-with-context-lucy <prompt>` : Lucy reçoit une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils) avant votre question. | 51 | - **Laisser l'autre agent voir la conversation.** Utilisez `/ask-with-context-lucy <prompt>` : Lucy reçoit une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils) avant votre question. |
| 52 | - **Dans le terminal ou la fenêtre, par nom.** `/ask lucy <prompt>` et `/ask-with-context lucy <prompt>` sont les mêmes commandes, avec l'agent en argument. | 52 | - **Dans le terminal ou la fenêtre, par nom.** `/ask lucy <prompt>` et `/ask-with-context lucy <prompt>` sont les mêmes commandes, avec l'agent en argument. |
| 53 | -- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est la boîte de dialogue de permission de l'éditeur qui vous le demande. | 53 | +- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est le `yolo` de votre agent qui décide : avec `yolo: true` elle s'exécute, avec `yolo: false` c'est la boîte de dialogue de permission de l'éditeur qui vous le demande — quoi que dise le fichier de Lucy. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). |
| 54 | - **Arrêter une longue réponse.** `/abort`, `Échap` ou `Ctrl+C` — rien n'est gardé d'une demande annulée. | 54 | - **Arrêter une longue réponse.** `/abort`, `Échap` ou `Ctrl+C` — rien n'est gardé d'une demande annulée. |
| 55 | - **Des agents sur d'autres fournisseurs.** N'importe quelle configuration mm convient : un agent Ollama à côté d'un agent llmman n'est qu'une entrée de plus dans `mmConfigs`. | 55 | - **Des agents sur d'autres fournisseurs.** N'importe quelle configuration mm convient : un agent Ollama à côté d'un agent llmman n'est qu'une entrée de plus dans `mmConfigs`. |
| 56 | 56 | ||
added
docs/fr/how-to/confirm-tool-calls.md +57 -0 | new file mode 100644 | ||
| @@ -0,0 +1,57 @@ | ||
| 1 | +# Confirmer les appels d'outils avant leur exécution | |
| 2 | + | |
| 3 | +Par défaut, mini-me tourne en **mode yolo** : `bash`, les écritures et les modifications de fichiers s'exécutent dès que le modèle les demande. Ce guide montre comment faire en sorte que l'agent vous demande d'abord, dans le terminal, dans la fenêtre et dans un éditeur. Il suppose que vous lancez déjà `mm` avec un fichier de configuration YAML. | |
| 4 | + | |
| 5 | +## Étapes | |
| 6 | + | |
| 7 | +1. Désactivez yolo dans votre fichier de configuration : | |
| 8 | + | |
| 9 | + ```yaml | |
| 10 | + yolo: false | |
| 11 | + bashTool: true | |
| 12 | + ``` | |
| 13 | + | |
| 14 | + `bashTool` (et `editTools`, `fetchContentTool`) décident toujours quels outils **existent** ; `yolo` décide si ceux qui existent demandent avant de s'exécuter. | |
| 15 | + | |
| 16 | +2. Lancez `mm` comme d'habitude. La bannière se termine par `yolo: off (tools ask)` : | |
| 17 | + | |
| 18 | + ``` | |
| 19 | + mm v0.2.6 | config: agent.yaml | provider: dmr | model: … | tools: bash, read_file, write_file, edit_file | skills: 3 | yolo: off (tools ask) | |
| 20 | + ``` | |
| 21 | + | |
| 22 | +3. Demandez quelque chose qui lance une commande. En mode ligne, la ligne de l'outil est suivie d'une question : | |
| 23 | + | |
| 24 | + ``` | |
| 25 | + 🛠️ bash: ls -la | |
| 26 | + | |
| 27 | + 🔐 bash: ls -la | |
| 28 | + [a] allow once · [s] allow bash for this session · [r] reject · [n] never bash | |
| 29 | + ? | |
| 30 | + ``` | |
| 31 | + | |
| 32 | +4. Tapez une lettre puis Entrée : | |
| 33 | + | |
| 34 | + | Touche | Effet | | |
| 35 | + |--------|-------| | |
| 36 | + | `a` | Exécute cet appel. Le prochain appel à `bash` demandera de nouveau. | | |
| 37 | + | `s` | Exécute cet appel et tous les appels à `bash` suivants de la session, sans demander. | | |
| 38 | + | `r` | N'exécute pas cet appel. Le modèle reçoit `[command rejected by the user]` et peut proposer autre chose. Le prochain appel demandera de nouveau. | | |
| 39 | + | `n` | N'exécute pas cet appel, et refuse sans demander tous les appels à `bash` suivants de la session. | | |
| 40 | + | |
| 41 | + Toute autre réponse réaffiche la légende. `/abort` ou **Ctrl+C** refuse l'appel et arrête le tour. | |
| 42 | + | |
| 43 | +## Variantes | |
| 44 | + | |
| 45 | +- **Dans la fenêtre (`mm -tui`).** La zone de saisie est remplacée par `? bash: ls -la` et les mêmes quatre touches y répondent ; **Échap** équivaut à `r`. | |
| 46 | +- **Dans un éditeur (`mm -acp`, Zed).** Le dialogue de permission de l'éditeur propose *Allow once*, *Allow bash for this session*, *Reject* et *Never allow bash in this session*. Avec `yolo: true`, aucun dialogue n'est affiché. | |
| 47 | +- **Pour une seule exécution, sans modifier le fichier.** `AGENT_YOLO=false mm agent.yaml` (ou `AGENT_YOLO=true` pour sauter les questions une fois). Dans un éditeur, mettez la variable dans les réglages `env` de l'agent, à côté d'`AGENT_CONFIG`. | |
| 48 | +- **Avec d'autres agents (`mmConfigs`).** L'agent que vous avez lancé décide pour les agents qu'il interroge. Avec `yolo: false` dans votre configuration principale, un appel à `bash` fait par `/ask-jan` vous est demandé, intitulé `jan · bash: …`, et `s` mémorise `jan/bash`, pas votre propre `bash`. Le `yolo` écrit dans le fichier de Jan est ignoré tant qu'il répond à un `/ask`. | |
| 49 | +- **Dans un script (`mm -p`, `mm --ask`).** Personne ne peut y répondre : avec `yolo: false`, les appels qui demanderaient sont refusés, et stderr le dit une fois : `[yolo is off and this mode cannot ask: …]`. Gardez `yolo: true` pour les exécutions scriptées, ou retirez les outils que vous ne voulez pas avec `bashTool: false` / `editTools: false`. | |
| 50 | +- **Outils MCP.** Avec `yolo: false`, chaque outil MCP demande aussi. Avec `yolo: true`, chaque serveur MCP garde ses propres `permissions` (`ask` par défaut, `auto` pour s'exécuter sans demander). | |
| 51 | +- **Une ancienne configuration avec `permissions: ask`.** Elle fonctionne encore et signifie `yolo: false` ; la bannière avertit que la clé est obsolète. Remplacez-la par `yolo: false`. | |
| 52 | + | |
| 53 | +## Voir aussi | |
| 54 | + | |
| 55 | +- Chaque clé et sa valeur par défaut : [référence de la configuration](../reference/configuration.md) (`yolo`, `permissions`). | |
| 56 | +- Ce que montre chaque façade : [référence de la CLI](../reference/cli.md), [référence ACP](../reference/acp.md#requêtes-sessionrequest_permission-envoyées). | |
| 57 | +- Pourquoi yolo est la valeur par défaut et pourquoi l'agent qui interroge l'emporte : [Le mode yolo et les permissions](../explanation/yolo-and-permissions.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | +# Confirmer les appels d'outils avant leur exécution | ||
| 2 | + | ||
| 3 | +Par défaut, mini-me tourne en **mode yolo** : `bash`, les écritures et les modifications de fichiers s'exécutent dès que le modèle les demande. Ce guide montre comment faire en sorte que l'agent vous demande d'abord, dans le terminal, dans la fenêtre et dans un éditeur. Il suppose que vous lancez déjà `mm` avec un fichier de configuration YAML. | ||
| 4 | + | ||
| 5 | +## Étapes | ||
| 6 | + | ||
| 7 | +1. Désactivez yolo dans votre fichier de configuration : | ||
| 8 | + | ||
| 9 | + ```yaml | ||
| 10 | + yolo: false | ||
| 11 | + bashTool: true | ||
| 12 | + ``` | ||
| 13 | + | ||
| 14 | + `bashTool` (et `editTools`, `fetchContentTool`) décident toujours quels outils **existent** ; `yolo` décide si ceux qui existent demandent avant de s'exécuter. | ||
| 15 | + | ||
| 16 | +2. Lancez `mm` comme d'habitude. La bannière se termine par `yolo: off (tools ask)` : | ||
| 17 | + | ||
| 18 | + ``` | ||
| 19 | + mm v0.2.6 | config: agent.yaml | provider: dmr | model: … | tools: bash, read_file, write_file, edit_file | skills: 3 | yolo: off (tools ask) | ||
| 20 | + ``` | ||
| 21 | + | ||
| 22 | +3. Demandez quelque chose qui lance une commande. En mode ligne, la ligne de l'outil est suivie d'une question : | ||
| 23 | + | ||
| 24 | + ``` | ||
| 25 | + 🛠️ bash: ls -la | ||
| 26 | + | ||
| 27 | + 🔐 bash: ls -la | ||
| 28 | + [a] allow once · [s] allow bash for this session · [r] reject · [n] never bash | ||
| 29 | + ? | ||
| 30 | + ``` | ||
| 31 | + | ||
| 32 | +4. Tapez une lettre puis Entrée : | ||
| 33 | + | ||
| 34 | + | Touche | Effet | | ||
| 35 | + |--------|-------| | ||
| 36 | + | `a` | Exécute cet appel. Le prochain appel à `bash` demandera de nouveau. | | ||
| 37 | + | `s` | Exécute cet appel et tous les appels à `bash` suivants de la session, sans demander. | | ||
| 38 | + | `r` | N'exécute pas cet appel. Le modèle reçoit `[command rejected by the user]` et peut proposer autre chose. Le prochain appel demandera de nouveau. | | ||
| 39 | + | `n` | N'exécute pas cet appel, et refuse sans demander tous les appels à `bash` suivants de la session. | | ||
| 40 | + | ||
| 41 | + Toute autre réponse réaffiche la légende. `/abort` ou **Ctrl+C** refuse l'appel et arrête le tour. | ||
| 42 | + | ||
| 43 | +## Variantes | ||
| 44 | + | ||
| 45 | +- **Dans la fenêtre (`mm -tui`).** La zone de saisie est remplacée par `? bash: ls -la` et les mêmes quatre touches y répondent ; **Échap** équivaut à `r`. | ||
| 46 | +- **Dans un éditeur (`mm -acp`, Zed).** Le dialogue de permission de l'éditeur propose *Allow once*, *Allow bash for this session*, *Reject* et *Never allow bash in this session*. Avec `yolo: true`, aucun dialogue n'est affiché. | ||
| 47 | +- **Pour une seule exécution, sans modifier le fichier.** `AGENT_YOLO=false mm agent.yaml` (ou `AGENT_YOLO=true` pour sauter les questions une fois). Dans un éditeur, mettez la variable dans les réglages `env` de l'agent, à côté d'`AGENT_CONFIG`. | ||
| 48 | +- **Avec d'autres agents (`mmConfigs`).** L'agent que vous avez lancé décide pour les agents qu'il interroge. Avec `yolo: false` dans votre configuration principale, un appel à `bash` fait par `/ask-jan` vous est demandé, intitulé `jan · bash: …`, et `s` mémorise `jan/bash`, pas votre propre `bash`. Le `yolo` écrit dans le fichier de Jan est ignoré tant qu'il répond à un `/ask`. | ||
| 49 | +- **Dans un script (`mm -p`, `mm --ask`).** Personne ne peut y répondre : avec `yolo: false`, les appels qui demanderaient sont refusés, et stderr le dit une fois : `[yolo is off and this mode cannot ask: …]`. Gardez `yolo: true` pour les exécutions scriptées, ou retirez les outils que vous ne voulez pas avec `bashTool: false` / `editTools: false`. | ||
| 50 | +- **Outils MCP.** Avec `yolo: false`, chaque outil MCP demande aussi. Avec `yolo: true`, chaque serveur MCP garde ses propres `permissions` (`ask` par défaut, `auto` pour s'exécuter sans demander). | ||
| 51 | +- **Une ancienne configuration avec `permissions: ask`.** Elle fonctionne encore et signifie `yolo: false` ; la bannière avertit que la clé est obsolète. Remplacez-la par `yolo: false`. | ||
| 52 | + | ||
| 53 | +## Voir aussi | ||
| 54 | + | ||
| 55 | +- Chaque clé et sa valeur par défaut : [référence de la configuration](../reference/configuration.md) (`yolo`, `permissions`). | ||
| 56 | +- Ce que montre chaque façade : [référence de la CLI](../reference/cli.md), [référence ACP](../reference/acp.md#requêtes-sessionrequest_permission-envoyées). | ||
| 57 | +- Pourquoi yolo est la valeur par défaut et pourquoi l'agent qui interroge l'emporte : [Le mode yolo et les permissions](../explanation/yolo-and-permissions.md). | ||
modified
docs/fr/how-to/fetch-a-web-page.md +1 -1 | @@ -46,7 +46,7 @@ La page n'entre pas dans la conversation : une page longue n'épuise donc pas la | ||
| 46 | 46 | - **Tableaux complexes.** Les tableaux à cellules fusionnées sont gardés en HTML brut par défaut. Demandez des tableaux aplatis (« …en tableaux Markdown, aplatis-les ») et le modèle passe `tables: flatten`. |
| 47 | 47 | - **Texte barré et listes de tâches.** Demandez-les explicitement (« garde le texte barré », « garde les cases à cocher ») et le modèle passe `strikethrough: true` ou `taskList: true`. |
| 48 | 48 | - **Liens relatifs à un autre site.** Si la page est un miroir, demandez de résoudre les liens par rapport à l'original ; le modèle passe `baseUrl`. |
| 49 | -- **Dans un éditeur (ACP) ou avec `permissions: ask`.** Lire une page ne demande aucune permission ; l'enregistrer dans un fichier la demande d'abord, comme `write_file`. | |
| 49 | +- **Avec `yolo: false`.** Lire une page ne demande aucune permission ; l'enregistrer dans un fichier la demande d'abord, comme `write_file`. | |
| 50 | 50 | |
| 51 | 51 | ## En cas d'échec |
| 52 | 52 | |
| @@ -46,7 +46,7 @@ La page n'entre pas dans la conversation : une page longue n'épuise donc pas la | |||
| 46 | - **Tableaux complexes.** Les tableaux à cellules fusionnées sont gardés en HTML brut par défaut. Demandez des tableaux aplatis (« …en tableaux Markdown, aplatis-les ») et le modèle passe `tables: flatten`. | 46 | - **Tableaux complexes.** Les tableaux à cellules fusionnées sont gardés en HTML brut par défaut. Demandez des tableaux aplatis (« …en tableaux Markdown, aplatis-les ») et le modèle passe `tables: flatten`. |
| 47 | - **Texte barré et listes de tâches.** Demandez-les explicitement (« garde le texte barré », « garde les cases à cocher ») et le modèle passe `strikethrough: true` ou `taskList: true`. | 47 | - **Texte barré et listes de tâches.** Demandez-les explicitement (« garde le texte barré », « garde les cases à cocher ») et le modèle passe `strikethrough: true` ou `taskList: true`. |
| 48 | - **Liens relatifs à un autre site.** Si la page est un miroir, demandez de résoudre les liens par rapport à l'original ; le modèle passe `baseUrl`. | 48 | - **Liens relatifs à un autre site.** Si la page est un miroir, demandez de résoudre les liens par rapport à l'original ; le modèle passe `baseUrl`. |
| 49 | -- **Dans un éditeur (ACP) ou avec `permissions: ask`.** Lire une page ne demande aucune permission ; l'enregistrer dans un fichier la demande d'abord, comme `write_file`. | 49 | +- **Avec `yolo: false`.** Lire une page ne demande aucune permission ; l'enregistrer dans un fichier la demande d'abord, comme `write_file`. |
| 50 | 50 | ||
| 51 | ## En cas d'échec | 51 | ## En cas d'échec |
| 52 | 52 | ||
modified
docs/fr/how-to/use-from-an-editor.md +2 -2 | @@ -21,13 +21,13 @@ Ce guide montre comment héberger `mm` dans un éditeur qui parle l'Agent Client | ||
| 21 | 21 | |
| 22 | 22 | 2. Donnez à `AGENT_CONFIG` un chemin absolu. L'éditeur lance `mm` avec le répertoire du projet comme répertoire de travail, donc un `./agent.yaml` relatif ne serait pas trouvé. En mode ACP, un `skillsDir` relatif dans ce fichier est résolu à côté du fichier, pas à côté du répertoire de travail de l'éditeur. |
| 23 | 23 | |
| 24 | -3. Ouvrez le panneau d'agent et démarrez une conversation. Chaque appel à `bash`, `read_file`, `write_file` et `edit_file` apparaît comme un appel d'outil ; l'éditeur vous demande de l'autoriser avant exécution. `read_skill` s'exécute sans dialogue. | |
| 24 | +3. Ouvrez le panneau d'agent et démarrez une conversation. Chaque appel à `bash`, `read_file`, `write_file` et `edit_file` apparaît comme un appel d'outil. Par défaut (`yolo: true`), ils s'exécutent aussitôt ; avec `yolo: false` dans la configuration, l'éditeur vous demande d'autoriser chaque appel à `bash`, `write_file` et `edit_file` avant exécution. `read_file` et `read_skill` ne demandent jamais. | |
| 25 | 25 | |
| 26 | 26 | 4. Lisez les journaux quand quelque chose semble anormal. En mode ACP, rien de lisible par un humain ne va sur stdout : la bannière, les avertissements et une trace `[acp] …` vont sur stderr, que Zed affiche via `dev: open acp logs`. |
| 27 | 27 | |
| 28 | 28 | ## Variantes |
| 29 | 29 | |
| 30 | -- **Autoriser un outil pour toute la session.** Choisissez « Allow bash for this session » dans le dialogue de permission ; le choix est mémorisé par nom d'outil jusqu'à la fin de la session. | |
| 30 | +- **Autoriser ou refuser un outil pour toute la session** (`yolo: false`). Choisissez « Allow bash for this session » ou « Never allow bash in this session » dans le dialogue de permission ; le choix est mémorisé par nom d'outil jusqu'à la fin de la session. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | |
| 31 | 31 | - **Annuler un tour en cours.** Utilisez la touche d'arrêt de l'éditeur (Échap dans Zed). L'agent répond au prompt avec la raison d'arrêt `cancelled` et conserve dans l'historique les commandes déjà exécutées. Il n'y a pas de `/abort` en ACP. |
| 32 | 32 | - **Compresser ou recommencer la conversation.** Envoyez `/compact` ou `/new` ; les éditeurs qui listent les commandes de l'agent proposent les deux dans le menu des commandes slash. Voir la [référence des commandes slash](../reference/slash-commands.md#mode-acp). |
| 33 | 33 | - **Attacher un fichier avec `@`.** Le sélecteur de l'éditeur envoie un `resource_link`, transmis au modèle sous la forme `[attached file: <chemin>]` ; un chemin tapé à la main comme `@chemin` est compris de la même façon (voir [`@chemin`](attach-a-file.md)). Le modèle lit le fichier avec ses outils. |
| @@ -21,13 +21,13 @@ Ce guide montre comment héberger `mm` dans un éditeur qui parle l'Agent Client | |||
| 21 | 21 | ||
| 22 | 2. Donnez à `AGENT_CONFIG` un chemin absolu. L'éditeur lance `mm` avec le répertoire du projet comme répertoire de travail, donc un `./agent.yaml` relatif ne serait pas trouvé. En mode ACP, un `skillsDir` relatif dans ce fichier est résolu à côté du fichier, pas à côté du répertoire de travail de l'éditeur. | 22 | 2. Donnez à `AGENT_CONFIG` un chemin absolu. L'éditeur lance `mm` avec le répertoire du projet comme répertoire de travail, donc un `./agent.yaml` relatif ne serait pas trouvé. En mode ACP, un `skillsDir` relatif dans ce fichier est résolu à côté du fichier, pas à côté du répertoire de travail de l'éditeur. |
| 23 | 23 | ||
| 24 | -3. Ouvrez le panneau d'agent et démarrez une conversation. Chaque appel à `bash`, `read_file`, `write_file` et `edit_file` apparaît comme un appel d'outil ; l'éditeur vous demande de l'autoriser avant exécution. `read_skill` s'exécute sans dialogue. | 24 | +3. Ouvrez le panneau d'agent et démarrez une conversation. Chaque appel à `bash`, `read_file`, `write_file` et `edit_file` apparaît comme un appel d'outil. Par défaut (`yolo: true`), ils s'exécutent aussitôt ; avec `yolo: false` dans la configuration, l'éditeur vous demande d'autoriser chaque appel à `bash`, `write_file` et `edit_file` avant exécution. `read_file` et `read_skill` ne demandent jamais. |
| 25 | 25 | ||
| 26 | 4. Lisez les journaux quand quelque chose semble anormal. En mode ACP, rien de lisible par un humain ne va sur stdout : la bannière, les avertissements et une trace `[acp] …` vont sur stderr, que Zed affiche via `dev: open acp logs`. | 26 | 4. Lisez les journaux quand quelque chose semble anormal. En mode ACP, rien de lisible par un humain ne va sur stdout : la bannière, les avertissements et une trace `[acp] …` vont sur stderr, que Zed affiche via `dev: open acp logs`. |
| 27 | 27 | ||
| 28 | ## Variantes | 28 | ## Variantes |
| 29 | 29 | ||
| 30 | -- **Autoriser un outil pour toute la session.** Choisissez « Allow bash for this session » dans le dialogue de permission ; le choix est mémorisé par nom d'outil jusqu'à la fin de la session. | 30 | +- **Autoriser ou refuser un outil pour toute la session** (`yolo: false`). Choisissez « Allow bash for this session » ou « Never allow bash in this session » dans le dialogue de permission ; le choix est mémorisé par nom d'outil jusqu'à la fin de la session. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). |
| 31 | - **Annuler un tour en cours.** Utilisez la touche d'arrêt de l'éditeur (Échap dans Zed). L'agent répond au prompt avec la raison d'arrêt `cancelled` et conserve dans l'historique les commandes déjà exécutées. Il n'y a pas de `/abort` en ACP. | 31 | - **Annuler un tour en cours.** Utilisez la touche d'arrêt de l'éditeur (Échap dans Zed). L'agent répond au prompt avec la raison d'arrêt `cancelled` et conserve dans l'historique les commandes déjà exécutées. Il n'y a pas de `/abort` en ACP. |
| 32 | - **Compresser ou recommencer la conversation.** Envoyez `/compact` ou `/new` ; les éditeurs qui listent les commandes de l'agent proposent les deux dans le menu des commandes slash. Voir la [référence des commandes slash](../reference/slash-commands.md#mode-acp). | 32 | - **Compresser ou recommencer la conversation.** Envoyez `/compact` ou `/new` ; les éditeurs qui listent les commandes de l'agent proposent les deux dans le menu des commandes slash. Voir la [référence des commandes slash](../reference/slash-commands.md#mode-acp). |
| 33 | - **Attacher un fichier avec `@`.** Le sélecteur de l'éditeur envoie un `resource_link`, transmis au modèle sous la forme `[attached file: <chemin>]` ; un chemin tapé à la main comme `@chemin` est compris de la même façon (voir [`@chemin`](attach-a-file.md)). Le modèle lit le fichier avec ses outils. | 33 | - **Attacher un fichier avec `@`.** Le sélecteur de l'éditeur envoie un `resource_link`, transmis au modèle sous la forme `[attached file: <chemin>]` ; un chemin tapé à la main comme `@chemin` est compris de la même façon (voir [`@chemin`](attach-a-file.md)). Le modèle lit le fichier avec ses outils. |
modified
docs/fr/how-to/use-the-window-mode.md +1 -1 | @@ -26,7 +26,7 @@ Ce guide montre comment lancer mini-me dans une fenêtre unique en plein écran | ||
| 26 | 26 | |
| 27 | 27 | ## Variantes |
| 28 | 28 | |
| 29 | -- **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `permissions: ask` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et trois touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `auto`, exécute les outils comme le mode ligne l'a toujours fait. | |
| 29 | +- **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `yolo: false` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et quatre touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser, `n` ne jamais autoriser cet outil dans cette session. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `yolo: true`, exécute les outils sans demander. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | |
| 30 | 30 | - **Désigner un fichier au modèle.** `@chemin` fonctionne exactement comme dans le mode ligne : les chemins existants sont attachés et repris en lignes `📎`. Voir [Désigner un fichier au modèle avec `@chemin`](attach-a-file.md). |
| 31 | 31 | - **Conserver et reprendre des sessions.** Avec `sessions.enabled: true`, l'en-tête affiche l'identifiant de la session enregistrée ; `/sessions` et `/resume` fonctionnent comme dans le mode ligne. Voir [Conserver les sessions et les reprendre](keep-sessions.md). |
| 32 | 32 | - **Faire défiler la conversation.** PgUp/PgDn paginent ; **Tab** donne le focus à la conversation pour que ↑/↓, Début et Fin la fassent défiler ; Fin reprend le suivi de la fin. **Ctrl-L** efface la conversation affichée sans toucher à l'historique. La molette ne fait défiler qu'après **F2** (voir plus bas). |
| @@ -26,7 +26,7 @@ Ce guide montre comment lancer mini-me dans une fenêtre unique en plein écran | |||
| 26 | 26 | ||
| 27 | ## Variantes | 27 | ## Variantes |
| 28 | 28 | ||
| 29 | -- **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `permissions: ask` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et trois touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `auto`, exécute les outils comme le mode ligne l'a toujours fait. | 29 | +- **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `yolo: false` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et quatre touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser, `n` ne jamais autoriser cet outil dans cette session. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `yolo: true`, exécute les outils sans demander. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). |
| 30 | - **Désigner un fichier au modèle.** `@chemin` fonctionne exactement comme dans le mode ligne : les chemins existants sont attachés et repris en lignes `📎`. Voir [Désigner un fichier au modèle avec `@chemin`](attach-a-file.md). | 30 | - **Désigner un fichier au modèle.** `@chemin` fonctionne exactement comme dans le mode ligne : les chemins existants sont attachés et repris en lignes `📎`. Voir [Désigner un fichier au modèle avec `@chemin`](attach-a-file.md). |
| 31 | - **Conserver et reprendre des sessions.** Avec `sessions.enabled: true`, l'en-tête affiche l'identifiant de la session enregistrée ; `/sessions` et `/resume` fonctionnent comme dans le mode ligne. Voir [Conserver les sessions et les reprendre](keep-sessions.md). | 31 | - **Conserver et reprendre des sessions.** Avec `sessions.enabled: true`, l'en-tête affiche l'identifiant de la session enregistrée ; `/sessions` et `/resume` fonctionnent comme dans le mode ligne. Voir [Conserver les sessions et les reprendre](keep-sessions.md). |
| 32 | - **Faire défiler la conversation.** PgUp/PgDn paginent ; **Tab** donne le focus à la conversation pour que ↑/↓, Début et Fin la fassent défiler ; Fin reprend le suivi de la fin. **Ctrl-L** efface la conversation affichée sans toucher à l'historique. La molette ne fait défiler qu'après **F2** (voir plus bas). | 32 | - **Faire défiler la conversation.** PgUp/PgDn paginent ; **Tab** donne le focus à la conversation pour que ↑/↓, Début et Fin la fassent défiler ; Fin reprend le suivi de la fin. **Ctrl-L** efface la conversation affichée sans toucher à l'historique. La molette ne fait défiler qu'après **F2** (voir plus bas). |
modified
docs/fr/reference/acp.md +5 -2 | @@ -111,13 +111,16 @@ Les clients affichent le nom précédé d'une barre oblique et le renvoient comm | ||
| 111 | 111 | |
| 112 | 112 | ## Requêtes `session/request_permission` envoyées |
| 113 | 113 | |
| 114 | -Envoyées avant l'exécution de `bash`, `read_file`, `write_file` et `edit_file` ; jamais pour `read_skill`. | |
| 114 | +Envoyées seulement quand `yolo: false` (par défaut `true` : aucune n'est envoyée pour les outils intégrés), avant l'exécution de `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp` et des actions de `plan` qui modifient un plan ; jamais pour `read_skill`, `read_file`, `fetch_content` sans `output` ni les actions de navigation de `lsp`. Les outils MCP en envoient une quand `yolo: false` ou quand leur `permissions` vaut `ask`. | |
| 115 | 115 | |
| 116 | 116 | | Identifiant d'option | Genre | Effet | |
| 117 | 117 | |----------------------|-------|-------| |
| 118 | 118 | | `allow` | `allow_once` | Exécute l'outil. | |
| 119 | 119 | | `allow_always` | `allow_always` | Exécute l'outil et supprime le dialogue pour ce nom d'outil jusqu'à la fin de la session. | |
| 120 | -| `reject` | `reject_once` | N'exécute pas ; le modèle reçoit `[command rejected by the user]`. | | |
| 120 | +| `reject` | `reject_once` | N'exécute pas ; le modèle reçoit `[command rejected by the user]` (ou `[<outil> rejected by the user]`). | | |
| 121 | +| `never` | `reject_always` | N'exécute pas, et chaque appel ultérieur de ce nom d'outil dans la session est refusé sans dialogue. | | |
| 122 | + | |
| 123 | +Les réponses mémorisées (`allow_always`, `never`) survivent à `/new`. | |
| 121 | 124 | |
| 122 | 125 | Une erreur de transport ou un résultat `cancelled` compte comme un refus. |
| 123 | 126 | |
| @@ -111,13 +111,16 @@ Les clients affichent le nom précédé d'une barre oblique et le renvoient comm | |||
| 111 | 111 | ||
| 112 | ## Requêtes `session/request_permission` envoyées | 112 | ## Requêtes `session/request_permission` envoyées |
| 113 | 113 | ||
| 114 | -Envoyées avant l'exécution de `bash`, `read_file`, `write_file` et `edit_file` ; jamais pour `read_skill`. | 114 | +Envoyées seulement quand `yolo: false` (par défaut `true` : aucune n'est envoyée pour les outils intégrés), avant l'exécution de `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp` et des actions de `plan` qui modifient un plan ; jamais pour `read_skill`, `read_file`, `fetch_content` sans `output` ni les actions de navigation de `lsp`. Les outils MCP en envoient une quand `yolo: false` ou quand leur `permissions` vaut `ask`. |
| 115 | 115 | ||
| 116 | | Identifiant d'option | Genre | Effet | | 116 | | Identifiant d'option | Genre | Effet | |
| 117 | |----------------------|-------|-------| | 117 | |----------------------|-------|-------| |
| 118 | | `allow` | `allow_once` | Exécute l'outil. | | 118 | | `allow` | `allow_once` | Exécute l'outil. | |
| 119 | | `allow_always` | `allow_always` | Exécute l'outil et supprime le dialogue pour ce nom d'outil jusqu'à la fin de la session. | | 119 | | `allow_always` | `allow_always` | Exécute l'outil et supprime le dialogue pour ce nom d'outil jusqu'à la fin de la session. | |
| 120 | -| `reject` | `reject_once` | N'exécute pas ; le modèle reçoit `[command rejected by the user]`. | | 120 | +| `reject` | `reject_once` | N'exécute pas ; le modèle reçoit `[command rejected by the user]` (ou `[<outil> rejected by the user]`). | |
| 121 | +| `never` | `reject_always` | N'exécute pas, et chaque appel ultérieur de ce nom d'outil dans la session est refusé sans dialogue. | | ||
| 122 | + | ||
| 123 | +Les réponses mémorisées (`allow_always`, `never`) survivent à `/new`. | ||
| 121 | 124 | ||
| 122 | Une erreur de transport ou un résultat `cancelled` compte comme un refus. | 125 | Une erreur de transport ou un résultat `cancelled` compte comme un refus. |
| 123 | 126 | ||
modified
docs/fr/reference/cli.md +14 -5 | @@ -60,6 +60,7 @@ La première source existante gagne. | ||
| 60 | 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | |
| 61 | 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | |
| 62 | 62 | | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. | |
| 63 | +| `AGENT_YOLO` | Surcharge la clé `yolo` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Posée à `false` par `/ask` sur les agents qu'il lance, pour que l'agent qui interroge décide. | | |
| 63 | 64 | | `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. | |
| 64 | 65 | | `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. | |
| 65 | 66 | | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. | |
| @@ -71,8 +72,8 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | ||
| 71 | 72 | |
| 72 | 73 | | Ligne | Contenu | |
| 73 | 74 | |-------|---------| |
| 74 | -| `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`). | | |
| 75 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés. | | |
| 75 | +| `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | | |
| 76 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`. | | |
| 76 | 77 | | `Agent ready. Commands: …` | Mode terminal seulement. | |
| 77 | 78 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | |
| 78 | 79 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | |
| @@ -81,7 +82,7 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | ||
| 81 | 82 | |
| 82 | 83 | ``` |
| 83 | 84 | [warning: llamacpp: nothing answers at http://127.0.0.1:8080/v1 — start it: llama-server -m <model.gguf> --jinja --port 8080] |
| 84 | -mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | |
| 85 | +mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | yolo: on | |
| 85 | 86 | ``` |
| 86 | 87 | |
| 87 | 88 | ## Spécificités du mode ACP |
| @@ -92,6 +93,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 92 | 93 | | stderr | Bannière, avertissements, trace `[acp] …`, journaux du SDK. | |
| 93 | 94 | | Spinner | Désactivé. | |
| 94 | 95 | | `skillsDir` relatif | Résolu par rapport au répertoire du fichier de configuration, quand un fichier a été trouvé ; sinon depuis le répertoire de travail, comme en mode terminal. | |
| 96 | +| Permissions | `yolo: true` : aucune `session/request_permission` n'est envoyée pour les outils intégrés. `yolo: false` : une par appel, avec quatre options — `allow` (autoriser une fois), `allow_always` (cet outil pour la session), `reject` (refuser une fois), `never` (`reject_always` : cet outil, pour la session). Les outils MCP marqués `ask` en envoient toujours une. | | |
| 97 | + | |
| 98 | +## Spécificités du mode ligne | |
| 99 | + | |
| 100 | +| Aspect | Comportement | | |
| 101 | +|--------|--------------| | |
| 102 | +| Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), après la ligne `🛠️` de l'outil : `🔐 <titre de l'outil>`, la légende `[a] allow once · [s] allow <outil> for this session · [r] reject · [n] never <outil>` et ` ? `. La ligne tapée ensuite répond (`a`, `s`, `r`, `n` ou les mots `once`, `session`, `reject`, `never`) ; une autre réponse réaffiche la légende ; une ligne commençant par `/` est une commande (`/abort`, `/quit`), pas une réponse. `Ctrl+C` ou `/abort` refuse et arrête le tour. La même invite répond aux appels d'un agent interrogé avec `/ask`. | | |
| 95 | 103 | |
| 96 | 104 | ## Spécificités du mode fenêtre |
| 97 | 105 | |
| @@ -108,7 +116,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 108 | 116 | | Sélecteurs | `/` complète une commande, `@` un chemin sous le répertoire de travail, `$` un skill du catalogue — les deux derniers n'importe où dans une phrase, en conservant leur préfixe. Voir [Commandes slash](slash-commands.md#les-trois-sélecteurs). | |
| 109 | 117 | | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. | |
| 110 | 118 | | Erreurs | Une ligne `[error: <explication>]` ; un tour annulé affiche `🛑 Generation aborted.` ; chaque tour se termine par `⚙ N command(s) [· 📝 N file op(s)] [· 📖 N skill(s)]`. | |
| 111 | -| Permissions | Avec `permissions: ask`, la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject` jusqu'à la réponse ; un tour annulé pendant la question refuse. | | |
| 119 | +| Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject [n] never <outil>` jusqu'à la réponse (`Échap` = `r`) ; un tour annulé pendant la question refuse. | | |
| 112 | 120 | | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). | |
| 113 | 121 | | Souris | Non capturée au démarrage, donc le cliquer-glisser du terminal sélectionne du texte. `F2` la capture (défilement à la molette ; `Maj`+glisser sélectionne) puis la rend. | |
| 114 | 122 | |
| @@ -122,6 +130,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 122 | 130 | | Session | Non sauvegardée ; aucun répertoire de sessions utilisé. | |
| 123 | 131 | | Fichier de config | Peut apparaître avant ou après le flag `-prompt`/`-p` : `mm -p "prompt" config.yaml` et `mm config.yaml -p "prompt"` fonctionnent tous deux. | |
| 124 | 132 | | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. | |
| 133 | +| Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. | | |
| 125 | 134 | |
| 126 | 135 | ## Spécificités du mode ask |
| 127 | 136 | |
| @@ -132,7 +141,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 132 | 141 | | Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. | |
| 133 | 142 | | stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. | |
| 134 | 143 | | stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. | |
| 135 | -| Appels d'outils de l'agent | Autorisés, comme en mode ligne. | | |
| 144 | +| Appels d'outils de l'agent | Décidés par le `yolo` de cette configuration : `true` = autorisés ; `false` = refusés, avec la même ligne sur stderr qu'en mode invite (stdin portait la question, personne ne peut répondre). | | |
| 136 | 145 | | Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. | |
| 137 | 146 | | Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. | |
| 138 | 147 | |
| @@ -60,6 +60,7 @@ La première source existante gagne. | |||
| 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | | 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | |
| 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | | 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | |
| 62 | | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. | | 62 | | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. | |
| 63 | +| `AGENT_YOLO` | Surcharge la clé `yolo` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Posée à `false` par `/ask` sur les agents qu'il lance, pour que l'agent qui interroge décide. | | ||
| 63 | | `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. | | 64 | | `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. | |
| 64 | | `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. | | 65 | | `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. | |
| 65 | | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. | | 66 | | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. | |
| @@ -71,8 +72,8 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | |||
| 71 | 72 | ||
| 72 | | Ligne | Contenu | | 73 | | Ligne | Contenu | |
| 73 | |-------|---------| | 74 | |-------|---------| |
| 74 | -| `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`). | | 75 | +| `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | |
| 75 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés. | | 76 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`. | |
| 76 | | `Agent ready. Commands: …` | Mode terminal seulement. | | 77 | | `Agent ready. Commands: …` | Mode terminal seulement. | |
| 77 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | | 78 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | |
| 78 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | | 79 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | |
| @@ -81,7 +82,7 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | |||
| 81 | 82 | ||
| 82 | ``` | 83 | ``` |
| 83 | [warning: llamacpp: nothing answers at http://127.0.0.1:8080/v1 — start it: llama-server -m <model.gguf> --jinja --port 8080] | 84 | [warning: llamacpp: nothing answers at http://127.0.0.1:8080/v1 — start it: llama-server -m <model.gguf> --jinja --port 8080] |
| 84 | -mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | 85 | +mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ctx: unknown | tools: bash, read_file, write_file, edit_file | skills: 0 | yolo: on |
| 85 | ``` | 86 | ``` |
| 86 | 87 | ||
| 87 | ## Spécificités du mode ACP | 88 | ## Spécificités du mode ACP |
| @@ -92,6 +93,13 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 92 | | stderr | Bannière, avertissements, trace `[acp] …`, journaux du SDK. | | 93 | | stderr | Bannière, avertissements, trace `[acp] …`, journaux du SDK. | |
| 93 | | Spinner | Désactivé. | | 94 | | Spinner | Désactivé. | |
| 94 | | `skillsDir` relatif | Résolu par rapport au répertoire du fichier de configuration, quand un fichier a été trouvé ; sinon depuis le répertoire de travail, comme en mode terminal. | | 95 | | `skillsDir` relatif | Résolu par rapport au répertoire du fichier de configuration, quand un fichier a été trouvé ; sinon depuis le répertoire de travail, comme en mode terminal. | |
| 96 | +| Permissions | `yolo: true` : aucune `session/request_permission` n'est envoyée pour les outils intégrés. `yolo: false` : une par appel, avec quatre options — `allow` (autoriser une fois), `allow_always` (cet outil pour la session), `reject` (refuser une fois), `never` (`reject_always` : cet outil, pour la session). Les outils MCP marqués `ask` en envoient toujours une. | | ||
| 97 | + | ||
| 98 | +## Spécificités du mode ligne | ||
| 99 | + | ||
| 100 | +| Aspect | Comportement | | ||
| 101 | +|--------|--------------| | ||
| 102 | +| Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), après la ligne `🛠️` de l'outil : `🔐 <titre de l'outil>`, la légende `[a] allow once · [s] allow <outil> for this session · [r] reject · [n] never <outil>` et ` ? `. La ligne tapée ensuite répond (`a`, `s`, `r`, `n` ou les mots `once`, `session`, `reject`, `never`) ; une autre réponse réaffiche la légende ; une ligne commençant par `/` est une commande (`/abort`, `/quit`), pas une réponse. `Ctrl+C` ou `/abort` refuse et arrête le tour. La même invite répond aux appels d'un agent interrogé avec `/ask`. | | ||
| 95 | 103 | ||
| 96 | ## Spécificités du mode fenêtre | 104 | ## Spécificités du mode fenêtre |
| 97 | 105 | ||
| @@ -108,7 +116,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 108 | | Sélecteurs | `/` complète une commande, `@` un chemin sous le répertoire de travail, `$` un skill du catalogue — les deux derniers n'importe où dans une phrase, en conservant leur préfixe. Voir [Commandes slash](slash-commands.md#les-trois-sélecteurs). | | 116 | | Sélecteurs | `/` complète une commande, `@` un chemin sous le répertoire de travail, `$` un skill du catalogue — les deux derniers n'importe où dans une phrase, en conservant leur préfixe. Voir [Commandes slash](slash-commands.md#les-trois-sélecteurs). | |
| 109 | | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. | | 117 | | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. | |
| 110 | | Erreurs | Une ligne `[error: <explication>]` ; un tour annulé affiche `🛑 Generation aborted.` ; chaque tour se termine par `⚙ N command(s) [· 📝 N file op(s)] [· 📖 N skill(s)]`. | | 118 | | Erreurs | Une ligne `[error: <explication>]` ; un tour annulé affiche `🛑 Generation aborted.` ; chaque tour se termine par `⚙ N command(s) [· 📝 N file op(s)] [· 📖 N skill(s)]`. | |
| 111 | -| Permissions | Avec `permissions: ask`, la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject` jusqu'à la réponse ; un tour annulé pendant la question refuse. | | 119 | +| Permissions | Avec `yolo: false` (et pour les outils MCP marqués `ask`), la zone affiche `? <titre de l'outil>` et `[a] allow once [s] allow <outil> for this session [r] reject [n] never <outil>` jusqu'à la réponse (`Échap` = `r`) ; un tour annulé pendant la question refuse. | |
| 112 | | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). | | 120 | | Touches | Voir [Commandes slash — touches du mode fenêtre](slash-commands.md#touches-du-mode-fenêtre). | |
| 113 | | Souris | Non capturée au démarrage, donc le cliquer-glisser du terminal sélectionne du texte. `F2` la capture (défilement à la molette ; `Maj`+glisser sélectionne) puis la rend. | | 121 | | Souris | Non capturée au démarrage, donc le cliquer-glisser du terminal sélectionne du texte. `F2` la capture (défilement à la molette ; `Maj`+glisser sélectionne) puis la rend. | |
| 114 | 122 | ||
| @@ -122,6 +130,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 122 | | Session | Non sauvegardée ; aucun répertoire de sessions utilisé. | | 130 | | Session | Non sauvegardée ; aucun répertoire de sessions utilisé. | |
| 123 | | Fichier de config | Peut apparaître avant ou après le flag `-prompt`/`-p` : `mm -p "prompt" config.yaml` et `mm config.yaml -p "prompt"` fonctionnent tous deux. | | 131 | | Fichier de config | Peut apparaître avant ou après le flag `-prompt`/`-p` : `mm -p "prompt" config.yaml` et `mm config.yaml -p "prompt"` fonctionnent tous deux. | |
| 124 | | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. | | 132 | | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. | |
| 133 | +| Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. | | ||
| 125 | 134 | ||
| 126 | ## Spécificités du mode ask | 135 | ## Spécificités du mode ask |
| 127 | 136 | ||
| @@ -132,7 +141,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 132 | | Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. | | 141 | | Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. | |
| 133 | | stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. | | 142 | | stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. | |
| 134 | | stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. | | 143 | | stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. | |
| 135 | -| Appels d'outils de l'agent | Autorisés, comme en mode ligne. | | 144 | +| Appels d'outils de l'agent | Décidés par le `yolo` de cette configuration : `true` = autorisés ; `false` = refusés, avec la même ligne sur stderr qu'en mode invite (stdin portait la question, personne ne peut répondre). | |
| 136 | | Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. | | 145 | | Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. | |
| 137 | | Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. | | 146 | | Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. | |
| 138 | 147 | ||
modified
docs/fr/reference/configuration.md +5 -3 | @@ -31,7 +31,8 @@ | ||
| 31 | 31 | | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. | |
| 32 | 32 | | `lsp` | dictionnaire | voir ci-dessous | Serveurs de langage derrière l'outil `lsp`. Aucun serveur listé = pas d'outil. | |
| 33 | 33 | | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. | |
| 34 | -| `permissions` | chaîne | `auto` | `ask` : en mode fenêtre, `bash`, `write_file` et `edit_file` attendent la réponse de l'utilisateur (`a`/`s`/`r`) avant de s'exécuter. `auto` : les outils s'exécutent aussitôt. Le mode ligne et la façade ACP ne la lisent pas (ACP a son propre dialogue). Doit valoir `auto` ou `ask`. | | |
| 34 | +| `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | | |
| 35 | +| `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. | | |
| 35 | 36 | | `mmConfigs` | dictionnaire nom → chemin | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)). Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. | |
| 36 | 37 | |
| 37 | 38 | ## Clés de `context` |
| @@ -118,14 +119,15 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1 | ||
| 118 | 119 | | `lsp.servers[N].extensions must list at least one extension` | un serveur sans `extensions` | |
| 119 | 120 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point | |
| 120 | 121 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension | |
| 121 | -| `permissions must be "auto" or "ask"` | toute autre valeur | | |
| 122 | +| `permissions must be "auto" or "ask"` | toute autre valeur de la clé obsolète | | |
| 123 | +| `AGENT_YOLO="maybe": must be true or false` | une valeur d'`AGENT_YOLO` que `strconv.ParseBool` refuse (acceptées : `1`, `t`, `true`, `0`, `f`, `false`, sans distinction de casse) | | |
| 122 | 124 | | `bashTimeout must be > 0` | zéro ou négative | |
| 123 | 125 | | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` | |
| 124 | 126 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` | |
| 125 | 127 | | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` | |
| 126 | 128 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. | |
| 127 | 129 | |
| 128 | -Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`. | |
| 130 | +Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`). | |
| 129 | 131 | |
| 130 | 132 | ## Fichiers livrés |
| 131 | 133 | |
| @@ -31,7 +31,8 @@ | |||
| 31 | | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. | | 31 | | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. | |
| 32 | | `lsp` | dictionnaire | voir ci-dessous | Serveurs de langage derrière l'outil `lsp`. Aucun serveur listé = pas d'outil. | | 32 | | `lsp` | dictionnaire | voir ci-dessous | Serveurs de langage derrière l'outil `lsp`. Aucun serveur listé = pas d'outil. | |
| 33 | | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. | | 33 | | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. | |
| 34 | -| `permissions` | chaîne | `auto` | `ask` : en mode fenêtre, `bash`, `write_file` et `edit_file` attendent la réponse de l'utilisateur (`a`/`s`/`r`) avant de s'exécuter. `auto` : les outils s'exécutent aussitôt. Le mode ligne et la façade ACP ne la lisent pas (ACP a son propre dialogue). Doit valoir `auto` ou `ask`. | | 34 | +| `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | |
| 35 | +| `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. | | ||
| 35 | | `mmConfigs` | dictionnaire nom → chemin | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)). Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. | | 36 | | `mmConfigs` | dictionnaire nom → chemin | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)). Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. | |
| 36 | 37 | ||
| 37 | ## Clés de `context` | 38 | ## Clés de `context` |
| @@ -118,14 +119,15 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1 | |||
| 118 | | `lsp.servers[N].extensions must list at least one extension` | un serveur sans `extensions` | | 119 | | `lsp.servers[N].extensions must list at least one extension` | un serveur sans `extensions` | |
| 119 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point | | 120 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point | |
| 120 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension | | 121 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension | |
| 121 | -| `permissions must be "auto" or "ask"` | toute autre valeur | | 122 | +| `permissions must be "auto" or "ask"` | toute autre valeur de la clé obsolète | |
| 123 | +| `AGENT_YOLO="maybe": must be true or false` | une valeur d'`AGENT_YOLO` que `strconv.ParseBool` refuse (acceptées : `1`, `t`, `true`, `0`, `f`, `false`, sans distinction de casse) | | ||
| 122 | | `bashTimeout must be > 0` | zéro ou négative | | 124 | | `bashTimeout must be > 0` | zéro ou négative | |
| 123 | | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` | | 125 | | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` | |
| 124 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` | | 126 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` | |
| 125 | | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` | | 127 | | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` | |
| 126 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. | | 128 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. | |
| 127 | 129 | ||
| 128 | -Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`. | 130 | +Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`). |
| 129 | 131 | ||
| 130 | ## Fichiers livrés | 132 | ## Fichiers livrés |
| 131 | 133 | ||
modified
docs/fr/reference/mcp-configuration.md +1 -1 | @@ -47,7 +47,7 @@ mcp: | ||
| 47 | 47 | - **Valeurs valides :** `ask`, `auto` |
| 48 | 48 | - **Description :** Mode de permission par défaut pour les outils MCP. |
| 49 | 49 | - `ask` : Demande à l'utilisateur avant d'exécuter chaque outil MCP |
| 50 | - - `auto` : Exécute les outils MCP automatiquement sans demande | |
| 50 | + - `auto` : Exécute les outils MCP automatiquement sans demande — sauf avec `yolo: false` au niveau supérieur, qui fait demander chaque outil MCP comme les outils intégrés (voir la [configuration](configuration.md)) | |
| 51 | 51 | - **Exemple :** `permissions: ask` |
| 52 | 52 | |
| 53 | 53 | ### `discoveryTimeout` |
| @@ -47,7 +47,7 @@ mcp: | |||
| 47 | - **Valeurs valides :** `ask`, `auto` | 47 | - **Valeurs valides :** `ask`, `auto` |
| 48 | - **Description :** Mode de permission par défaut pour les outils MCP. | 48 | - **Description :** Mode de permission par défaut pour les outils MCP. |
| 49 | - `ask` : Demande à l'utilisateur avant d'exécuter chaque outil MCP | 49 | - `ask` : Demande à l'utilisateur avant d'exécuter chaque outil MCP |
| 50 | - - `auto` : Exécute les outils MCP automatiquement sans demande | 50 | + - `auto` : Exécute les outils MCP automatiquement sans demande — sauf avec `yolo: false` au niveau supérieur, qui fait demander chaque outil MCP comme les outils intégrés (voir la [configuration](configuration.md)) |
| 51 | - **Exemple :** `permissions: ask` | 51 | - **Exemple :** `permissions: ask` |
| 52 | 52 | ||
| 53 | ### `discoveryTimeout` | 53 | ### `discoveryTimeout` |
modified
docs/fr/reference/slash-commands.md +1 -1 | @@ -104,7 +104,7 @@ Les touches : | ||
| 104 | 104 | | `F1` | Comme `/help`. | |
| 105 | 105 | | `F2` | Bascule la capture de la souris. Désactivée (défaut) : le cliquer-glisser du terminal sélectionne du texte ; la barre d'état indique `F2 mouse: off`. Activée : la molette fait défiler, `Maj`+glisser sélectionne, la barre d'état indique `mouse: on (Shift+drag selects)`. | |
| 106 | 106 | | `Ctrl-Y` | Comme `/copy` : la dernière réponse dans le presse-papiers. | |
| 107 | -| `a` `s` `r` | Répondent à une permission en attente : autoriser une fois, autoriser cet outil pour la session, refuser. | | |
| 107 | +| `a` `s` `r` `n` | Répondent à une permission en attente (`yolo: false`, ou un outil MCP marqué `ask`) : autoriser une fois, autoriser cet outil pour la session, refuser cet appel (`Échap` aussi), ne jamais autoriser cet outil dans cette session. | | |
| 108 | 108 | | Molette | Fait défiler la conversation — seulement quand la souris est capturée (F2). | |
| 109 | 109 | | Collage encadré | Inséré en un bloc ; ses sauts de ligne deviennent des sauts de ligne, jamais un envoi. Au-delà de 3 lignes ou de 200 caractères : replié en un jeton `[Pasted #n · L lines · C chars]` qui s'édite comme un seul caractère ; Entrée envoie le texte complet, la conversation garde le jeton. Un `@chemin` dans un collage n'est pas résolu. | |
| 110 | 110 | |
| @@ -104,7 +104,7 @@ Les touches : | |||
| 104 | | `F1` | Comme `/help`. | | 104 | | `F1` | Comme `/help`. | |
| 105 | | `F2` | Bascule la capture de la souris. Désactivée (défaut) : le cliquer-glisser du terminal sélectionne du texte ; la barre d'état indique `F2 mouse: off`. Activée : la molette fait défiler, `Maj`+glisser sélectionne, la barre d'état indique `mouse: on (Shift+drag selects)`. | | 105 | | `F2` | Bascule la capture de la souris. Désactivée (défaut) : le cliquer-glisser du terminal sélectionne du texte ; la barre d'état indique `F2 mouse: off`. Activée : la molette fait défiler, `Maj`+glisser sélectionne, la barre d'état indique `mouse: on (Shift+drag selects)`. | |
| 106 | | `Ctrl-Y` | Comme `/copy` : la dernière réponse dans le presse-papiers. | | 106 | | `Ctrl-Y` | Comme `/copy` : la dernière réponse dans le presse-papiers. | |
| 107 | -| `a` `s` `r` | Répondent à une permission en attente : autoriser une fois, autoriser cet outil pour la session, refuser. | | 107 | +| `a` `s` `r` `n` | Répondent à une permission en attente (`yolo: false`, ou un outil MCP marqué `ask`) : autoriser une fois, autoriser cet outil pour la session, refuser cet appel (`Échap` aussi), ne jamais autoriser cet outil dans cette session. | |
| 108 | | Molette | Fait défiler la conversation — seulement quand la souris est capturée (F2). | | 108 | | Molette | Fait défiler la conversation — seulement quand la souris est capturée (F2). | |
| 109 | | Collage encadré | Inséré en un bloc ; ses sauts de ligne deviennent des sauts de ligne, jamais un envoi. Au-delà de 3 lignes ou de 200 caractères : replié en un jeton `[Pasted #n · L lines · C chars]` qui s'édite comme un seul caractère ; Entrée envoie le texte complet, la conversation garde le jeton. Un `@chemin` dans un collage n'est pas résolu. | | 109 | | Collage encadré | Inséré en un bloc ; ses sauts de ligne deviennent des sauts de ligne, jamais un envoi. Au-delà de 3 lignes ou de 200 caractères : replié en un jeton `[Pasted #n · L lines · C chars]` qui s'édite comme un seul caractère ; Entrée envoie le texte complet, la conversation garde le jeton. Un `@chemin` dans un collage n'est pas résolu. | |
| 110 | 110 | ||
modified
docs/fr/reference/tools.md +4 -2 | @@ -2,6 +2,8 @@ | ||
| 2 | 2 | |
| 3 | 3 | > Description neutre des outils déclarés au modèle. Un outil ne renvoie jamais d'erreur Go pour un échec ordinaire : le texte de l'échec est renvoyé au modèle comme sortie de l'outil. Chaque outil enregistre son appel dans le détecteur de boucle ; le même outil, la même entrée et la même sortie trois fois de suite ajoutent `[LOOP_DETECTED: …]` à la sortie. |
| 4 | 4 | |
| 5 | +> **Permissions.** « Permission demandée » ci-dessous s'applique quand `yolo: false` (voir la [configuration](configuration.md)), dans toutes les façades : l'invite `🔐` du mode ligne, la zone de la fenêtre, le dialogue de l'éditeur. Avec `yolo: true` (la valeur par défaut), les mêmes appels s'exécutent sans demander. Avec `-p` et `mm --ask`, un appel qui demanderait est refusé. Un refus renvoie `[<outil> rejected by the user]` (`[command rejected by the user]` pour `bash`) au modèle. | |
| 6 | + | |
| 5 | 7 | ## `bash` |
| 6 | 8 | |
| 7 | 9 | Déclaré quand `bashTool: true` (la valeur par défaut). |
| @@ -20,9 +22,9 @@ Déclaré quand `bashTool: true` (la valeur par défaut). | ||
| 20 | 22 | | Sortie très volumineuse | Relue par ses deux extrémités (1 Mio chacune) avant la troncature à `maxOutput`, avec `[... output too large to read back in full ...]` entre les deux. | |
| 21 | 23 | | Sortie | stdout et stderr fusionnés, tronqués à `maxOutput` caractères. | |
| 22 | 24 | | Code de sortie non nul | La sortie se termine par `[exit code: exit status N]`. | |
| 23 | -| Affichage terminal | `🛠️ bash: <command>` avant exécution, puis jusqu'à `previewLines` lignes grisées et indentées, ou `(no output)`. | | |
| 25 | +| Affichage terminal | `🛠️ bash: <command>` avant exécution (puis l'invite `🔐` quand la permission est demandée), puis jusqu'à `previewLines` lignes grisées et indentées, ou `(no output)`. | | |
| 24 | 26 | | ACP | Appel d'outil de genre `execute` ; permission demandée avant exécution ; un refus renvoie `[command rejected by the user]` avec le statut `failed`. | |
| 25 | -| Mode fenêtre | Une ligne `‣ bash <commande>` avec statut, durée et sortie repliée. Avec `permissions: ask`, attend `a`/`s`/`r` ; un refus renvoie le même texte qu'en ACP. | | |
| 27 | +| Mode fenêtre | Une ligne `‣ bash <commande>` avec statut, durée et sortie repliée. Permission demandée : attend `a`/`s`/`r`/`n` ; un refus renvoie le même texte qu'en ACP. | | |
| 26 | 28 | |
| 27 | 29 | ## `read_skill` |
| 28 | 30 | |
| @@ -2,6 +2,8 @@ | |||
| 2 | 2 | ||
| 3 | > Description neutre des outils déclarés au modèle. Un outil ne renvoie jamais d'erreur Go pour un échec ordinaire : le texte de l'échec est renvoyé au modèle comme sortie de l'outil. Chaque outil enregistre son appel dans le détecteur de boucle ; le même outil, la même entrée et la même sortie trois fois de suite ajoutent `[LOOP_DETECTED: …]` à la sortie. | 3 | > Description neutre des outils déclarés au modèle. Un outil ne renvoie jamais d'erreur Go pour un échec ordinaire : le texte de l'échec est renvoyé au modèle comme sortie de l'outil. Chaque outil enregistre son appel dans le détecteur de boucle ; le même outil, la même entrée et la même sortie trois fois de suite ajoutent `[LOOP_DETECTED: …]` à la sortie. |
| 4 | 4 | ||
| 5 | +> **Permissions.** « Permission demandée » ci-dessous s'applique quand `yolo: false` (voir la [configuration](configuration.md)), dans toutes les façades : l'invite `🔐` du mode ligne, la zone de la fenêtre, le dialogue de l'éditeur. Avec `yolo: true` (la valeur par défaut), les mêmes appels s'exécutent sans demander. Avec `-p` et `mm --ask`, un appel qui demanderait est refusé. Un refus renvoie `[<outil> rejected by the user]` (`[command rejected by the user]` pour `bash`) au modèle. | ||
| 6 | + | ||
| 5 | ## `bash` | 7 | ## `bash` |
| 6 | 8 | ||
| 7 | Déclaré quand `bashTool: true` (la valeur par défaut). | 9 | Déclaré quand `bashTool: true` (la valeur par défaut). |
| @@ -20,9 +22,9 @@ Déclaré quand `bashTool: true` (la valeur par défaut). | |||
| 20 | | Sortie très volumineuse | Relue par ses deux extrémités (1 Mio chacune) avant la troncature à `maxOutput`, avec `[... output too large to read back in full ...]` entre les deux. | | 22 | | Sortie très volumineuse | Relue par ses deux extrémités (1 Mio chacune) avant la troncature à `maxOutput`, avec `[... output too large to read back in full ...]` entre les deux. | |
| 21 | | Sortie | stdout et stderr fusionnés, tronqués à `maxOutput` caractères. | | 23 | | Sortie | stdout et stderr fusionnés, tronqués à `maxOutput` caractères. | |
| 22 | | Code de sortie non nul | La sortie se termine par `[exit code: exit status N]`. | | 24 | | Code de sortie non nul | La sortie se termine par `[exit code: exit status N]`. | |
| 23 | -| Affichage terminal | `🛠️ bash: <command>` avant exécution, puis jusqu'à `previewLines` lignes grisées et indentées, ou `(no output)`. | | 25 | +| Affichage terminal | `🛠️ bash: <command>` avant exécution (puis l'invite `🔐` quand la permission est demandée), puis jusqu'à `previewLines` lignes grisées et indentées, ou `(no output)`. | |
| 24 | | ACP | Appel d'outil de genre `execute` ; permission demandée avant exécution ; un refus renvoie `[command rejected by the user]` avec le statut `failed`. | | 26 | | ACP | Appel d'outil de genre `execute` ; permission demandée avant exécution ; un refus renvoie `[command rejected by the user]` avec le statut `failed`. | |
| 25 | -| Mode fenêtre | Une ligne `‣ bash <commande>` avec statut, durée et sortie repliée. Avec `permissions: ask`, attend `a`/`s`/`r` ; un refus renvoie le même texte qu'en ACP. | | 27 | +| Mode fenêtre | Une ligne `‣ bash <commande>` avec statut, durée et sortie repliée. Permission demandée : attend `a`/`s`/`r`/`n` ; un refus renvoie le même texte qu'en ACP. | |
| 26 | 28 | ||
| 27 | ## `read_skill` | 29 | ## `read_skill` |
| 28 | 30 | ||
modified
internal/acp/acp.go +6 -6 | @@ -33,6 +33,7 @@ import ( | ||
| 33 | 33 | "mm/internal/command" |
| 34 | 34 | "mm/internal/compact" |
| 35 | 35 | "mm/internal/config" |
| 36 | + "mm/internal/consent" | |
| 36 | 37 | "mm/internal/engine" |
| 37 | 38 | "mm/internal/mention" |
| 38 | 39 | history "mm/internal/session" |
| @@ -57,9 +58,9 @@ type session struct { | ||
| 57 | 58 | // an editor's session/load names the file directly. |
| 58 | 59 | record *store.Session |
| 59 | 60 | st *store.Store |
| 60 | - // allowAlways remembers the tools the user granted for the whole session | |
| 61 | - // ("allow always" in the permission dialog). | |
| 62 | - allowAlways map[string]bool | |
| 61 | + // policy remembers, per tool, the user's session-wide answers in the | |
| 62 | + // permission dialog: "allow always" and "never". | |
| 63 | + policy consent.Policy | |
| 63 | 64 | // usage is the token accounting: totals for the session/prompt response, |
| 64 | 65 | // the last measure for the context meter (usage.go). |
| 65 | 66 | usage usage |
| @@ -234,9 +235,8 @@ func (f *front) NewSession(ctx context.Context, p sdk.NewSessionRequest) (sdk.Ne | ||
| 234 | 235 | // persisted, so both modes log the same kind of id. |
| 235 | 236 | sid := sdk.SessionId(store.NewID()) |
| 236 | 237 | s := &session{ |
| 237 | - cwd: p.Cwd, // absolute, per the spec; where bash runs for this session | |
| 238 | - messages: history.Fresh(f.system), | |
| 239 | - allowAlways: map[string]bool{}, | |
| 238 | + cwd: p.Cwd, // absolute, per the spec; where bash runs for this session | |
| 239 | + messages: history.Fresh(f.system), | |
| 240 | 240 | } |
| 241 | 241 | f.persistNew(sid, s) |
| 242 | 242 | f.mu.Lock() |
| @@ -33,6 +33,7 @@ import ( | |||
| 33 | "mm/internal/command" | 33 | "mm/internal/command" |
| 34 | "mm/internal/compact" | 34 | "mm/internal/compact" |
| 35 | "mm/internal/config" | 35 | "mm/internal/config" |
| 36 | + "mm/internal/consent" | ||
| 36 | "mm/internal/engine" | 37 | "mm/internal/engine" |
| 37 | "mm/internal/mention" | 38 | "mm/internal/mention" |
| 38 | history "mm/internal/session" | 39 | history "mm/internal/session" |
| @@ -57,9 +58,9 @@ type session struct { | |||
| 57 | // an editor's session/load names the file directly. | 58 | // an editor's session/load names the file directly. |
| 58 | record *store.Session | 59 | record *store.Session |
| 59 | st *store.Store | 60 | st *store.Store |
| 60 | - // allowAlways remembers the tools the user granted for the whole session | 61 | + // policy remembers, per tool, the user's session-wide answers in the |
| 61 | - // ("allow always" in the permission dialog). | 62 | + // permission dialog: "allow always" and "never". |
| 62 | - allowAlways map[string]bool | 63 | + policy consent.Policy |
| 63 | // usage is the token accounting: totals for the session/prompt response, | 64 | // usage is the token accounting: totals for the session/prompt response, |
| 64 | // the last measure for the context meter (usage.go). | 65 | // the last measure for the context meter (usage.go). |
| 65 | usage usage | 66 | usage usage |
| @@ -234,9 +235,8 @@ func (f *front) NewSession(ctx context.Context, p sdk.NewSessionRequest) (sdk.Ne | |||
| 234 | // persisted, so both modes log the same kind of id. | 235 | // persisted, so both modes log the same kind of id. |
| 235 | sid := sdk.SessionId(store.NewID()) | 236 | sid := sdk.SessionId(store.NewID()) |
| 236 | s := &session{ | 237 | s := &session{ |
| 237 | - cwd: p.Cwd, // absolute, per the spec; where bash runs for this session | 238 | + cwd: p.Cwd, // absolute, per the spec; where bash runs for this session |
| 238 | - messages: history.Fresh(f.system), | 239 | + messages: history.Fresh(f.system), |
| 239 | - allowAlways: map[string]bool{}, | ||
| 240 | } | 240 | } |
| 241 | f.persistNew(sid, s) | 241 | f.persistNew(sid, s) |
| 242 | f.mu.Lock() | 242 | f.mu.Lock() |
modified
internal/acp/acp_test.go +14 -6 | @@ -13,6 +13,7 @@ import ( | ||
| 13 | 13 | "mm/internal/command" |
| 14 | 14 | "mm/internal/compact" |
| 15 | 15 | "mm/internal/config" |
| 16 | + "mm/internal/consent" | |
| 16 | 17 | history "mm/internal/session" |
| 17 | 18 | "mm/internal/version" |
| 18 | 19 | |
| @@ -94,10 +95,10 @@ func TestAvailableCommandsMatchWhatPromptIntercepts(t *testing.T) { | ||
| 94 | 95 | // the editor still sees the same session. |
| 95 | 96 | func TestResetSessionKeepsIdentityDropsHistory(t *testing.T) { |
| 96 | 97 | s := &session{ |
| 97 | - cwd: "/work/project", | |
| 98 | - messages: history.Fresh("you are bob"), | |
| 99 | - allowAlways: map[string]bool{"bash": true}, | |
| 98 | + cwd: "/work/project", | |
| 99 | + messages: history.Fresh("you are bob"), | |
| 100 | 100 | } |
| 101 | + s.policy.Record("bash", consent.AllowSession) | |
| 101 | 102 | s.messages = append(s.messages, ai.NewUserTextMessage("ls"), ai.NewModelTextMessage("main.go")) |
| 102 | 103 | |
| 103 | 104 | if got := resetSession(s, "you are bob"); got != 2 { |
| @@ -109,7 +110,7 @@ func TestResetSessionKeepsIdentityDropsHistory(t *testing.T) { | ||
| 109 | 110 | if s.cwd != "/work/project" { |
| 110 | 111 | t.Errorf("cwd changed to %q", s.cwd) |
| 111 | 112 | } |
| 112 | - if !s.allowAlways["bash"] { | |
| 113 | + if allowed, _ := s.policy.Remembered("bash"); !allowed { | |
| 113 | 114 | t.Error("the 'allow always' grant for bash was dropped") |
| 114 | 115 | } |
| 115 | 116 | |
| @@ -242,8 +243,15 @@ func oneTurn(q string) []*ai.Message { | ||
| 242 | 243 | } |
| 243 | 244 | } |
| 244 | 245 | |
| 246 | +// grantedBash reports whether the session remembers "allow always" for bash. | |
| 247 | +func grantedBash(s *session) bool { | |
| 248 | + allowed, _ := s.policy.Remembered("bash") | |
| 249 | + return allowed | |
| 250 | +} | |
| 251 | + | |
| 245 | 252 | func sessionWithTurns(qs ...string) *session { |
| 246 | - s := &session{cwd: "/work", messages: history.Fresh("You are Bob."), allowAlways: map[string]bool{"bash": true}} | |
| 253 | + s := &session{cwd: "/work", messages: history.Fresh("You are Bob.")} | |
| 254 | + s.policy.Record("bash", consent.AllowSession) | |
| 247 | 255 | for _, q := range qs { |
| 248 | 256 | s.messages = append(s.messages, oneTurn(q)...) |
| 249 | 257 | } |
| @@ -271,7 +279,7 @@ func TestCompactHistoryOutcomes(t *testing.T) { | ||
| 271 | 279 | if len(s.messages) >= before { |
| 272 | 280 | t.Errorf("history has %d messages after compaction, %d before", len(s.messages), before) |
| 273 | 281 | } |
| 274 | - if s.messages[0].Role != ai.RoleSystem || s.allowAlways["bash"] != true || s.cwd != "/work" { | |
| 282 | + if s.messages[0].Role != ai.RoleSystem || !grantedBash(s) || s.cwd != "/work" { | |
| 275 | 283 | t.Error("compaction touched something other than the turns") |
| 276 | 284 | } |
| 277 | 285 | }) |
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | "mm/internal/command" | 13 | "mm/internal/command" |
| 14 | "mm/internal/compact" | 14 | "mm/internal/compact" |
| 15 | "mm/internal/config" | 15 | "mm/internal/config" |
| 16 | + "mm/internal/consent" | ||
| 16 | history "mm/internal/session" | 17 | history "mm/internal/session" |
| 17 | "mm/internal/version" | 18 | "mm/internal/version" |
| 18 | 19 | ||
| @@ -94,10 +95,10 @@ func TestAvailableCommandsMatchWhatPromptIntercepts(t *testing.T) { | |||
| 94 | // the editor still sees the same session. | 95 | // the editor still sees the same session. |
| 95 | func TestResetSessionKeepsIdentityDropsHistory(t *testing.T) { | 96 | func TestResetSessionKeepsIdentityDropsHistory(t *testing.T) { |
| 96 | s := &session{ | 97 | s := &session{ |
| 97 | - cwd: "/work/project", | 98 | + cwd: "/work/project", |
| 98 | - messages: history.Fresh("you are bob"), | 99 | + messages: history.Fresh("you are bob"), |
| 99 | - allowAlways: map[string]bool{"bash": true}, | ||
| 100 | } | 100 | } |
| 101 | + s.policy.Record("bash", consent.AllowSession) | ||
| 101 | s.messages = append(s.messages, ai.NewUserTextMessage("ls"), ai.NewModelTextMessage("main.go")) | 102 | s.messages = append(s.messages, ai.NewUserTextMessage("ls"), ai.NewModelTextMessage("main.go")) |
| 102 | 103 | ||
| 103 | if got := resetSession(s, "you are bob"); got != 2 { | 104 | if got := resetSession(s, "you are bob"); got != 2 { |
| @@ -109,7 +110,7 @@ func TestResetSessionKeepsIdentityDropsHistory(t *testing.T) { | |||
| 109 | if s.cwd != "/work/project" { | 110 | if s.cwd != "/work/project" { |
| 110 | t.Errorf("cwd changed to %q", s.cwd) | 111 | t.Errorf("cwd changed to %q", s.cwd) |
| 111 | } | 112 | } |
| 112 | - if !s.allowAlways["bash"] { | 113 | + if allowed, _ := s.policy.Remembered("bash"); !allowed { |
| 113 | t.Error("the 'allow always' grant for bash was dropped") | 114 | t.Error("the 'allow always' grant for bash was dropped") |
| 114 | } | 115 | } |
| 115 | 116 | ||
| @@ -242,8 +243,15 @@ func oneTurn(q string) []*ai.Message { | |||
| 242 | } | 243 | } |
| 243 | } | 244 | } |
| 244 | 245 | ||
| 246 | +// grantedBash reports whether the session remembers "allow always" for bash. | ||
| 247 | +func grantedBash(s *session) bool { | ||
| 248 | + allowed, _ := s.policy.Remembered("bash") | ||
| 249 | + return allowed | ||
| 250 | +} | ||
| 251 | + | ||
| 245 | func sessionWithTurns(qs ...string) *session { | 252 | func sessionWithTurns(qs ...string) *session { |
| 246 | - s := &session{cwd: "/work", messages: history.Fresh("You are Bob."), allowAlways: map[string]bool{"bash": true}} | 253 | + s := &session{cwd: "/work", messages: history.Fresh("You are Bob.")} |
| 254 | + s.policy.Record("bash", consent.AllowSession) | ||
| 247 | for _, q := range qs { | 255 | for _, q := range qs { |
| 248 | s.messages = append(s.messages, oneTurn(q)...) | 256 | s.messages = append(s.messages, oneTurn(q)...) |
| 249 | } | 257 | } |
| @@ -271,7 +279,7 @@ func TestCompactHistoryOutcomes(t *testing.T) { | |||
| 271 | if len(s.messages) >= before { | 279 | if len(s.messages) >= before { |
| 272 | t.Errorf("history has %d messages after compaction, %d before", len(s.messages), before) | 280 | t.Errorf("history has %d messages after compaction, %d before", len(s.messages), before) |
| 273 | } | 281 | } |
| 274 | - if s.messages[0].Role != ai.RoleSystem || s.allowAlways["bash"] != true || s.cwd != "/work" { | 282 | + if s.messages[0].Role != ai.RoleSystem || !grantedBash(s) || s.cwd != "/work" { |
| 275 | t.Error("compaction touched something other than the turns") | 283 | t.Error("compaction touched something other than the turns") |
| 276 | } | 284 | } |
| 277 | }) | 285 | }) |
modified
internal/acp/persist.go +1 -1 | @@ -44,7 +44,7 @@ func (f *front) LoadSession(ctx context.Context, p sdk.LoadSessionRequest) (sdk. | ||
| 44 | 44 | // predate a change to agent.yaml, and the prompt is the agent's, not the |
| 45 | 45 | // conversation's. |
| 46 | 46 | rec.Messages = history.WithSystem(rec.Messages, f.system) |
| 47 | - s := &session{cwd: p.Cwd, messages: rec.Messages, allowAlways: map[string]bool{}, record: rec, st: st} | |
| 47 | + s := &session{cwd: p.Cwd, messages: rec.Messages, record: rec, st: st} | |
| 48 | 48 | f.mu.Lock() |
| 49 | 49 | f.sessions[p.SessionId] = s |
| 50 | 50 | f.mu.Unlock() |
| @@ -44,7 +44,7 @@ func (f *front) LoadSession(ctx context.Context, p sdk.LoadSessionRequest) (sdk. | |||
| 44 | // predate a change to agent.yaml, and the prompt is the agent's, not the | 44 | // predate a change to agent.yaml, and the prompt is the agent's, not the |
| 45 | // conversation's. | 45 | // conversation's. |
| 46 | rec.Messages = history.WithSystem(rec.Messages, f.system) | 46 | rec.Messages = history.WithSystem(rec.Messages, f.system) |
| 47 | - s := &session{cwd: p.Cwd, messages: rec.Messages, allowAlways: map[string]bool{}, record: rec, st: st} | 47 | + s := &session{cwd: p.Cwd, messages: rec.Messages, record: rec, st: st} |
| 48 | f.mu.Lock() | 48 | f.mu.Lock() |
| 49 | f.sessions[p.SessionId] = s | 49 | f.sessions[p.SessionId] = s |
| 50 | f.mu.Unlock() | 50 | f.mu.Unlock() |
modified
internal/acp/persist_test.go +2 -2 | @@ -209,7 +209,7 @@ func TestArchiveKeepsTheOldConversationUnderANewID(t *testing.T) { | ||
| 209 | 209 | withSessions(t) |
| 210 | 210 | cwd := t.TempDir() |
| 211 | 211 | f := &front{sessionsDir: ".mm/sessions", system: "sys"} |
| 212 | - s := &session{cwd: cwd, messages: storedHistory(), allowAlways: map[string]bool{}} | |
| 212 | + s := &session{cwd: cwd, messages: storedHistory()} | |
| 213 | 213 | f.persistNew("id-1", s) |
| 214 | 214 | |
| 215 | 215 | f.archive("id-1", s) |
| @@ -247,7 +247,7 @@ func TestFailIsPersistedAndArchivedWithItsConversation(t *testing.T) { | ||
| 247 | 247 | withSessions(t) |
| 248 | 248 | cwd := t.TempDir() |
| 249 | 249 | f := &front{sessionsDir: ".mm/sessions", system: "sys"} |
| 250 | - s := &session{cwd: cwd, messages: storedHistory(), allowAlways: map[string]bool{}} | |
| 250 | + s := &session{cwd: cwd, messages: storedHistory()} | |
| 251 | 251 | f.persistNew("id-1", s) |
| 252 | 252 | f.fail("id-1", s, store.NewFailure(s.messages, store.FailGeneration, "the server is down", nil)) |
| 253 | 253 | |
| @@ -209,7 +209,7 @@ func TestArchiveKeepsTheOldConversationUnderANewID(t *testing.T) { | |||
| 209 | withSessions(t) | 209 | withSessions(t) |
| 210 | cwd := t.TempDir() | 210 | cwd := t.TempDir() |
| 211 | f := &front{sessionsDir: ".mm/sessions", system: "sys"} | 211 | f := &front{sessionsDir: ".mm/sessions", system: "sys"} |
| 212 | - s := &session{cwd: cwd, messages: storedHistory(), allowAlways: map[string]bool{}} | 212 | + s := &session{cwd: cwd, messages: storedHistory()} |
| 213 | f.persistNew("id-1", s) | 213 | f.persistNew("id-1", s) |
| 214 | 214 | ||
| 215 | f.archive("id-1", s) | 215 | f.archive("id-1", s) |
| @@ -247,7 +247,7 @@ func TestFailIsPersistedAndArchivedWithItsConversation(t *testing.T) { | |||
| 247 | withSessions(t) | 247 | withSessions(t) |
| 248 | cwd := t.TempDir() | 248 | cwd := t.TempDir() |
| 249 | f := &front{sessionsDir: ".mm/sessions", system: "sys"} | 249 | f := &front{sessionsDir: ".mm/sessions", system: "sys"} |
| 250 | - s := &session{cwd: cwd, messages: storedHistory(), allowAlways: map[string]bool{}} | 250 | + s := &session{cwd: cwd, messages: storedHistory()} |
| 251 | f.persistNew("id-1", s) | 251 | f.persistNew("id-1", s) |
| 252 | f.fail("id-1", s, store.NewFailure(s.messages, store.FailGeneration, "the server is down", nil)) | 252 | f.fail("id-1", s, store.NewFailure(s.messages, store.FailGeneration, "the server is down", nil)) |
| 253 | 253 | ||
modified
internal/acp/sink.go +33 -18 | @@ -4,6 +4,7 @@ import ( | ||
| 4 | 4 | "context" |
| 5 | 5 | |
| 6 | 6 | "mm/internal/config" |
| 7 | + "mm/internal/consent" | |
| 7 | 8 | "mm/internal/ui" |
| 8 | 9 | |
| 9 | 10 | sdk "github.com/coder/acp-go-sdk" |
| @@ -69,14 +70,19 @@ func (k *sink) ToolStart(ev ui.ToolEvent) { | ||
| 69 | 70 | k.send(sdk.StartToolCall(sdk.ToolCallId(ev.ID), ev.Title, opts...)) |
| 70 | 71 | } |
| 71 | 72 | |
| 72 | -// Allow asks the user, through the editor, before the tool runs. This dialog | |
| 73 | -// simply does not exist in the terminal front end — it is the main thing an | |
| 74 | -// editor adds — so the policy lives here, not in the tools: "allow always" is | |
| 75 | -// remembered per tool for the rest of the session. | |
| 73 | +// Allow asks the user, through the editor, before the tool runs — when the | |
| 74 | +// caller decided to ask: consent.Gate (yolo: false) for the built-in tools, | |
| 75 | +// the MCP wrapper for its own. "Allow always" and "never" are remembered per | |
| 76 | +// tool for the rest of the session by the session's consent.Policy. | |
| 76 | 77 | func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { |
| 77 | - if k.s.allowAlways[ev.Tool] { | |
| 78 | - return true | |
| 79 | - } | |
| 78 | + return k.s.policy.Decide(ctx, ev.Tool, func(ctx context.Context) consent.Answer { | |
| 79 | + return k.ask(ctx, ev) | |
| 80 | + }) | |
| 81 | +} | |
| 82 | + | |
| 83 | +// ask opens the editor's dialog with consent's four answers, mapped onto | |
| 84 | +// ACP's four option kinds. | |
| 85 | +func (k *sink) ask(ctx context.Context, ev ui.ToolEvent) consent.Answer { | |
| 80 | 86 | resp, err := k.f.conn.RequestPermission(ctx, sdk.RequestPermissionRequest{ |
| 81 | 87 | SessionId: k.sid, |
| 82 | 88 | ToolCall: sdk.ToolCallUpdate{ |
| @@ -90,24 +96,33 @@ func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { | ||
| 90 | 96 | {OptionId: "allow", Name: "Allow once", Kind: sdk.PermissionOptionKindAllowOnce}, |
| 91 | 97 | {OptionId: "allow_always", Name: "Allow " + ev.Tool + " for this session", Kind: sdk.PermissionOptionKindAllowAlways}, |
| 92 | 98 | {OptionId: "reject", Name: "Reject", Kind: sdk.PermissionOptionKindRejectOnce}, |
| 99 | + {OptionId: "never", Name: "Never allow " + ev.Tool + " in this session", Kind: sdk.PermissionOptionKindRejectAlways}, | |
| 93 | 100 | }, |
| 94 | 101 | }) |
| 95 | 102 | // An error or a cancelled outcome both mean "do not run": the client hung |
| 96 | 103 | // up, or the user hit stop while the dialog was open — the spec requires |
| 97 | 104 | // the client to answer "cancelled" in that case. |
| 98 | - if err != nil || resp.Outcome.Cancelled != nil { | |
| 99 | - return false | |
| 105 | + if err != nil { | |
| 106 | + return consent.RejectOnce | |
| 107 | + } | |
| 108 | + return answerOf(resp.Outcome) | |
| 109 | +} | |
| 110 | + | |
| 111 | +// answerOf reads the editor's choice. A cancelled outcome, or one naming no | |
| 112 | +// option we offered, means "do not run". | |
| 113 | +func answerOf(o sdk.RequestPermissionOutcome) consent.Answer { | |
| 114 | + if o.Cancelled != nil || o.Selected == nil { | |
| 115 | + return consent.RejectOnce | |
| 100 | 116 | } |
| 101 | - if sel := resp.Outcome.Selected; sel != nil { | |
| 102 | - switch string(sel.OptionId) { | |
| 103 | - case "allow": | |
| 104 | - return true | |
| 105 | - case "allow_always": | |
| 106 | - k.s.allowAlways[ev.Tool] = true | |
| 107 | - return true | |
| 108 | - } | |
| 117 | + switch string(o.Selected.OptionId) { | |
| 118 | + case "allow": | |
| 119 | + return consent.AllowOnce | |
| 120 | + case "allow_always": | |
| 121 | + return consent.AllowSession | |
| 122 | + case "never": | |
| 123 | + return consent.Never | |
| 109 | 124 | } |
| 110 | - return false | |
| 125 | + return consent.RejectOnce | |
| 111 | 126 | } |
| 112 | 127 | |
| 113 | 128 | // ToolRunning flips the call to in_progress: permission granted, executing. |
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | "context" | 4 | "context" |
| 5 | 5 | ||
| 6 | "mm/internal/config" | 6 | "mm/internal/config" |
| 7 | + "mm/internal/consent" | ||
| 7 | "mm/internal/ui" | 8 | "mm/internal/ui" |
| 8 | 9 | ||
| 9 | sdk "github.com/coder/acp-go-sdk" | 10 | sdk "github.com/coder/acp-go-sdk" |
| @@ -69,14 +70,19 @@ func (k *sink) ToolStart(ev ui.ToolEvent) { | |||
| 69 | k.send(sdk.StartToolCall(sdk.ToolCallId(ev.ID), ev.Title, opts...)) | 70 | k.send(sdk.StartToolCall(sdk.ToolCallId(ev.ID), ev.Title, opts...)) |
| 70 | } | 71 | } |
| 71 | 72 | ||
| 72 | -// Allow asks the user, through the editor, before the tool runs. This dialog | 73 | +// Allow asks the user, through the editor, before the tool runs — when the |
| 73 | -// simply does not exist in the terminal front end — it is the main thing an | 74 | +// caller decided to ask: consent.Gate (yolo: false) for the built-in tools, |
| 74 | -// editor adds — so the policy lives here, not in the tools: "allow always" is | 75 | +// the MCP wrapper for its own. "Allow always" and "never" are remembered per |
| 75 | -// remembered per tool for the rest of the session. | 76 | +// tool for the rest of the session by the session's consent.Policy. |
| 76 | func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { | 77 | func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { |
| 77 | - if k.s.allowAlways[ev.Tool] { | 78 | + return k.s.policy.Decide(ctx, ev.Tool, func(ctx context.Context) consent.Answer { |
| 78 | - return true | 79 | + return k.ask(ctx, ev) |
| 79 | - } | 80 | + }) |
| 81 | +} | ||
| 82 | + | ||
| 83 | +// ask opens the editor's dialog with consent's four answers, mapped onto | ||
| 84 | +// ACP's four option kinds. | ||
| 85 | +func (k *sink) ask(ctx context.Context, ev ui.ToolEvent) consent.Answer { | ||
| 80 | resp, err := k.f.conn.RequestPermission(ctx, sdk.RequestPermissionRequest{ | 86 | resp, err := k.f.conn.RequestPermission(ctx, sdk.RequestPermissionRequest{ |
| 81 | SessionId: k.sid, | 87 | SessionId: k.sid, |
| 82 | ToolCall: sdk.ToolCallUpdate{ | 88 | ToolCall: sdk.ToolCallUpdate{ |
| @@ -90,24 +96,33 @@ func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { | |||
| 90 | {OptionId: "allow", Name: "Allow once", Kind: sdk.PermissionOptionKindAllowOnce}, | 96 | {OptionId: "allow", Name: "Allow once", Kind: sdk.PermissionOptionKindAllowOnce}, |
| 91 | {OptionId: "allow_always", Name: "Allow " + ev.Tool + " for this session", Kind: sdk.PermissionOptionKindAllowAlways}, | 97 | {OptionId: "allow_always", Name: "Allow " + ev.Tool + " for this session", Kind: sdk.PermissionOptionKindAllowAlways}, |
| 92 | {OptionId: "reject", Name: "Reject", Kind: sdk.PermissionOptionKindRejectOnce}, | 98 | {OptionId: "reject", Name: "Reject", Kind: sdk.PermissionOptionKindRejectOnce}, |
| 99 | + {OptionId: "never", Name: "Never allow " + ev.Tool + " in this session", Kind: sdk.PermissionOptionKindRejectAlways}, | ||
| 93 | }, | 100 | }, |
| 94 | }) | 101 | }) |
| 95 | // An error or a cancelled outcome both mean "do not run": the client hung | 102 | // An error or a cancelled outcome both mean "do not run": the client hung |
| 96 | // up, or the user hit stop while the dialog was open — the spec requires | 103 | // up, or the user hit stop while the dialog was open — the spec requires |
| 97 | // the client to answer "cancelled" in that case. | 104 | // the client to answer "cancelled" in that case. |
| 98 | - if err != nil || resp.Outcome.Cancelled != nil { | 105 | + if err != nil { |
| 99 | - return false | 106 | + return consent.RejectOnce |
| 107 | + } | ||
| 108 | + return answerOf(resp.Outcome) | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +// answerOf reads the editor's choice. A cancelled outcome, or one naming no | ||
| 112 | +// option we offered, means "do not run". | ||
| 113 | +func answerOf(o sdk.RequestPermissionOutcome) consent.Answer { | ||
| 114 | + if o.Cancelled != nil || o.Selected == nil { | ||
| 115 | + return consent.RejectOnce | ||
| 100 | } | 116 | } |
| 101 | - if sel := resp.Outcome.Selected; sel != nil { | 117 | + switch string(o.Selected.OptionId) { |
| 102 | - switch string(sel.OptionId) { | 118 | + case "allow": |
| 103 | - case "allow": | 119 | + return consent.AllowOnce |
| 104 | - return true | 120 | + case "allow_always": |
| 105 | - case "allow_always": | 121 | + return consent.AllowSession |
| 106 | - k.s.allowAlways[ev.Tool] = true | 122 | + case "never": |
| 107 | - return true | 123 | + return consent.Never |
| 108 | - } | ||
| 109 | } | 124 | } |
| 110 | - return false | 125 | + return consent.RejectOnce |
| 111 | } | 126 | } |
| 112 | 127 | ||
| 113 | // ToolRunning flips the call to in_progress: permission granted, executing. | 128 | // ToolRunning flips the call to in_progress: permission granted, executing. |
added
internal/acp/sink_test.go +33 -0 | new file mode 100644 | ||
| @@ -0,0 +1,33 @@ | ||
| 1 | +package acp | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "testing" | |
| 5 | + | |
| 6 | + "mm/internal/consent" | |
| 7 | + | |
| 8 | + sdk "github.com/coder/acp-go-sdk" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// Each of the four options of the dialog maps onto consent's answer of the | |
| 12 | +// same meaning; a cancelled dialog or an unknown option does not run. | |
| 13 | +func TestEditorChoicesMapOntoConsentAnswers(t *testing.T) { | |
| 14 | + selected := func(id string) sdk.RequestPermissionOutcome { | |
| 15 | + return sdk.RequestPermissionOutcome{Selected: &sdk.RequestPermissionOutcomeSelected{OptionId: sdk.PermissionOptionId(id)}} | |
| 16 | + } | |
| 17 | + cases := map[string]struct { | |
| 18 | + outcome sdk.RequestPermissionOutcome | |
| 19 | + want consent.Answer | |
| 20 | + }{ | |
| 21 | + "allow once": {selected("allow"), consent.AllowOnce}, | |
| 22 | + "allow always": {selected("allow_always"), consent.AllowSession}, | |
| 23 | + "reject": {selected("reject"), consent.RejectOnce}, | |
| 24 | + "never": {selected("never"), consent.Never}, | |
| 25 | + "unknown": {selected("maybe"), consent.RejectOnce}, | |
| 26 | + "cancelled": {sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}}, consent.RejectOnce}, | |
| 27 | + } | |
| 28 | + for name, c := range cases { | |
| 29 | + if got := answerOf(c.outcome); got != c.want { | |
| 30 | + t.Errorf("%s: answerOf = %v, want %v", name, got, c.want) | |
| 31 | + } | |
| 32 | + } | |
| 33 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | +package acp | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "testing" | ||
| 5 | + | ||
| 6 | + "mm/internal/consent" | ||
| 7 | + | ||
| 8 | + sdk "github.com/coder/acp-go-sdk" | ||
| 9 | +) | ||
| 10 | + | ||
| 11 | +// Each of the four options of the dialog maps onto consent's answer of the | ||
| 12 | +// same meaning; a cancelled dialog or an unknown option does not run. | ||
| 13 | +func TestEditorChoicesMapOntoConsentAnswers(t *testing.T) { | ||
| 14 | + selected := func(id string) sdk.RequestPermissionOutcome { | ||
| 15 | + return sdk.RequestPermissionOutcome{Selected: &sdk.RequestPermissionOutcomeSelected{OptionId: sdk.PermissionOptionId(id)}} | ||
| 16 | + } | ||
| 17 | + cases := map[string]struct { | ||
| 18 | + outcome sdk.RequestPermissionOutcome | ||
| 19 | + want consent.Answer | ||
| 20 | + }{ | ||
| 21 | + "allow once": {selected("allow"), consent.AllowOnce}, | ||
| 22 | + "allow always": {selected("allow_always"), consent.AllowSession}, | ||
| 23 | + "reject": {selected("reject"), consent.RejectOnce}, | ||
| 24 | + "never": {selected("never"), consent.Never}, | ||
| 25 | + "unknown": {selected("maybe"), consent.RejectOnce}, | ||
| 26 | + "cancelled": {sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}}, consent.RejectOnce}, | ||
| 27 | + } | ||
| 28 | + for name, c := range cases { | ||
| 29 | + if got := answerOf(c.outcome); got != c.want { | ||
| 30 | + t.Errorf("%s: answerOf = %v, want %v", name, got, c.want) | ||
| 31 | + } | ||
| 32 | + } | ||
| 33 | +} | ||
modified
internal/agent/agent.go +19 -1 | @@ -15,6 +15,7 @@ import ( | ||
| 15 | 15 | "mm/internal/command" |
| 16 | 16 | "mm/internal/compact" |
| 17 | 17 | "mm/internal/config" |
| 18 | + "mm/internal/consent" | |
| 18 | 19 | "mm/internal/detector" |
| 19 | 20 | "mm/internal/engine" |
| 20 | 21 | "mm/internal/mention" |
| @@ -94,6 +95,12 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | ||
| 94 | 95 | |
| 95 | 96 | startInputReader() |
| 96 | 97 | |
| 98 | + // The permission prompt (yolo: false, MCP tools marked "ask"): the tools | |
| 99 | + // post their question, the waiting rooms below print it and route the | |
| 100 | + // next typed line to it. | |
| 101 | + prompt := newLinePrompt() | |
| 102 | + defer consent.SetPrompter(prompt.allow)() | |
| 103 | + | |
| 97 | 104 | fmt.Println(`Agent ready. Commands: "/quit" to exit, "/abort" to stop current generation, "/compact" to compress the history, "/new" to start a new session, "/sessions" and "/resume" for stored sessions, "/help" to list them.`) |
| 98 | 105 | fmt.Println(`Shortcut: Ctrl+C to abort generation, or Ctrl+C at prompt to quit.`) |
| 99 | 106 | if hint := keep.Hint(); hint != "" { |
| @@ -152,7 +159,7 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | ||
| 152 | 159 | continue |
| 153 | 160 | } |
| 154 | 161 | var quit bool |
| 155 | - messages, quit = asking{pool: pool, cwd: cwd, keep: keep, sigCh: sigCh, inputCh: inputCh}.run(ctx, req, messages) | |
| 162 | + messages, quit = asking{pool: pool, cwd: cwd, keep: keep, sigCh: sigCh, inputCh: inputCh, prompt: prompt}.run(ctx, req, messages) | |
| 156 | 163 | if quit { |
| 157 | 164 | fmt.Println("Goodbye.") |
| 158 | 165 | return |
| @@ -227,6 +234,7 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | ||
| 227 | 234 | case res := <-resultCh: |
| 228 | 235 | generationRunning = false |
| 229 | 236 | generating = false |
| 237 | + prompt.drop() | |
| 230 | 238 | |
| 231 | 239 | // We take the FULL history returned by engine.Generate, not just |
| 232 | 240 | // resp.Message — and we take it EVEN when the generation failed |
| @@ -303,11 +311,21 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | ||
| 303 | 311 | // and without it this message lands on its line — the next |
| 304 | 312 | // frame would erase it. Same rule as in dmr.emit. |
| 305 | 313 | spinner.Stop() |
| 314 | + prompt.drop() | |
| 306 | 315 | fmt.Println("\n🛑 Interrupted (Ctrl+C).") |
| 307 | 316 | cancelGen() |
| 308 | 317 | // We don't set generationRunning=false; we wait for resultCh to catch the Canceled error. |
| 309 | 318 | |
| 319 | + case req := <-prompt.requestCh(): | |
| 320 | + prompt.show(req) | |
| 321 | + | |
| 310 | 322 | case nextInput := <-inputCh: |
| 323 | + // A line typed while a tool waits for permission is its answer; | |
| 324 | + // anything else stops the turn, question included. | |
| 325 | + if prompt.answer(nextInput) { | |
| 326 | + continue | |
| 327 | + } | |
| 328 | + prompt.drop() | |
| 311 | 329 | spinner.Stop() // le spinner tourne : on lui reprend la ligne |
| 312 | 330 | switch { |
| 313 | 331 | case command.Is(nextInput, command.Abort): |
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | "mm/internal/command" | 15 | "mm/internal/command" |
| 16 | "mm/internal/compact" | 16 | "mm/internal/compact" |
| 17 | "mm/internal/config" | 17 | "mm/internal/config" |
| 18 | + "mm/internal/consent" | ||
| 18 | "mm/internal/detector" | 19 | "mm/internal/detector" |
| 19 | "mm/internal/engine" | 20 | "mm/internal/engine" |
| 20 | "mm/internal/mention" | 21 | "mm/internal/mention" |
| @@ -94,6 +95,12 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | |||
| 94 | 95 | ||
| 95 | startInputReader() | 96 | startInputReader() |
| 96 | 97 | ||
| 98 | + // The permission prompt (yolo: false, MCP tools marked "ask"): the tools | ||
| 99 | + // post their question, the waiting rooms below print it and route the | ||
| 100 | + // next typed line to it. | ||
| 101 | + prompt := newLinePrompt() | ||
| 102 | + defer consent.SetPrompter(prompt.allow)() | ||
| 103 | + | ||
| 97 | fmt.Println(`Agent ready. Commands: "/quit" to exit, "/abort" to stop current generation, "/compact" to compress the history, "/new" to start a new session, "/sessions" and "/resume" for stored sessions, "/help" to list them.`) | 104 | fmt.Println(`Agent ready. Commands: "/quit" to exit, "/abort" to stop current generation, "/compact" to compress the history, "/new" to start a new session, "/sessions" and "/resume" for stored sessions, "/help" to list them.`) |
| 98 | fmt.Println(`Shortcut: Ctrl+C to abort generation, or Ctrl+C at prompt to quit.`) | 105 | fmt.Println(`Shortcut: Ctrl+C to abort generation, or Ctrl+C at prompt to quit.`) |
| 99 | if hint := keep.Hint(); hint != "" { | 106 | if hint := keep.Hint(); hint != "" { |
| @@ -152,7 +159,7 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | |||
| 152 | continue | 159 | continue |
| 153 | } | 160 | } |
| 154 | var quit bool | 161 | var quit bool |
| 155 | - messages, quit = asking{pool: pool, cwd: cwd, keep: keep, sigCh: sigCh, inputCh: inputCh}.run(ctx, req, messages) | 162 | + messages, quit = asking{pool: pool, cwd: cwd, keep: keep, sigCh: sigCh, inputCh: inputCh, prompt: prompt}.run(ctx, req, messages) |
| 156 | if quit { | 163 | if quit { |
| 157 | fmt.Println("Goodbye.") | 164 | fmt.Println("Goodbye.") |
| 158 | return | 165 | return |
| @@ -227,6 +234,7 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | |||
| 227 | case res := <-resultCh: | 234 | case res := <-resultCh: |
| 228 | generationRunning = false | 235 | generationRunning = false |
| 229 | generating = false | 236 | generating = false |
| 237 | + prompt.drop() | ||
| 230 | 238 | ||
| 231 | // We take the FULL history returned by engine.Generate, not just | 239 | // We take the FULL history returned by engine.Generate, not just |
| 232 | // resp.Message — and we take it EVEN when the generation failed | 240 | // resp.Message — and we take it EVEN when the generation failed |
| @@ -303,11 +311,21 @@ func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRe | |||
| 303 | // and without it this message lands on its line — the next | 311 | // and without it this message lands on its line — the next |
| 304 | // frame would erase it. Same rule as in dmr.emit. | 312 | // frame would erase it. Same rule as in dmr.emit. |
| 305 | spinner.Stop() | 313 | spinner.Stop() |
| 314 | + prompt.drop() | ||
| 306 | fmt.Println("\n🛑 Interrupted (Ctrl+C).") | 315 | fmt.Println("\n🛑 Interrupted (Ctrl+C).") |
| 307 | cancelGen() | 316 | cancelGen() |
| 308 | // We don't set generationRunning=false; we wait for resultCh to catch the Canceled error. | 317 | // We don't set generationRunning=false; we wait for resultCh to catch the Canceled error. |
| 309 | 318 | ||
| 319 | + case req := <-prompt.requestCh(): | ||
| 320 | + prompt.show(req) | ||
| 321 | + | ||
| 310 | case nextInput := <-inputCh: | 322 | case nextInput := <-inputCh: |
| 323 | + // A line typed while a tool waits for permission is its answer; | ||
| 324 | + // anything else stops the turn, question included. | ||
| 325 | + if prompt.answer(nextInput) { | ||
| 326 | + continue | ||
| 327 | + } | ||
| 328 | + prompt.drop() | ||
| 311 | spinner.Stop() // le spinner tourne : on lui reprend la ligne | 329 | spinner.Stop() // le spinner tourne : on lui reprend la ligne |
| 312 | switch { | 330 | switch { |
| 313 | case command.Is(nextInput, command.Abort): | 331 | case command.Is(nextInput, command.Abort): |
modified
internal/agent/ask.go +16 -5 | @@ -8,6 +8,7 @@ import ( | ||
| 8 | 8 | "mm/internal/ask" |
| 9 | 9 | "mm/internal/command" |
| 10 | 10 | "mm/internal/config" |
| 11 | + "mm/internal/consent" | |
| 11 | 12 | "mm/internal/spinner" |
| 12 | 13 | "mm/internal/store" |
| 13 | 14 | "mm/internal/ui" |
| @@ -24,6 +25,7 @@ type asking struct { | ||
| 24 | 25 | keep *store.Keeper |
| 25 | 26 | sigCh <-chan os.Signal |
| 26 | 27 | inputCh <-chan string |
| 28 | + prompt *linePrompt // the asked agent's calls may need the user's answer | |
| 27 | 29 | } |
| 28 | 30 | |
| 29 | 31 | // run asks req.Agent, streaming its answer and tool calls, and returns the |
| @@ -48,14 +50,22 @@ func (a asking) run(ctx context.Context, req command.AskRequest, messages []*ai. | ||
| 48 | 50 | case res := <-done: |
| 49 | 51 | return a.settle(askCtx, req, messages, res.exchange, res.err), quit |
| 50 | 52 | case <-a.sigCh: |
| 53 | + a.prompt.drop() | |
| 51 | 54 | fmt.Println("\n🛑 Interrupted (Ctrl+C).") |
| 52 | 55 | cancel() |
| 56 | + case req := <-a.prompt.requestCh(): | |
| 57 | + a.prompt.show(req) | |
| 53 | 58 | case input := <-a.inputCh: |
| 59 | + if a.prompt.answer(input) { | |
| 60 | + continue | |
| 61 | + } | |
| 54 | 62 | switch { |
| 55 | 63 | case command.Is(input, command.Abort): |
| 64 | + a.prompt.drop() | |
| 56 | 65 | fmt.Println("🛑 Aborting…") |
| 57 | 66 | cancel() |
| 58 | 67 | case command.Is(input, command.Quit): |
| 68 | + a.prompt.drop() | |
| 59 | 69 | quit = true |
| 60 | 70 | cancel() |
| 61 | 71 | default: |
| @@ -84,8 +94,9 @@ func (a asking) settle(askCtx context.Context, req command.AskRequest, messages, | ||
| 84 | 94 | } |
| 85 | 95 | |
| 86 | 96 | // lineSink shows the other agent in the line mode: its answer as it streams, |
| 87 | -// its thoughts dimmed, one line per tool call. Like the line mode's own | |
| 88 | -// tools, its calls are not gated: this front end has no permission prompt. | |
| 97 | +// its thoughts dimmed, one line per tool call. Its calls reach Allow only | |
| 98 | +// when this agent's yolo is off (consent.Gate, in internal/ask): they are | |
| 99 | +// then asked through the REPL's prompt, like the line mode's own tools. | |
| 89 | 100 | type lineSink struct { |
| 90 | 101 | cwd string |
| 91 | 102 | } |
| @@ -120,6 +131,6 @@ func (s *lineSink) ToolEnd(_ string, res ui.ToolResult) { | ||
| 120 | 131 | } |
| 121 | 132 | } |
| 122 | 133 | |
| 123 | -func (s *lineSink) Allow(context.Context, ui.ToolEvent) bool { return true } | |
| 124 | -func (s *lineSink) ToolRunning(string) {} | |
| 125 | -func (s *lineSink) WorkDir() string { return s.cwd } | |
| 134 | +func (s *lineSink) Allow(ctx context.Context, ev ui.ToolEvent) bool { return consent.Prompt(ctx, ev) } | |
| 135 | +func (s *lineSink) ToolRunning(string) {} | |
| 136 | +func (s *lineSink) WorkDir() string { return s.cwd } | |
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | "mm/internal/ask" | 8 | "mm/internal/ask" |
| 9 | "mm/internal/command" | 9 | "mm/internal/command" |
| 10 | "mm/internal/config" | 10 | "mm/internal/config" |
| 11 | + "mm/internal/consent" | ||
| 11 | "mm/internal/spinner" | 12 | "mm/internal/spinner" |
| 12 | "mm/internal/store" | 13 | "mm/internal/store" |
| 13 | "mm/internal/ui" | 14 | "mm/internal/ui" |
| @@ -24,6 +25,7 @@ type asking struct { | |||
| 24 | keep *store.Keeper | 25 | keep *store.Keeper |
| 25 | sigCh <-chan os.Signal | 26 | sigCh <-chan os.Signal |
| 26 | inputCh <-chan string | 27 | inputCh <-chan string |
| 28 | + prompt *linePrompt // the asked agent's calls may need the user's answer | ||
| 27 | } | 29 | } |
| 28 | 30 | ||
| 29 | // run asks req.Agent, streaming its answer and tool calls, and returns the | 31 | // run asks req.Agent, streaming its answer and tool calls, and returns the |
| @@ -48,14 +50,22 @@ func (a asking) run(ctx context.Context, req command.AskRequest, messages []*ai. | |||
| 48 | case res := <-done: | 50 | case res := <-done: |
| 49 | return a.settle(askCtx, req, messages, res.exchange, res.err), quit | 51 | return a.settle(askCtx, req, messages, res.exchange, res.err), quit |
| 50 | case <-a.sigCh: | 52 | case <-a.sigCh: |
| 53 | + a.prompt.drop() | ||
| 51 | fmt.Println("\n🛑 Interrupted (Ctrl+C).") | 54 | fmt.Println("\n🛑 Interrupted (Ctrl+C).") |
| 52 | cancel() | 55 | cancel() |
| 56 | + case req := <-a.prompt.requestCh(): | ||
| 57 | + a.prompt.show(req) | ||
| 53 | case input := <-a.inputCh: | 58 | case input := <-a.inputCh: |
| 59 | + if a.prompt.answer(input) { | ||
| 60 | + continue | ||
| 61 | + } | ||
| 54 | switch { | 62 | switch { |
| 55 | case command.Is(input, command.Abort): | 63 | case command.Is(input, command.Abort): |
| 64 | + a.prompt.drop() | ||
| 56 | fmt.Println("🛑 Aborting…") | 65 | fmt.Println("🛑 Aborting…") |
| 57 | cancel() | 66 | cancel() |
| 58 | case command.Is(input, command.Quit): | 67 | case command.Is(input, command.Quit): |
| 68 | + a.prompt.drop() | ||
| 59 | quit = true | 69 | quit = true |
| 60 | cancel() | 70 | cancel() |
| 61 | default: | 71 | default: |
| @@ -84,8 +94,9 @@ func (a asking) settle(askCtx context.Context, req command.AskRequest, messages, | |||
| 84 | } | 94 | } |
| 85 | 95 | ||
| 86 | // lineSink shows the other agent in the line mode: its answer as it streams, | 96 | // lineSink shows the other agent in the line mode: its answer as it streams, |
| 87 | -// its thoughts dimmed, one line per tool call. Like the line mode's own | 97 | +// its thoughts dimmed, one line per tool call. Its calls reach Allow only |
| 88 | -// tools, its calls are not gated: this front end has no permission prompt. | 98 | +// when this agent's yolo is off (consent.Gate, in internal/ask): they are |
| 99 | +// then asked through the REPL's prompt, like the line mode's own tools. | ||
| 89 | type lineSink struct { | 100 | type lineSink struct { |
| 90 | cwd string | 101 | cwd string |
| 91 | } | 102 | } |
| @@ -120,6 +131,6 @@ func (s *lineSink) ToolEnd(_ string, res ui.ToolResult) { | |||
| 120 | } | 131 | } |
| 121 | } | 132 | } |
| 122 | 133 | ||
| 123 | -func (s *lineSink) Allow(context.Context, ui.ToolEvent) bool { return true } | 134 | +func (s *lineSink) Allow(ctx context.Context, ev ui.ToolEvent) bool { return consent.Prompt(ctx, ev) } |
| 124 | -func (s *lineSink) ToolRunning(string) {} | 135 | +func (s *lineSink) ToolRunning(string) {} |
| 125 | -func (s *lineSink) WorkDir() string { return s.cwd } | 136 | +func (s *lineSink) WorkDir() string { return s.cwd } |
added
internal/agent/prompt.go +119 -0 | new file mode 100644 | ||
| @@ -0,0 +1,119 @@ | ||
| 1 | +package agent | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "fmt" | |
| 6 | + "strings" | |
| 7 | + | |
| 8 | + "mm/internal/consent" | |
| 9 | + "mm/internal/spinner" | |
| 10 | + "mm/internal/ui" | |
| 11 | +) | |
| 12 | + | |
| 13 | +// linePrompt is the line mode's permission prompt: when yolo is off (or an | |
| 14 | +// MCP tool says "ask"), a tool asks here before it runs. Two goroutines meet | |
| 15 | +// in it, and each touches its own half: | |
| 16 | +// | |
| 17 | +// - the tool's goroutine calls allow (the consent.Prompter): it posts a | |
| 18 | +// request and waits for the answer, or for the turn to end; | |
| 19 | +// - the REPL's goroutine, which owns stdin, picks the request up in its | |
| 20 | +// waiting room (requests), prints the question (show) and routes the next | |
| 21 | +// typed line to it (answer) — or refuses it when the turn is stopped | |
| 22 | +// (drop). | |
| 23 | +// | |
| 24 | +// One question at a time: while one is pending, requests returns nil and | |
| 25 | +// the next tool call waits its turn. "For this session" and "never" are | |
| 26 | +// remembered per tool by the policy, as in the window and the editor. | |
| 27 | +type linePrompt struct { | |
| 28 | + policy consent.Policy | |
| 29 | + requests chan *permRequest | |
| 30 | + pending *permRequest // REPL goroutine only | |
| 31 | +} | |
| 32 | + | |
| 33 | +// permRequest is one tool call waiting for the user. | |
| 34 | +type permRequest struct { | |
| 35 | + ev ui.ToolEvent | |
| 36 | + reply chan consent.Answer // buffered: the REPL never blocks answering | |
| 37 | +} | |
| 38 | + | |
| 39 | +func newLinePrompt() *linePrompt { | |
| 40 | + return &linePrompt{requests: make(chan *permRequest)} | |
| 41 | +} | |
| 42 | + | |
| 43 | +// allow asks the user about ev, unless the session already answered for its | |
| 44 | +// tool. Called on the tool's goroutine; the spinner is held off for the | |
| 45 | +// whole question so it cannot draw over it. | |
| 46 | +func (p *linePrompt) allow(ctx context.Context, ev ui.ToolEvent) bool { | |
| 47 | + return p.policy.Decide(ctx, ev.Tool, func(ctx context.Context) consent.Answer { | |
| 48 | + answer := consent.RejectOnce | |
| 49 | + spinner.Suspend(func() { answer = p.wait(ctx, ev) }) | |
| 50 | + return answer | |
| 51 | + }) | |
| 52 | +} | |
| 53 | + | |
| 54 | +// wait posts the request and blocks until the REPL answers it or the turn's | |
| 55 | +// context ends — a stopped turn refuses. | |
| 56 | +func (p *linePrompt) wait(ctx context.Context, ev ui.ToolEvent) consent.Answer { | |
| 57 | + req := &permRequest{ev: ev, reply: make(chan consent.Answer, 1)} | |
| 58 | + select { | |
| 59 | + case p.requests <- req: | |
| 60 | + case <-ctx.Done(): | |
| 61 | + return consent.RejectOnce | |
| 62 | + } | |
| 63 | + select { | |
| 64 | + case a := <-req.reply: | |
| 65 | + return a | |
| 66 | + case <-ctx.Done(): | |
| 67 | + return consent.RejectOnce | |
| 68 | + } | |
| 69 | +} | |
| 70 | + | |
| 71 | +// requestCh is the channel the waiting room selects on: nil while a question | |
| 72 | +// is pending (or for a nil prompt), so a second one waits. | |
| 73 | +func (p *linePrompt) requestCh() <-chan *permRequest { | |
| 74 | + if p == nil || p.pending != nil { | |
| 75 | + return nil | |
| 76 | + } | |
| 77 | + return p.requests | |
| 78 | +} | |
| 79 | + | |
| 80 | +// show makes req the pending question and prints it. | |
| 81 | +func (p *linePrompt) show(req *permRequest) { | |
| 82 | + p.pending = req | |
| 83 | + fmt.Printf("\n🔐 %s\n", oneLine(req.ev.Title)) | |
| 84 | + p.legend() | |
| 85 | +} | |
| 86 | + | |
| 87 | +// legend prints the four answers and the input marker. | |
| 88 | +func (p *linePrompt) legend() { | |
| 89 | + dimln(" " + consent.Choices(p.pending.ev.Tool)) | |
| 90 | + fmt.Print(" ? ") | |
| 91 | +} | |
| 92 | + | |
| 93 | +// answer routes a typed line to the pending question and reports whether it | |
| 94 | +// took it. A slash command is never an answer — /abort and /quit keep | |
| 95 | +// working during a question; anything else that is not one of the four | |
| 96 | +// letters prints the legend again. | |
| 97 | +func (p *linePrompt) answer(line string) bool { | |
| 98 | + if p == nil || p.pending == nil || strings.HasPrefix(line, "/") { | |
| 99 | + return false | |
| 100 | + } | |
| 101 | + a, ok := consent.Parse(line) | |
| 102 | + if !ok { | |
| 103 | + p.legend() | |
| 104 | + return true | |
| 105 | + } | |
| 106 | + p.pending.reply <- a | |
| 107 | + p.pending = nil | |
| 108 | + return true | |
| 109 | +} | |
| 110 | + | |
| 111 | +// drop refuses the pending question, if any: the turn it belongs to is being | |
| 112 | +// stopped (Ctrl+C, /abort, a new question, the turn ended). | |
| 113 | +func (p *linePrompt) drop() { | |
| 114 | + if p == nil || p.pending == nil { | |
| 115 | + return | |
| 116 | + } | |
| 117 | + p.pending.reply <- consent.RejectOnce | |
| 118 | + p.pending = nil | |
| 119 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | +package agent | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "fmt" | ||
| 6 | + "strings" | ||
| 7 | + | ||
| 8 | + "mm/internal/consent" | ||
| 9 | + "mm/internal/spinner" | ||
| 10 | + "mm/internal/ui" | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +// linePrompt is the line mode's permission prompt: when yolo is off (or an | ||
| 14 | +// MCP tool says "ask"), a tool asks here before it runs. Two goroutines meet | ||
| 15 | +// in it, and each touches its own half: | ||
| 16 | +// | ||
| 17 | +// - the tool's goroutine calls allow (the consent.Prompter): it posts a | ||
| 18 | +// request and waits for the answer, or for the turn to end; | ||
| 19 | +// - the REPL's goroutine, which owns stdin, picks the request up in its | ||
| 20 | +// waiting room (requests), prints the question (show) and routes the next | ||
| 21 | +// typed line to it (answer) — or refuses it when the turn is stopped | ||
| 22 | +// (drop). | ||
| 23 | +// | ||
| 24 | +// One question at a time: while one is pending, requests returns nil and | ||
| 25 | +// the next tool call waits its turn. "For this session" and "never" are | ||
| 26 | +// remembered per tool by the policy, as in the window and the editor. | ||
| 27 | +type linePrompt struct { | ||
| 28 | + policy consent.Policy | ||
| 29 | + requests chan *permRequest | ||
| 30 | + pending *permRequest // REPL goroutine only | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +// permRequest is one tool call waiting for the user. | ||
| 34 | +type permRequest struct { | ||
| 35 | + ev ui.ToolEvent | ||
| 36 | + reply chan consent.Answer // buffered: the REPL never blocks answering | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +func newLinePrompt() *linePrompt { | ||
| 40 | + return &linePrompt{requests: make(chan *permRequest)} | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +// allow asks the user about ev, unless the session already answered for its | ||
| 44 | +// tool. Called on the tool's goroutine; the spinner is held off for the | ||
| 45 | +// whole question so it cannot draw over it. | ||
| 46 | +func (p *linePrompt) allow(ctx context.Context, ev ui.ToolEvent) bool { | ||
| 47 | + return p.policy.Decide(ctx, ev.Tool, func(ctx context.Context) consent.Answer { | ||
| 48 | + answer := consent.RejectOnce | ||
| 49 | + spinner.Suspend(func() { answer = p.wait(ctx, ev) }) | ||
| 50 | + return answer | ||
| 51 | + }) | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +// wait posts the request and blocks until the REPL answers it or the turn's | ||
| 55 | +// context ends — a stopped turn refuses. | ||
| 56 | +func (p *linePrompt) wait(ctx context.Context, ev ui.ToolEvent) consent.Answer { | ||
| 57 | + req := &permRequest{ev: ev, reply: make(chan consent.Answer, 1)} | ||
| 58 | + select { | ||
| 59 | + case p.requests <- req: | ||
| 60 | + case <-ctx.Done(): | ||
| 61 | + return consent.RejectOnce | ||
| 62 | + } | ||
| 63 | + select { | ||
| 64 | + case a := <-req.reply: | ||
| 65 | + return a | ||
| 66 | + case <-ctx.Done(): | ||
| 67 | + return consent.RejectOnce | ||
| 68 | + } | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +// requestCh is the channel the waiting room selects on: nil while a question | ||
| 72 | +// is pending (or for a nil prompt), so a second one waits. | ||
| 73 | +func (p *linePrompt) requestCh() <-chan *permRequest { | ||
| 74 | + if p == nil || p.pending != nil { | ||
| 75 | + return nil | ||
| 76 | + } | ||
| 77 | + return p.requests | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +// show makes req the pending question and prints it. | ||
| 81 | +func (p *linePrompt) show(req *permRequest) { | ||
| 82 | + p.pending = req | ||
| 83 | + fmt.Printf("\n🔐 %s\n", oneLine(req.ev.Title)) | ||
| 84 | + p.legend() | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +// legend prints the four answers and the input marker. | ||
| 88 | +func (p *linePrompt) legend() { | ||
| 89 | + dimln(" " + consent.Choices(p.pending.ev.Tool)) | ||
| 90 | + fmt.Print(" ? ") | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +// answer routes a typed line to the pending question and reports whether it | ||
| 94 | +// took it. A slash command is never an answer — /abort and /quit keep | ||
| 95 | +// working during a question; anything else that is not one of the four | ||
| 96 | +// letters prints the legend again. | ||
| 97 | +func (p *linePrompt) answer(line string) bool { | ||
| 98 | + if p == nil || p.pending == nil || strings.HasPrefix(line, "/") { | ||
| 99 | + return false | ||
| 100 | + } | ||
| 101 | + a, ok := consent.Parse(line) | ||
| 102 | + if !ok { | ||
| 103 | + p.legend() | ||
| 104 | + return true | ||
| 105 | + } | ||
| 106 | + p.pending.reply <- a | ||
| 107 | + p.pending = nil | ||
| 108 | + return true | ||
| 109 | +} | ||
| 110 | + | ||
| 111 | +// drop refuses the pending question, if any: the turn it belongs to is being | ||
| 112 | +// stopped (Ctrl+C, /abort, a new question, the turn ended). | ||
| 113 | +func (p *linePrompt) drop() { | ||
| 114 | + if p == nil || p.pending == nil { | ||
| 115 | + return | ||
| 116 | + } | ||
| 117 | + p.pending.reply <- consent.RejectOnce | ||
| 118 | + p.pending = nil | ||
| 119 | +} | ||
added
internal/agent/prompt_test.go +101 -0 | new file mode 100644 | ||
| @@ -0,0 +1,101 @@ | ||
| 1 | +package agent | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "testing" | |
| 6 | + "time" | |
| 7 | + | |
| 8 | + "mm/internal/ui" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// askAsync runs allow on its own goroutine, as a tool would, and waits until | |
| 12 | +// the request reaches the waiting room; it returns the request and where the | |
| 13 | +// tool's verdict will arrive. | |
| 14 | +func askAsync(t *testing.T, p *linePrompt, ctx context.Context, ev ui.ToolEvent) (*permRequest, <-chan bool) { | |
| 15 | + t.Helper() | |
| 16 | + got := make(chan bool, 1) | |
| 17 | + go func() { got <- p.allow(ctx, ev) }() | |
| 18 | + select { | |
| 19 | + case req := <-p.requestCh(): | |
| 20 | + p.show(req) | |
| 21 | + return req, got | |
| 22 | + case <-time.After(2 * time.Second): | |
| 23 | + t.Fatal("the tool's question never reached the waiting room") | |
| 24 | + return nil, nil | |
| 25 | + } | |
| 26 | +} | |
| 27 | + | |
| 28 | +func TestLinePromptAllowOnceAsksAgainNextTime(t *testing.T) { | |
| 29 | + p := newLinePrompt() | |
| 30 | + ev := ui.ToolEvent{Tool: "bash", Title: "bash: ls"} | |
| 31 | + _, got := askAsync(t, p, context.Background(), ev) | |
| 32 | + if !p.answer("a") || !<-got { | |
| 33 | + t.Fatal("a did not allow the call") | |
| 34 | + } | |
| 35 | + if p.pending != nil { | |
| 36 | + t.Error("the question is still pending after its answer") | |
| 37 | + } | |
| 38 | + _, got = askAsync(t, p, context.Background(), ev) // asks again: once is once | |
| 39 | + p.answer("r") | |
| 40 | + if <-got { | |
| 41 | + t.Error("r allowed the call") | |
| 42 | + } | |
| 43 | +} | |
| 44 | + | |
| 45 | +// "s" and "n" are remembered for the tool: no second question. | |
| 46 | +func TestLinePromptRemembersSessionAndNever(t *testing.T) { | |
| 47 | + p := newLinePrompt() | |
| 48 | + _, got := askAsync(t, p, context.Background(), ui.ToolEvent{Tool: "bash"}) | |
| 49 | + p.answer("s") | |
| 50 | + if !<-got || !p.allow(context.Background(), ui.ToolEvent{Tool: "bash"}) { | |
| 51 | + t.Error("allow for the session not honoured") | |
| 52 | + } | |
| 53 | + _, got = askAsync(t, p, context.Background(), ui.ToolEvent{Tool: "write_file"}) | |
| 54 | + p.answer("n") | |
| 55 | + if <-got || p.allow(context.Background(), ui.ToolEvent{Tool: "write_file"}) { | |
| 56 | + t.Error("never not honoured") | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +// A line that is not one of the four answers keeps the question; a slash | |
| 61 | +// command is not taken as an answer, so /abort still reaches the REPL. | |
| 62 | +func TestLinePromptIgnoresOtherLinesAndCommands(t *testing.T) { | |
| 63 | + p := newLinePrompt() | |
| 64 | + _, got := askAsync(t, p, context.Background(), ui.ToolEvent{Tool: "bash"}) | |
| 65 | + if !p.answer("maybe") || p.pending == nil { | |
| 66 | + t.Error("an unknown answer was not kept on the question") | |
| 67 | + } | |
| 68 | + if p.answer("/abort") { | |
| 69 | + t.Error("/abort was taken as an answer") | |
| 70 | + } | |
| 71 | + if p.requestCh() != nil { | |
| 72 | + t.Error("a second question can be shown while one is pending") | |
| 73 | + } | |
| 74 | + p.drop() | |
| 75 | + if <-got || p.pending != nil { | |
| 76 | + t.Error("drop did not refuse and clear the question") | |
| 77 | + } | |
| 78 | +} | |
| 79 | + | |
| 80 | +// A stopped turn refuses the question the tool was waiting on. | |
| 81 | +func TestLinePromptRefusesWhenTheTurnEnds(t *testing.T) { | |
| 82 | + p := newLinePrompt() | |
| 83 | + ctx, cancel := context.WithCancel(context.Background()) | |
| 84 | + _, got := askAsync(t, p, ctx, ui.ToolEvent{Tool: "bash"}) | |
| 85 | + cancel() | |
| 86 | + if <-got { | |
| 87 | + t.Error("a cancelled turn allowed the call") | |
| 88 | + } | |
| 89 | +} | |
| 90 | + | |
| 91 | +// Nothing to answer: no prompt (a nil one, in a test), or no question. | |
| 92 | +func TestLinePromptWithNothingPending(t *testing.T) { | |
| 93 | + var none *linePrompt | |
| 94 | + if none.answer("a") || none.requestCh() != nil { | |
| 95 | + t.Error("a nil prompt took an answer or offered a question") | |
| 96 | + } | |
| 97 | + none.drop() | |
| 98 | + if newLinePrompt().answer("a") { | |
| 99 | + t.Error("an answer was taken with no question pending") | |
| 100 | + } | |
| 101 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | +package agent | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "testing" | ||
| 6 | + "time" | ||
| 7 | + | ||
| 8 | + "mm/internal/ui" | ||
| 9 | +) | ||
| 10 | + | ||
| 11 | +// askAsync runs allow on its own goroutine, as a tool would, and waits until | ||
| 12 | +// the request reaches the waiting room; it returns the request and where the | ||
| 13 | +// tool's verdict will arrive. | ||
| 14 | +func askAsync(t *testing.T, p *linePrompt, ctx context.Context, ev ui.ToolEvent) (*permRequest, <-chan bool) { | ||
| 15 | + t.Helper() | ||
| 16 | + got := make(chan bool, 1) | ||
| 17 | + go func() { got <- p.allow(ctx, ev) }() | ||
| 18 | + select { | ||
| 19 | + case req := <-p.requestCh(): | ||
| 20 | + p.show(req) | ||
| 21 | + return req, got | ||
| 22 | + case <-time.After(2 * time.Second): | ||
| 23 | + t.Fatal("the tool's question never reached the waiting room") | ||
| 24 | + return nil, nil | ||
| 25 | + } | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +func TestLinePromptAllowOnceAsksAgainNextTime(t *testing.T) { | ||
| 29 | + p := newLinePrompt() | ||
| 30 | + ev := ui.ToolEvent{Tool: "bash", Title: "bash: ls"} | ||
| 31 | + _, got := askAsync(t, p, context.Background(), ev) | ||
| 32 | + if !p.answer("a") || !<-got { | ||
| 33 | + t.Fatal("a did not allow the call") | ||
| 34 | + } | ||
| 35 | + if p.pending != nil { | ||
| 36 | + t.Error("the question is still pending after its answer") | ||
| 37 | + } | ||
| 38 | + _, got = askAsync(t, p, context.Background(), ev) // asks again: once is once | ||
| 39 | + p.answer("r") | ||
| 40 | + if <-got { | ||
| 41 | + t.Error("r allowed the call") | ||
| 42 | + } | ||
| 43 | +} | ||
| 44 | + | ||
| 45 | +// "s" and "n" are remembered for the tool: no second question. | ||
| 46 | +func TestLinePromptRemembersSessionAndNever(t *testing.T) { | ||
| 47 | + p := newLinePrompt() | ||
| 48 | + _, got := askAsync(t, p, context.Background(), ui.ToolEvent{Tool: "bash"}) | ||
| 49 | + p.answer("s") | ||
| 50 | + if !<-got || !p.allow(context.Background(), ui.ToolEvent{Tool: "bash"}) { | ||
| 51 | + t.Error("allow for the session not honoured") | ||
| 52 | + } | ||
| 53 | + _, got = askAsync(t, p, context.Background(), ui.ToolEvent{Tool: "write_file"}) | ||
| 54 | + p.answer("n") | ||
| 55 | + if <-got || p.allow(context.Background(), ui.ToolEvent{Tool: "write_file"}) { | ||
| 56 | + t.Error("never not honoured") | ||
| 57 | + } | ||
| 58 | +} | ||
| 59 | + | ||
| 60 | +// A line that is not one of the four answers keeps the question; a slash | ||
| 61 | +// command is not taken as an answer, so /abort still reaches the REPL. | ||
| 62 | +func TestLinePromptIgnoresOtherLinesAndCommands(t *testing.T) { | ||
| 63 | + p := newLinePrompt() | ||
| 64 | + _, got := askAsync(t, p, context.Background(), ui.ToolEvent{Tool: "bash"}) | ||
| 65 | + if !p.answer("maybe") || p.pending == nil { | ||
| 66 | + t.Error("an unknown answer was not kept on the question") | ||
| 67 | + } | ||
| 68 | + if p.answer("/abort") { | ||
| 69 | + t.Error("/abort was taken as an answer") | ||
| 70 | + } | ||
| 71 | + if p.requestCh() != nil { | ||
| 72 | + t.Error("a second question can be shown while one is pending") | ||
| 73 | + } | ||
| 74 | + p.drop() | ||
| 75 | + if <-got || p.pending != nil { | ||
| 76 | + t.Error("drop did not refuse and clear the question") | ||
| 77 | + } | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +// A stopped turn refuses the question the tool was waiting on. | ||
| 81 | +func TestLinePromptRefusesWhenTheTurnEnds(t *testing.T) { | ||
| 82 | + p := newLinePrompt() | ||
| 83 | + ctx, cancel := context.WithCancel(context.Background()) | ||
| 84 | + _, got := askAsync(t, p, ctx, ui.ToolEvent{Tool: "bash"}) | ||
| 85 | + cancel() | ||
| 86 | + if <-got { | ||
| 87 | + t.Error("a cancelled turn allowed the call") | ||
| 88 | + } | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +// Nothing to answer: no prompt (a nil one, in a test), or no question. | ||
| 92 | +func TestLinePromptWithNothingPending(t *testing.T) { | ||
| 93 | + var none *linePrompt | ||
| 94 | + if none.answer("a") || none.requestCh() != nil { | ||
| 95 | + t.Error("a nil prompt took an answer or offered a question") | ||
| 96 | + } | ||
| 97 | + none.drop() | ||
| 98 | + if newLinePrompt().answer("a") { | ||
| 99 | + t.Error("an answer was taken with no question pending") | ||
| 100 | + } | ||
| 101 | +} | ||
modified
internal/ask/cli.go +11 -3 | @@ -9,6 +9,7 @@ import ( | ||
| 9 | 9 | "strings" |
| 10 | 10 | |
| 11 | 11 | "mm/internal/config" |
| 12 | + "mm/internal/consent" | |
| 12 | 13 | "mm/internal/ui" |
| 13 | 14 | ) |
| 14 | 15 | |
| @@ -129,6 +130,13 @@ func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) { | ||
| 129 | 130 | fmt.Fprintln(s.log, " │ failed") |
| 130 | 131 | } |
| 131 | 132 | } |
| 132 | -func (s *cliSink) Allow(context.Context, ui.ToolEvent) bool { return true } | |
| 133 | -func (s *cliSink) ToolRunning(string) {} | |
| 134 | -func (s *cliSink) WorkDir() string { return "" } | |
| 133 | +func (s *cliSink) ToolRunning(string) {} | |
| 134 | +func (s *cliSink) WorkDir() string { return "" } | |
| 135 | + | |
| 136 | +// Allow is reached only when the asking agent's yolo is off (consent.Gate): | |
| 137 | +// stdin carried the question, so nobody can answer — the call is refused, | |
| 138 | +// and stderr says why once. | |
| 139 | +func (s *cliSink) Allow(context.Context, ui.ToolEvent) bool { | |
| 140 | + consent.CannotAsk() | |
| 141 | + return false | |
| 142 | +} | |
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | "strings" | 9 | "strings" |
| 10 | 10 | ||
| 11 | "mm/internal/config" | 11 | "mm/internal/config" |
| 12 | + "mm/internal/consent" | ||
| 12 | "mm/internal/ui" | 13 | "mm/internal/ui" |
| 13 | ) | 14 | ) |
| 14 | 15 | ||
| @@ -129,6 +130,13 @@ func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) { | |||
| 129 | fmt.Fprintln(s.log, " │ failed") | 130 | fmt.Fprintln(s.log, " │ failed") |
| 130 | } | 131 | } |
| 131 | } | 132 | } |
| 132 | -func (s *cliSink) Allow(context.Context, ui.ToolEvent) bool { return true } | 133 | +func (s *cliSink) ToolRunning(string) {} |
| 133 | -func (s *cliSink) ToolRunning(string) {} | 134 | +func (s *cliSink) WorkDir() string { return "" } |
| 134 | -func (s *cliSink) WorkDir() string { return "" } | 135 | + |
| 136 | +// Allow is reached only when the asking agent's yolo is off (consent.Gate): | ||
| 137 | +// stdin carried the question, so nobody can answer — the call is refused, | ||
| 138 | +// and stderr says why once. | ||
| 139 | +func (s *cliSink) Allow(context.Context, ui.ToolEvent) bool { | ||
| 140 | + consent.CannotAsk() | ||
| 141 | + return false | ||
| 142 | +} | ||
modified
internal/ask/cli_test.go +14 -0 | @@ -60,6 +60,20 @@ func TestCLIPrintsOnlyTheAnswerOnStdout(t *testing.T) { | ||
| 60 | 60 | } |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | +// The asking agent with yolo off: the CLI cannot ask anyone (stdin was the | |
| 64 | +// question), so the asked agent's call is refused rather than hanging. | |
| 65 | +func TestCLIRefusesToolsWhenTheAskerIsNotInYoloMode(t *testing.T) { | |
| 66 | + withAgent(t) | |
| 67 | + askerYolo(t, false) | |
| 68 | + var out, errs bytes.Buffer | |
| 69 | + if err := fakePool(t, "tool").CLI(context.Background(), CLIRequest{Agent: "bob", Question: "2+2?"}, t.TempDir(), &out, &errs); err != nil { | |
| 70 | + t.Fatal(err) | |
| 71 | + } | |
| 72 | + if got := out.String(); got != "You asked: 2+2? (tool rejected)\n" { | |
| 73 | + t.Errorf("stdout = %q, want the call rejected", got) | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 63 | 77 | // With context, the other agent receives the transcript first, in the |
| 64 | 78 | // /ask-with-context format. |
| 65 | 79 | func TestCLIWithContext(t *testing.T) { |
| @@ -60,6 +60,20 @@ func TestCLIPrintsOnlyTheAnswerOnStdout(t *testing.T) { | |||
| 60 | } | 60 | } |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | +// The asking agent with yolo off: the CLI cannot ask anyone (stdin was the | ||
| 64 | +// question), so the asked agent's call is refused rather than hanging. | ||
| 65 | +func TestCLIRefusesToolsWhenTheAskerIsNotInYoloMode(t *testing.T) { | ||
| 66 | + withAgent(t) | ||
| 67 | + askerYolo(t, false) | ||
| 68 | + var out, errs bytes.Buffer | ||
| 69 | + if err := fakePool(t, "tool").CLI(context.Background(), CLIRequest{Agent: "bob", Question: "2+2?"}, t.TempDir(), &out, &errs); err != nil { | ||
| 70 | + t.Fatal(err) | ||
| 71 | + } | ||
| 72 | + if got := out.String(); got != "You asked: 2+2? (tool rejected)\n" { | ||
| 73 | + t.Errorf("stdout = %q, want the call rejected", got) | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | + | ||
| 63 | // With context, the other agent receives the transcript first, in the | 77 | // With context, the other agent receives the transcript first, in the |
| 64 | // /ask-with-context format. | 78 | // /ask-with-context format. |
| 65 | func TestCLIWithContext(t *testing.T) { | 79 | func TestCLIWithContext(t *testing.T) { |
modified
internal/ask/client.go +8 -5 | @@ -5,6 +5,7 @@ import ( | ||
| 5 | 5 | "strings" |
| 6 | 6 | "sync" |
| 7 | 7 | |
| 8 | + "mm/internal/consent" | |
| 8 | 9 | "mm/internal/ui" |
| 9 | 10 | |
| 10 | 11 | sdk "github.com/coder/acp-go-sdk" |
| @@ -126,10 +127,12 @@ func (c *client) toolUpdate(sink ui.Sink, up *sdk.SessionToolCallUpdate) { | ||
| 126 | 127 | } |
| 127 | 128 | } |
| 128 | 129 | |
| 129 | -// RequestPermission asks the asker's user, through the asker's front end, | |
| 130 | -// whether the child may run a call; the answer is one of the options the | |
| 131 | -// child offered (allow once / reject once), or "cancelled" when it offered | |
| 132 | -// none that fits. | |
| 130 | +// RequestPermission decides, for the asker, whether the child may run a call: | |
| 131 | +// the asker's yolo first (consent.Gate — the child runs with yolo off, so | |
| 132 | +// every call it would gate lands here), then the asker's user through the | |
| 133 | +// asker's front end. The answer is one of the options the child offered | |
| 134 | +// (allow once / reject once — the asker remembers session answers, not the | |
| 135 | +// child), or "cancelled" when it offered none that fits. | |
| 133 | 136 | func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) { |
| 134 | 137 | sink := c.current(p.SessionId) |
| 135 | 138 | title := "" |
| @@ -144,7 +147,7 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR | ||
| 144 | 147 | if c.firstSight(p.ToolCall.ToolCallId) { |
| 145 | 148 | sink.ToolStart(ev) // the request overtook the tool_call: show it first |
| 146 | 149 | } |
| 147 | - allowed = sink.Allow(ctx, ev) | |
| 150 | + allowed = consent.Gate(ctx, sink, ev) | |
| 148 | 151 | } |
| 149 | 152 | want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways} |
| 150 | 153 | if allowed { |
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | "strings" | 5 | "strings" |
| 6 | "sync" | 6 | "sync" |
| 7 | 7 | ||
| 8 | + "mm/internal/consent" | ||
| 8 | "mm/internal/ui" | 9 | "mm/internal/ui" |
| 9 | 10 | ||
| 10 | sdk "github.com/coder/acp-go-sdk" | 11 | sdk "github.com/coder/acp-go-sdk" |
| @@ -126,10 +127,12 @@ func (c *client) toolUpdate(sink ui.Sink, up *sdk.SessionToolCallUpdate) { | |||
| 126 | } | 127 | } |
| 127 | } | 128 | } |
| 128 | 129 | ||
| 129 | -// RequestPermission asks the asker's user, through the asker's front end, | 130 | +// RequestPermission decides, for the asker, whether the child may run a call: |
| 130 | -// whether the child may run a call; the answer is one of the options the | 131 | +// the asker's yolo first (consent.Gate — the child runs with yolo off, so |
| 131 | -// child offered (allow once / reject once), or "cancelled" when it offered | 132 | +// every call it would gate lands here), then the asker's user through the |
| 132 | -// none that fits. | 133 | +// asker's front end. The answer is one of the options the child offered |
| 134 | +// (allow once / reject once — the asker remembers session answers, not the | ||
| 135 | +// child), or "cancelled" when it offered none that fits. | ||
| 133 | func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) { | 136 | func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) { |
| 134 | sink := c.current(p.SessionId) | 137 | sink := c.current(p.SessionId) |
| 135 | title := "" | 138 | title := "" |
| @@ -144,7 +147,7 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR | |||
| 144 | if c.firstSight(p.ToolCall.ToolCallId) { | 147 | if c.firstSight(p.ToolCall.ToolCallId) { |
| 145 | sink.ToolStart(ev) // the request overtook the tool_call: show it first | 148 | sink.ToolStart(ev) // the request overtook the tool_call: show it first |
| 146 | } | 149 | } |
| 147 | - allowed = sink.Allow(ctx, ev) | 150 | + allowed = consent.Gate(ctx, sink, ev) |
| 148 | } | 151 | } |
| 149 | want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways} | 152 | want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways} |
| 150 | if allowed { | 153 | if allowed { |
modified
internal/ask/env.go +6 -4 | @@ -35,12 +35,14 @@ func CanAsk() error { | ||
| 35 | 35 | |
| 36 | 36 | // overrides are the variables that would make the child ignore its own |
| 37 | 37 | // config file: they are meant for ONE agent, the one the user started. |
| 38 | -var overrides = []string{"AGENT_CONFIG", "AGENT_PROVIDER", "AGENT_MODEL", "AGENT_BASE_URL", config.SessionsEnv, DepthEnv} | |
| 38 | +var overrides = []string{"AGENT_CONFIG", "AGENT_PROVIDER", "AGENT_MODEL", "AGENT_BASE_URL", config.SessionsEnv, config.YoloEnv, DepthEnv} | |
| 39 | 39 | |
| 40 | 40 | // ChildEnv is the environment of an agent started by /ask: the parent's, |
| 41 | 41 | // minus the per-agent overrides, plus no session files (it works in the |
| 42 | -// parent's project, and its sessions would land in the parent's /sessions) | |
| 43 | -// and its depth. | |
| 42 | +// parent's project, and its sessions would land in the parent's /sessions), | |
| 43 | +// yolo off and its depth. Yolo off is how the asker's setting wins: the | |
| 44 | +// child submits every call to the asker (RequestPermission), and the asker | |
| 45 | +// runs it through ITS consent.Gate — its own yolo, its own user. | |
| 44 | 46 | // |
| 45 | 47 | // cmd.Env = ask.ChildEnv(os.Environ(), ask.Depth()) |
| 46 | 48 | func ChildEnv(parent []string, depth int) []string { |
| @@ -51,5 +53,5 @@ func ChildEnv(parent []string, depth int) []string { | ||
| 51 | 53 | env = append(env, kv) |
| 52 | 54 | } |
| 53 | 55 | } |
| 54 | - return append(env, config.SessionsEnv+"=off", fmt.Sprintf("%s=%d", DepthEnv, depth+1)) | |
| 56 | + return append(env, config.SessionsEnv+"=off", config.YoloEnv+"=false", fmt.Sprintf("%s=%d", DepthEnv, depth+1)) | |
| 55 | 57 | } |
| @@ -35,12 +35,14 @@ func CanAsk() error { | |||
| 35 | 35 | ||
| 36 | // overrides are the variables that would make the child ignore its own | 36 | // overrides are the variables that would make the child ignore its own |
| 37 | // config file: they are meant for ONE agent, the one the user started. | 37 | // config file: they are meant for ONE agent, the one the user started. |
| 38 | -var overrides = []string{"AGENT_CONFIG", "AGENT_PROVIDER", "AGENT_MODEL", "AGENT_BASE_URL", config.SessionsEnv, DepthEnv} | 38 | +var overrides = []string{"AGENT_CONFIG", "AGENT_PROVIDER", "AGENT_MODEL", "AGENT_BASE_URL", config.SessionsEnv, config.YoloEnv, DepthEnv} |
| 39 | 39 | ||
| 40 | // ChildEnv is the environment of an agent started by /ask: the parent's, | 40 | // ChildEnv is the environment of an agent started by /ask: the parent's, |
| 41 | // minus the per-agent overrides, plus no session files (it works in the | 41 | // minus the per-agent overrides, plus no session files (it works in the |
| 42 | -// parent's project, and its sessions would land in the parent's /sessions) | 42 | +// parent's project, and its sessions would land in the parent's /sessions), |
| 43 | -// and its depth. | 43 | +// yolo off and its depth. Yolo off is how the asker's setting wins: the |
| 44 | +// child submits every call to the asker (RequestPermission), and the asker | ||
| 45 | +// runs it through ITS consent.Gate — its own yolo, its own user. | ||
| 44 | // | 46 | // |
| 45 | // cmd.Env = ask.ChildEnv(os.Environ(), ask.Depth()) | 47 | // cmd.Env = ask.ChildEnv(os.Environ(), ask.Depth()) |
| 46 | func ChildEnv(parent []string, depth int) []string { | 48 | func ChildEnv(parent []string, depth int) []string { |
| @@ -51,5 +53,5 @@ func ChildEnv(parent []string, depth int) []string { | |||
| 51 | env = append(env, kv) | 53 | env = append(env, kv) |
| 52 | } | 54 | } |
| 53 | } | 55 | } |
| 54 | - return append(env, config.SessionsEnv+"=off", fmt.Sprintf("%s=%d", DepthEnv, depth+1)) | 56 | + return append(env, config.SessionsEnv+"=off", config.YoloEnv+"=false", fmt.Sprintf("%s=%d", DepthEnv, depth+1)) |
| 55 | } | 57 | } |
modified
internal/ask/env_test.go +5 -4 | @@ -7,17 +7,18 @@ import ( | ||
| 7 | 7 | ) |
| 8 | 8 | |
| 9 | 9 | // The child keeps the parent's environment but none of the variables that |
| 10 | -// would override ITS config, runs without session files, one level deeper. | |
| 10 | +// would override ITS config, runs without session files and with yolo off | |
| 11 | +// (so the asker decides), one level deeper. | |
| 11 | 12 | func TestChildEnv(t *testing.T) { |
| 12 | - parent := []string{"PATH=/bin", "AGENT_MODEL=parent-model", "AGENT_PROVIDER=dmr", "AGENT_BASE_URL=http://x", "AGENT_CONFIG=p.yaml", "AGENT_SESSIONS=on", "MM_ASK_DEPTH=1", "DMR_BASE_URL=http://dmr", "HOME=/h"} | |
| 13 | + parent := []string{"PATH=/bin", "AGENT_MODEL=parent-model", "AGENT_PROVIDER=dmr", "AGENT_BASE_URL=http://x", "AGENT_CONFIG=p.yaml", "AGENT_SESSIONS=on", "AGENT_YOLO=true", "MM_ASK_DEPTH=1", "DMR_BASE_URL=http://dmr", "HOME=/h"} | |
| 13 | 14 | got := ChildEnv(parent, 1) |
| 14 | - for _, want := range []string{"PATH=/bin", "DMR_BASE_URL=http://dmr", "HOME=/h", "AGENT_SESSIONS=off", "MM_ASK_DEPTH=2"} { | |
| 15 | + for _, want := range []string{"PATH=/bin", "DMR_BASE_URL=http://dmr", "HOME=/h", "AGENT_SESSIONS=off", "AGENT_YOLO=false", "MM_ASK_DEPTH=2"} { | |
| 15 | 16 | if !slices.Contains(got, want) { |
| 16 | 17 | t.Errorf("child env lacks %q: %v", want, got) |
| 17 | 18 | } |
| 18 | 19 | } |
| 19 | 20 | for _, kv := range got { |
| 20 | - for _, banned := range []string{"AGENT_MODEL=", "AGENT_PROVIDER=", "AGENT_BASE_URL=", "AGENT_CONFIG=", "AGENT_SESSIONS=on", "MM_ASK_DEPTH=1"} { | |
| 21 | + for _, banned := range []string{"AGENT_MODEL=", "AGENT_PROVIDER=", "AGENT_BASE_URL=", "AGENT_CONFIG=", "AGENT_SESSIONS=on", "AGENT_YOLO=true", "MM_ASK_DEPTH=1"} { | |
| 21 | 22 | if strings.HasPrefix(kv, banned) { |
| 22 | 23 | t.Errorf("child env keeps %q", kv) |
| 23 | 24 | } |
| @@ -7,17 +7,18 @@ import ( | |||
| 7 | ) | 7 | ) |
| 8 | 8 | ||
| 9 | // The child keeps the parent's environment but none of the variables that | 9 | // The child keeps the parent's environment but none of the variables that |
| 10 | -// would override ITS config, runs without session files, one level deeper. | 10 | +// would override ITS config, runs without session files and with yolo off |
| 11 | +// (so the asker decides), one level deeper. | ||
| 11 | func TestChildEnv(t *testing.T) { | 12 | func TestChildEnv(t *testing.T) { |
| 12 | - parent := []string{"PATH=/bin", "AGENT_MODEL=parent-model", "AGENT_PROVIDER=dmr", "AGENT_BASE_URL=http://x", "AGENT_CONFIG=p.yaml", "AGENT_SESSIONS=on", "MM_ASK_DEPTH=1", "DMR_BASE_URL=http://dmr", "HOME=/h"} | 13 | + parent := []string{"PATH=/bin", "AGENT_MODEL=parent-model", "AGENT_PROVIDER=dmr", "AGENT_BASE_URL=http://x", "AGENT_CONFIG=p.yaml", "AGENT_SESSIONS=on", "AGENT_YOLO=true", "MM_ASK_DEPTH=1", "DMR_BASE_URL=http://dmr", "HOME=/h"} |
| 13 | got := ChildEnv(parent, 1) | 14 | got := ChildEnv(parent, 1) |
| 14 | - for _, want := range []string{"PATH=/bin", "DMR_BASE_URL=http://dmr", "HOME=/h", "AGENT_SESSIONS=off", "MM_ASK_DEPTH=2"} { | 15 | + for _, want := range []string{"PATH=/bin", "DMR_BASE_URL=http://dmr", "HOME=/h", "AGENT_SESSIONS=off", "AGENT_YOLO=false", "MM_ASK_DEPTH=2"} { |
| 15 | if !slices.Contains(got, want) { | 16 | if !slices.Contains(got, want) { |
| 16 | t.Errorf("child env lacks %q: %v", want, got) | 17 | t.Errorf("child env lacks %q: %v", want, got) |
| 17 | } | 18 | } |
| 18 | } | 19 | } |
| 19 | for _, kv := range got { | 20 | for _, kv := range got { |
| 20 | - for _, banned := range []string{"AGENT_MODEL=", "AGENT_PROVIDER=", "AGENT_BASE_URL=", "AGENT_CONFIG=", "AGENT_SESSIONS=on", "MM_ASK_DEPTH=1"} { | 21 | + for _, banned := range []string{"AGENT_MODEL=", "AGENT_PROVIDER=", "AGENT_BASE_URL=", "AGENT_CONFIG=", "AGENT_SESSIONS=on", "AGENT_YOLO=true", "MM_ASK_DEPTH=1"} { |
| 21 | if strings.HasPrefix(kv, banned) { | 22 | if strings.HasPrefix(kv, banned) { |
| 22 | t.Errorf("child env keeps %q", kv) | 23 | t.Errorf("child env keeps %q", kv) |
| 23 | } | 24 | } |
modified
internal/ask/pool_test.go +28 -1 | @@ -11,6 +11,7 @@ import ( | ||
| 11 | 11 | "testing" |
| 12 | 12 | "time" |
| 13 | 13 | |
| 14 | + "mm/internal/config" | |
| 14 | 15 | "mm/internal/ui" |
| 15 | 16 | |
| 16 | 17 | sdk "github.com/coder/acp-go-sdk" |
| @@ -87,10 +88,19 @@ func (r *recorder) all() string { | ||
| 87 | 88 | return strings.Join(r.events, " | ") |
| 88 | 89 | } |
| 89 | 90 | |
| 91 | +// askerYolo sets the ASKER's yolo for one test: it decides the child's calls. | |
| 92 | +func askerYolo(t *testing.T, on bool) { | |
| 93 | + t.Helper() | |
| 94 | + old := config.Cfg.Yolo | |
| 95 | + config.Cfg.Yolo = on | |
| 96 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | |
| 97 | +} | |
| 98 | + | |
| 90 | 99 | // One ask: the prompt reaches the child, its text streams to the sink and is |
| 91 | 100 | // returned, its tool call is shown under bob's name with the permission asked |
| 92 | -// of the asker, and its thoughts are shown as thoughts. | |
| 101 | +// of the asker (yolo off), and its thoughts are shown as thoughts. | |
| 93 | 102 | func TestAskStreamsTheAnswerAndTheToolCalls(t *testing.T) { |
| 103 | + askerYolo(t, false) | |
| 94 | 104 | p := fakePool(t, "tool") |
| 95 | 105 | sink := &recorder{allow: true} |
| 96 | 106 | answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "what is 2+2?", sink) |
| @@ -115,6 +125,7 @@ func TestAskStreamsTheAnswerAndTheToolCalls(t *testing.T) { | ||
| 115 | 125 | |
| 116 | 126 | // A refusal from the asker's user reaches the child as its reject option. |
| 117 | 127 | func TestAskForwardsARefusal(t *testing.T) { |
| 128 | + askerYolo(t, false) | |
| 118 | 129 | p := fakePool(t, "tool") |
| 119 | 130 | answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", &recorder{allow: false}) |
| 120 | 131 | if err != nil || !strings.HasSuffix(answer, "(tool rejected)") { |
| @@ -122,6 +133,22 @@ func TestAskForwardsARefusal(t *testing.T) { | ||
| 122 | 133 | } |
| 123 | 134 | } |
| 124 | 135 | |
| 136 | +// The asker's yolo wins: on, the child's call runs without asking the | |
| 137 | +// asker's user — whatever the child's own config says, since it runs with | |
| 138 | +// AGENT_YOLO=false and submits every call. | |
| 139 | +func TestAskerInYoloModeAllowsWithoutAsking(t *testing.T) { | |
| 140 | + askerYolo(t, true) | |
| 141 | + p := fakePool(t, "tool") | |
| 142 | + sink := &recorder{allow: false} | |
| 143 | + answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", sink) | |
| 144 | + if err != nil || !strings.HasSuffix(answer, "(tool allowed)") { | |
| 145 | + t.Errorf("answer %q, err %v, want the call allowed by the asker's yolo", answer, err) | |
| 146 | + } | |
| 147 | + if strings.Contains(sink.all(), "allow?") { | |
| 148 | + t.Errorf("events %s: the asker's user was asked in yolo mode", sink.all()) | |
| 149 | + } | |
| 150 | +} | |
| 151 | + | |
| 125 | 152 | // The child is started once and reused, with a fresh session per ask. |
| 126 | 153 | func TestAskReusesTheChildWithAFreshSession(t *testing.T) { |
| 127 | 154 | p := fakePool(t, "echo") |
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | "testing" | 11 | "testing" |
| 12 | "time" | 12 | "time" |
| 13 | 13 | ||
| 14 | + "mm/internal/config" | ||
| 14 | "mm/internal/ui" | 15 | "mm/internal/ui" |
| 15 | 16 | ||
| 16 | sdk "github.com/coder/acp-go-sdk" | 17 | sdk "github.com/coder/acp-go-sdk" |
| @@ -87,10 +88,19 @@ func (r *recorder) all() string { | |||
| 87 | return strings.Join(r.events, " | ") | 88 | return strings.Join(r.events, " | ") |
| 88 | } | 89 | } |
| 89 | 90 | ||
| 91 | +// askerYolo sets the ASKER's yolo for one test: it decides the child's calls. | ||
| 92 | +func askerYolo(t *testing.T, on bool) { | ||
| 93 | + t.Helper() | ||
| 94 | + old := config.Cfg.Yolo | ||
| 95 | + config.Cfg.Yolo = on | ||
| 96 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | ||
| 97 | +} | ||
| 98 | + | ||
| 90 | // One ask: the prompt reaches the child, its text streams to the sink and is | 99 | // One ask: the prompt reaches the child, its text streams to the sink and is |
| 91 | // returned, its tool call is shown under bob's name with the permission asked | 100 | // returned, its tool call is shown under bob's name with the permission asked |
| 92 | -// of the asker, and its thoughts are shown as thoughts. | 101 | +// of the asker (yolo off), and its thoughts are shown as thoughts. |
| 93 | func TestAskStreamsTheAnswerAndTheToolCalls(t *testing.T) { | 102 | func TestAskStreamsTheAnswerAndTheToolCalls(t *testing.T) { |
| 103 | + askerYolo(t, false) | ||
| 94 | p := fakePool(t, "tool") | 104 | p := fakePool(t, "tool") |
| 95 | sink := &recorder{allow: true} | 105 | sink := &recorder{allow: true} |
| 96 | answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "what is 2+2?", sink) | 106 | answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "what is 2+2?", sink) |
| @@ -115,6 +125,7 @@ func TestAskStreamsTheAnswerAndTheToolCalls(t *testing.T) { | |||
| 115 | 125 | ||
| 116 | // A refusal from the asker's user reaches the child as its reject option. | 126 | // A refusal from the asker's user reaches the child as its reject option. |
| 117 | func TestAskForwardsARefusal(t *testing.T) { | 127 | func TestAskForwardsARefusal(t *testing.T) { |
| 128 | + askerYolo(t, false) | ||
| 118 | p := fakePool(t, "tool") | 129 | p := fakePool(t, "tool") |
| 119 | answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", &recorder{allow: false}) | 130 | answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", &recorder{allow: false}) |
| 120 | if err != nil || !strings.HasSuffix(answer, "(tool rejected)") { | 131 | if err != nil || !strings.HasSuffix(answer, "(tool rejected)") { |
| @@ -122,6 +133,22 @@ func TestAskForwardsARefusal(t *testing.T) { | |||
| 122 | } | 133 | } |
| 123 | } | 134 | } |
| 124 | 135 | ||
| 136 | +// The asker's yolo wins: on, the child's call runs without asking the | ||
| 137 | +// asker's user — whatever the child's own config says, since it runs with | ||
| 138 | +// AGENT_YOLO=false and submits every call. | ||
| 139 | +func TestAskerInYoloModeAllowsWithoutAsking(t *testing.T) { | ||
| 140 | + askerYolo(t, true) | ||
| 141 | + p := fakePool(t, "tool") | ||
| 142 | + sink := &recorder{allow: false} | ||
| 143 | + answer, err := p.Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", sink) | ||
| 144 | + if err != nil || !strings.HasSuffix(answer, "(tool allowed)") { | ||
| 145 | + t.Errorf("answer %q, err %v, want the call allowed by the asker's yolo", answer, err) | ||
| 146 | + } | ||
| 147 | + if strings.Contains(sink.all(), "allow?") { | ||
| 148 | + t.Errorf("events %s: the asker's user was asked in yolo mode", sink.all()) | ||
| 149 | + } | ||
| 150 | +} | ||
| 151 | + | ||
| 125 | // The child is started once and reused, with a fresh session per ask. | 152 | // The child is started once and reused, with a fresh session per ask. |
| 126 | func TestAskReusesTheChildWithAFreshSession(t *testing.T) { | 153 | func TestAskReusesTheChildWithAFreshSession(t *testing.T) { |
| 127 | p := fakePool(t, "echo") | 154 | p := fakePool(t, "echo") |
modified
internal/config/config.go +58 -9 | @@ -12,6 +12,7 @@ import ( | ||
| 12 | 12 | "io/fs" |
| 13 | 13 | "os" |
| 14 | 14 | "path/filepath" |
| 15 | + "strconv" | |
| 15 | 16 | "strings" |
| 16 | 17 | "time" |
| 17 | 18 | |
| @@ -159,10 +160,18 @@ type Config struct { | ||
| 159 | 160 | // as wrapped mini-me tools that respect all safety contracts. |
| 160 | 161 | MCP MCPConfig `yaml:"mcp"` |
| 161 | 162 | |
| 162 | - // Permissions: whether the window mode asks before `bash` and the file | |
| 163 | - // tools run — "ask" shows a/s/r in the box, "auto" (default) runs them as | |
| 164 | - // the terminal always has. Only the window mode reads it today; the | |
| 165 | - // editor has its own dialog and the line mode has no gatekeeper. | |
| 163 | + // Yolo: whether the tools that change something (bash, write_file, | |
| 164 | + // edit_file, fetch_content saving, lsp rename, plan) run without asking. | |
| 165 | + // true (default) = they run; false = every call waits for the user — | |
| 166 | + // once, for the session, reject, never — in every front end, and every | |
| 167 | + // MCP tool asks too. Which tools EXIST is still bashTool/editTools/…: | |
| 168 | + // `yolo: true` + `bashTool: false` has no bash at all. An agent started | |
| 169 | + // by another one's /ask always asks its asker, whose yolo decides | |
| 170 | + // (AGENT_YOLO=false, see internal/ask). Override: AGENT_YOLO. | |
| 171 | + Yolo bool `yaml:"yolo"` | |
| 172 | + | |
| 173 | + // Permissions: DEPRECATED, replaced by Yolo. "ask" still means | |
| 174 | + // `yolo: false`; any value adds a banner warning (Deprecations). | |
| 166 | 175 | Permissions string `yaml:"permissions"` |
| 167 | 176 | |
| 168 | 177 | // MMConfigs: the other mm agents this one can ask (`/ask-<name>`), name |
| @@ -172,7 +181,12 @@ type Config struct { | ||
| 172 | 181 | MMConfigs map[string]string `yaml:"mmConfigs"` |
| 173 | 182 | } |
| 174 | 183 | |
| 175 | -// The two values of Permissions. | |
| 184 | +// YoloEnv names the variable that overrides `yolo` for one run ("true", | |
| 185 | +// "false", any strconv.ParseBool spelling). The /ask machinery sets it to | |
| 186 | +// false on the agents it starts, so the asker decides. | |
| 187 | +const YoloEnv = "AGENT_YOLO" | |
| 188 | + | |
| 189 | +// The two values of Permissions (MCP's, and the deprecated top-level key). | |
| 176 | 190 | const ( |
| 177 | 191 | PermissionsAuto = "auto" |
| 178 | 192 | PermissionsAsk = "ask" |
| @@ -472,7 +486,7 @@ which ones you left running.`, | ||
| 472 | 486 | Name: "bob", |
| 473 | 487 | Title: "Bob (bash-first agent)", |
| 474 | 488 | }, |
| 475 | - Permissions: PermissionsAuto, | |
| 489 | + Yolo: true, | |
| 476 | 490 | Context: ContextConfig{ |
| 477 | 491 | Enabled: false, |
| 478 | 492 | // 75 % leaves a quarter of the window for the next question, the tool |
| @@ -529,7 +543,10 @@ func Load(cliPath string) (string, error) { | ||
| 529 | 543 | if err := yaml.Unmarshal(data, &Cfg); err != nil { |
| 530 | 544 | return "", fmt.Errorf("%s: %w", path, err) |
| 531 | 545 | } |
| 532 | - applyEnv() | |
| 546 | + applyDeprecated() | |
| 547 | + if err := applyEnv(); err != nil { | |
| 548 | + return "", err | |
| 549 | + } | |
| 533 | 550 | resolveAgentPaths(path) |
| 534 | 551 | if err := Cfg.validate(); err != nil { |
| 535 | 552 | return "", fmt.Errorf("%s: %w", path, err) |
| @@ -541,7 +558,7 @@ func Load(cliPath string) (string, error) { | ||
| 541 | 558 | // and — resolved by the provider, see engine.Resolve — AGENT_BASE_URL. Same |
| 542 | 559 | // reasoning as for AGENT_CONFIG: switching engine for one run should not mean |
| 543 | 560 | // editing a file that is shown on screen. |
| 544 | -func applyEnv() { | |
| 561 | +func applyEnv() error { | |
| 545 | 562 | if v := os.Getenv("AGENT_PROVIDER"); v != "" { |
| 546 | 563 | Cfg.Provider = v |
| 547 | 564 | } |
| @@ -554,6 +571,38 @@ func applyEnv() { | ||
| 554 | 571 | if os.Getenv(SessionsEnv) == "off" { |
| 555 | 572 | Cfg.Sessions.Enabled = false |
| 556 | 573 | } |
| 574 | + if v := os.Getenv(YoloEnv); v != "" { | |
| 575 | + yolo, err := strconv.ParseBool(v) | |
| 576 | + if err != nil { | |
| 577 | + return fmt.Errorf("%s=%q: must be true or false", YoloEnv, v) | |
| 578 | + } | |
| 579 | + Cfg.Yolo = yolo | |
| 580 | + } | |
| 581 | + return nil | |
| 582 | +} | |
| 583 | + | |
| 584 | +// applyDeprecated maps the deprecated top-level `permissions` onto Yolo: | |
| 585 | +// "ask" was the window's way of asking, which `yolo: false` now means | |
| 586 | +// everywhere. It wins over `yolo: true` written beside it — of two | |
| 587 | +// contradictory settings, the one that asks is the safe one. | |
| 588 | +func applyDeprecated() { | |
| 589 | + if Cfg.Permissions == PermissionsAsk { | |
| 590 | + Cfg.Yolo = false | |
| 591 | + } | |
| 592 | +} | |
| 593 | + | |
| 594 | +// Deprecations lists the settings of the loaded file that still work but | |
| 595 | +// should be rewritten, one banner warning each. | |
| 596 | +// | |
| 597 | +// for _, w := range config.Deprecations() { | |
| 598 | +// fmt.Println("[warning: " + w + "]") | |
| 599 | +// } | |
| 600 | +func Deprecations() []string { | |
| 601 | + if Cfg.Permissions == "" { | |
| 602 | + return nil | |
| 603 | + } | |
| 604 | + return []string{fmt.Sprintf("permissions: %s is deprecated — use yolo: %t instead (yolo: false asks before every tool call, in every front end)", | |
| 605 | + Cfg.Permissions, Cfg.Permissions != PermissionsAsk)} | |
| 557 | 606 | } |
| 558 | 607 | |
| 559 | 608 | // validate checks the language servers: every one named and runnable, every |
| @@ -633,7 +682,7 @@ func (c Config) validate() error { | ||
| 633 | 682 | return errors.New("sessions.dir must not be empty") |
| 634 | 683 | case strings.TrimSpace(c.ACP.Name) == "": |
| 635 | 684 | return errors.New("acp.name must not be empty") |
| 636 | - case c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: | |
| 685 | + case c.Permissions != "" && c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: | |
| 637 | 686 | return errors.New(`permissions must be "auto" or "ask"`) |
| 638 | 687 | } |
| 639 | 688 | // `enabled: true` with no window and no maxMessages would never trigger — |
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | "io/fs" | 12 | "io/fs" |
| 13 | "os" | 13 | "os" |
| 14 | "path/filepath" | 14 | "path/filepath" |
| 15 | + "strconv" | ||
| 15 | "strings" | 16 | "strings" |
| 16 | "time" | 17 | "time" |
| 17 | 18 | ||
| @@ -159,10 +160,18 @@ type Config struct { | |||
| 159 | // as wrapped mini-me tools that respect all safety contracts. | 160 | // as wrapped mini-me tools that respect all safety contracts. |
| 160 | MCP MCPConfig `yaml:"mcp"` | 161 | MCP MCPConfig `yaml:"mcp"` |
| 161 | 162 | ||
| 162 | - // Permissions: whether the window mode asks before `bash` and the file | 163 | + // Yolo: whether the tools that change something (bash, write_file, |
| 163 | - // tools run — "ask" shows a/s/r in the box, "auto" (default) runs them as | 164 | + // edit_file, fetch_content saving, lsp rename, plan) run without asking. |
| 164 | - // the terminal always has. Only the window mode reads it today; the | 165 | + // true (default) = they run; false = every call waits for the user — |
| 165 | - // editor has its own dialog and the line mode has no gatekeeper. | 166 | + // once, for the session, reject, never — in every front end, and every |
| 167 | + // MCP tool asks too. Which tools EXIST is still bashTool/editTools/…: | ||
| 168 | + // `yolo: true` + `bashTool: false` has no bash at all. An agent started | ||
| 169 | + // by another one's /ask always asks its asker, whose yolo decides | ||
| 170 | + // (AGENT_YOLO=false, see internal/ask). Override: AGENT_YOLO. | ||
| 171 | + Yolo bool `yaml:"yolo"` | ||
| 172 | + | ||
| 173 | + // Permissions: DEPRECATED, replaced by Yolo. "ask" still means | ||
| 174 | + // `yolo: false`; any value adds a banner warning (Deprecations). | ||
| 166 | Permissions string `yaml:"permissions"` | 175 | Permissions string `yaml:"permissions"` |
| 167 | 176 | ||
| 168 | // MMConfigs: the other mm agents this one can ask (`/ask-<name>`), name | 177 | // MMConfigs: the other mm agents this one can ask (`/ask-<name>`), name |
| @@ -172,7 +181,12 @@ type Config struct { | |||
| 172 | MMConfigs map[string]string `yaml:"mmConfigs"` | 181 | MMConfigs map[string]string `yaml:"mmConfigs"` |
| 173 | } | 182 | } |
| 174 | 183 | ||
| 175 | -// The two values of Permissions. | 184 | +// YoloEnv names the variable that overrides `yolo` for one run ("true", |
| 185 | +// "false", any strconv.ParseBool spelling). The /ask machinery sets it to | ||
| 186 | +// false on the agents it starts, so the asker decides. | ||
| 187 | +const YoloEnv = "AGENT_YOLO" | ||
| 188 | + | ||
| 189 | +// The two values of Permissions (MCP's, and the deprecated top-level key). | ||
| 176 | const ( | 190 | const ( |
| 177 | PermissionsAuto = "auto" | 191 | PermissionsAuto = "auto" |
| 178 | PermissionsAsk = "ask" | 192 | PermissionsAsk = "ask" |
| @@ -472,7 +486,7 @@ which ones you left running.`, | |||
| 472 | Name: "bob", | 486 | Name: "bob", |
| 473 | Title: "Bob (bash-first agent)", | 487 | Title: "Bob (bash-first agent)", |
| 474 | }, | 488 | }, |
| 475 | - Permissions: PermissionsAuto, | 489 | + Yolo: true, |
| 476 | Context: ContextConfig{ | 490 | Context: ContextConfig{ |
| 477 | Enabled: false, | 491 | Enabled: false, |
| 478 | // 75 % leaves a quarter of the window for the next question, the tool | 492 | // 75 % leaves a quarter of the window for the next question, the tool |
| @@ -529,7 +543,10 @@ func Load(cliPath string) (string, error) { | |||
| 529 | if err := yaml.Unmarshal(data, &Cfg); err != nil { | 543 | if err := yaml.Unmarshal(data, &Cfg); err != nil { |
| 530 | return "", fmt.Errorf("%s: %w", path, err) | 544 | return "", fmt.Errorf("%s: %w", path, err) |
| 531 | } | 545 | } |
| 532 | - applyEnv() | 546 | + applyDeprecated() |
| 547 | + if err := applyEnv(); err != nil { | ||
| 548 | + return "", err | ||
| 549 | + } | ||
| 533 | resolveAgentPaths(path) | 550 | resolveAgentPaths(path) |
| 534 | if err := Cfg.validate(); err != nil { | 551 | if err := Cfg.validate(); err != nil { |
| 535 | return "", fmt.Errorf("%s: %w", path, err) | 552 | return "", fmt.Errorf("%s: %w", path, err) |
| @@ -541,7 +558,7 @@ func Load(cliPath string) (string, error) { | |||
| 541 | // and — resolved by the provider, see engine.Resolve — AGENT_BASE_URL. Same | 558 | // and — resolved by the provider, see engine.Resolve — AGENT_BASE_URL. Same |
| 542 | // reasoning as for AGENT_CONFIG: switching engine for one run should not mean | 559 | // reasoning as for AGENT_CONFIG: switching engine for one run should not mean |
| 543 | // editing a file that is shown on screen. | 560 | // editing a file that is shown on screen. |
| 544 | -func applyEnv() { | 561 | +func applyEnv() error { |
| 545 | if v := os.Getenv("AGENT_PROVIDER"); v != "" { | 562 | if v := os.Getenv("AGENT_PROVIDER"); v != "" { |
| 546 | Cfg.Provider = v | 563 | Cfg.Provider = v |
| 547 | } | 564 | } |
| @@ -554,6 +571,38 @@ func applyEnv() { | |||
| 554 | if os.Getenv(SessionsEnv) == "off" { | 571 | if os.Getenv(SessionsEnv) == "off" { |
| 555 | Cfg.Sessions.Enabled = false | 572 | Cfg.Sessions.Enabled = false |
| 556 | } | 573 | } |
| 574 | + if v := os.Getenv(YoloEnv); v != "" { | ||
| 575 | + yolo, err := strconv.ParseBool(v) | ||
| 576 | + if err != nil { | ||
| 577 | + return fmt.Errorf("%s=%q: must be true or false", YoloEnv, v) | ||
| 578 | + } | ||
| 579 | + Cfg.Yolo = yolo | ||
| 580 | + } | ||
| 581 | + return nil | ||
| 582 | +} | ||
| 583 | + | ||
| 584 | +// applyDeprecated maps the deprecated top-level `permissions` onto Yolo: | ||
| 585 | +// "ask" was the window's way of asking, which `yolo: false` now means | ||
| 586 | +// everywhere. It wins over `yolo: true` written beside it — of two | ||
| 587 | +// contradictory settings, the one that asks is the safe one. | ||
| 588 | +func applyDeprecated() { | ||
| 589 | + if Cfg.Permissions == PermissionsAsk { | ||
| 590 | + Cfg.Yolo = false | ||
| 591 | + } | ||
| 592 | +} | ||
| 593 | + | ||
| 594 | +// Deprecations lists the settings of the loaded file that still work but | ||
| 595 | +// should be rewritten, one banner warning each. | ||
| 596 | +// | ||
| 597 | +// for _, w := range config.Deprecations() { | ||
| 598 | +// fmt.Println("[warning: " + w + "]") | ||
| 599 | +// } | ||
| 600 | +func Deprecations() []string { | ||
| 601 | + if Cfg.Permissions == "" { | ||
| 602 | + return nil | ||
| 603 | + } | ||
| 604 | + return []string{fmt.Sprintf("permissions: %s is deprecated — use yolo: %t instead (yolo: false asks before every tool call, in every front end)", | ||
| 605 | + Cfg.Permissions, Cfg.Permissions != PermissionsAsk)} | ||
| 557 | } | 606 | } |
| 558 | 607 | ||
| 559 | // validate checks the language servers: every one named and runnable, every | 608 | // validate checks the language servers: every one named and runnable, every |
| @@ -633,7 +682,7 @@ func (c Config) validate() error { | |||
| 633 | return errors.New("sessions.dir must not be empty") | 682 | return errors.New("sessions.dir must not be empty") |
| 634 | case strings.TrimSpace(c.ACP.Name) == "": | 683 | case strings.TrimSpace(c.ACP.Name) == "": |
| 635 | return errors.New("acp.name must not be empty") | 684 | return errors.New("acp.name must not be empty") |
| 636 | - case c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: | 685 | + case c.Permissions != "" && c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: |
| 637 | return errors.New(`permissions must be "auto" or "ask"`) | 686 | return errors.New(`permissions must be "auto" or "ask"`) |
| 638 | } | 687 | } |
| 639 | // `enabled: true` with no window and no maxMessages would never trigger — | 688 | // `enabled: true` with no window and no maxMessages would never trigger — |
modified
internal/config/config_test.go +5 -5 | @@ -40,15 +40,15 @@ func TestSessionsConfig(t *testing.T) { | ||
| 40 | 40 | } |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | -// The window mode is off by default and permissions are "auto", so nothing | |
| 44 | -// changes for existing YAML files; `tui: true` and `permissions: ask` are | |
| 45 | -// read; any other permissions value is refused with its name. | |
| 43 | +// The window mode is off by default and yolo is on, so nothing changes for | |
| 44 | +// existing YAML files; `tui: true` is read; the deprecated `permissions` | |
| 45 | +// still accepts its two values — any other is refused with its name. | |
| 46 | 46 | func TestTUIAndPermissionsConfig(t *testing.T) { |
| 47 | 47 | defaults := Cfg |
| 48 | 48 | t.Cleanup(func() { Cfg = defaults }) |
| 49 | 49 | |
| 50 | - if Cfg.TUI || Cfg.Permissions != PermissionsAuto { | |
| 51 | - t.Fatalf("defaults: tui %v permissions %q", Cfg.TUI, Cfg.Permissions) | |
| 50 | + if Cfg.TUI || !Cfg.Yolo || Cfg.Permissions != "" { | |
| 51 | + t.Fatalf("defaults: tui %v yolo %v permissions %q", Cfg.TUI, Cfg.Yolo, Cfg.Permissions) | |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | 54 | dir := t.TempDir() |
| @@ -40,15 +40,15 @@ func TestSessionsConfig(t *testing.T) { | |||
| 40 | } | 40 | } |
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | -// The window mode is off by default and permissions are "auto", so nothing | 43 | +// The window mode is off by default and yolo is on, so nothing changes for |
| 44 | -// changes for existing YAML files; `tui: true` and `permissions: ask` are | 44 | +// existing YAML files; `tui: true` is read; the deprecated `permissions` |
| 45 | -// read; any other permissions value is refused with its name. | 45 | +// still accepts its two values — any other is refused with its name. |
| 46 | func TestTUIAndPermissionsConfig(t *testing.T) { | 46 | func TestTUIAndPermissionsConfig(t *testing.T) { |
| 47 | defaults := Cfg | 47 | defaults := Cfg |
| 48 | t.Cleanup(func() { Cfg = defaults }) | 48 | t.Cleanup(func() { Cfg = defaults }) |
| 49 | 49 | ||
| 50 | - if Cfg.TUI || Cfg.Permissions != PermissionsAuto { | 50 | + if Cfg.TUI || !Cfg.Yolo || Cfg.Permissions != "" { |
| 51 | - t.Fatalf("defaults: tui %v permissions %q", Cfg.TUI, Cfg.Permissions) | 51 | + t.Fatalf("defaults: tui %v yolo %v permissions %q", Cfg.TUI, Cfg.Yolo, Cfg.Permissions) |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | dir := t.TempDir() | 54 | dir := t.TempDir() |
added
internal/config/yolo_test.go +97 -0 | new file mode 100644 | ||
| @@ -0,0 +1,97 @@ | ||
| 1 | +package config | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "os" | |
| 5 | + "path/filepath" | |
| 6 | + "strings" | |
| 7 | + "testing" | |
| 8 | +) | |
| 9 | + | |
| 10 | +// loadYAML writes body to a temporary file, loads it on top of the defaults | |
| 11 | +// and restores the defaults when the test ends. | |
| 12 | +func loadYAML(t *testing.T, body string) error { | |
| 13 | + t.Helper() | |
| 14 | + defaults := Cfg | |
| 15 | + t.Cleanup(func() { Cfg = defaults }) | |
| 16 | + path := filepath.Join(t.TempDir(), "agent.yaml") | |
| 17 | + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { | |
| 18 | + t.Fatal(err) | |
| 19 | + } | |
| 20 | + _, err := Load(path) | |
| 21 | + return err | |
| 22 | +} | |
| 23 | + | |
| 24 | +// yolo is on unless a file says otherwise, and a file that never mentions it | |
| 25 | +// keeps the default. | |
| 26 | +func TestYoloDefaultsToTrue(t *testing.T) { | |
| 27 | + if err := loadYAML(t, "model: m\n"); err != nil { | |
| 28 | + t.Fatal(err) | |
| 29 | + } | |
| 30 | + if !Cfg.Yolo { | |
| 31 | + t.Error("yolo = false for a file that does not mention it, want true") | |
| 32 | + } | |
| 33 | + if got := Deprecations(); got != nil { | |
| 34 | + t.Errorf("Deprecations() = %v, want none", got) | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +func TestYoloFalseIsRead(t *testing.T) { | |
| 39 | + if err := loadYAML(t, "yolo: false\nbashTool: true\n"); err != nil { | |
| 40 | + t.Fatal(err) | |
| 41 | + } | |
| 42 | + if Cfg.Yolo || !Cfg.BashTool { | |
| 43 | + t.Errorf("yolo %v bashTool %v, want false/true", Cfg.Yolo, Cfg.BashTool) | |
| 44 | + } | |
| 45 | +} | |
| 46 | + | |
| 47 | +// The deprecated `permissions: ask` still asks, even beside `yolo: true` — | |
| 48 | +// the safe reading of two contradictory settings — and the banner says so. | |
| 49 | +func TestDeprecatedPermissionsAskTurnsYoloOff(t *testing.T) { | |
| 50 | + if err := loadYAML(t, "yolo: true\npermissions: ask\n"); err != nil { | |
| 51 | + t.Fatal(err) | |
| 52 | + } | |
| 53 | + if Cfg.Yolo { | |
| 54 | + t.Error("yolo = true with permissions: ask, want false") | |
| 55 | + } | |
| 56 | + got := Deprecations() | |
| 57 | + if len(got) != 1 || !strings.Contains(got[0], "yolo: false") { | |
| 58 | + t.Errorf("Deprecations() = %v, want one warning pointing at yolo: false", got) | |
| 59 | + } | |
| 60 | +} | |
| 61 | + | |
| 62 | +func TestDeprecatedPermissionsAutoOnlyWarns(t *testing.T) { | |
| 63 | + if err := loadYAML(t, "permissions: auto\n"); err != nil { | |
| 64 | + t.Fatal(err) | |
| 65 | + } | |
| 66 | + if !Cfg.Yolo { | |
| 67 | + t.Error("yolo = false with permissions: auto, want true") | |
| 68 | + } | |
| 69 | + if got := Deprecations(); len(got) != 1 || !strings.Contains(got[0], "yolo: true") { | |
| 70 | + t.Errorf("Deprecations() = %v, want one warning pointing at yolo: true", got) | |
| 71 | + } | |
| 72 | +} | |
| 73 | + | |
| 74 | +// AGENT_YOLO overrides the file both ways — the /ask machinery relies on | |
| 75 | +// false — and a value that is not a boolean is an error naming the variable. | |
| 76 | +func TestYoloEnvOverridesTheFile(t *testing.T) { | |
| 77 | + t.Setenv(YoloEnv, "false") | |
| 78 | + if err := loadYAML(t, "yolo: true\n"); err != nil { | |
| 79 | + t.Fatal(err) | |
| 80 | + } | |
| 81 | + if Cfg.Yolo { | |
| 82 | + t.Error("AGENT_YOLO=false did not override yolo: true") | |
| 83 | + } | |
| 84 | + | |
| 85 | + t.Setenv(YoloEnv, "true") | |
| 86 | + if err := loadYAML(t, "yolo: false\n"); err != nil { | |
| 87 | + t.Fatal(err) | |
| 88 | + } | |
| 89 | + if !Cfg.Yolo { | |
| 90 | + t.Error("AGENT_YOLO=true did not override yolo: false") | |
| 91 | + } | |
| 92 | + | |
| 93 | + t.Setenv(YoloEnv, "maybe") | |
| 94 | + if err := loadYAML(t, "model: m\n"); err == nil || !strings.Contains(err.Error(), YoloEnv) { | |
| 95 | + t.Errorf("Load with AGENT_YOLO=maybe = %v, want an error naming %s", err, YoloEnv) | |
| 96 | + } | |
| 97 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | +package config | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "os" | ||
| 5 | + "path/filepath" | ||
| 6 | + "strings" | ||
| 7 | + "testing" | ||
| 8 | +) | ||
| 9 | + | ||
| 10 | +// loadYAML writes body to a temporary file, loads it on top of the defaults | ||
| 11 | +// and restores the defaults when the test ends. | ||
| 12 | +func loadYAML(t *testing.T, body string) error { | ||
| 13 | + t.Helper() | ||
| 14 | + defaults := Cfg | ||
| 15 | + t.Cleanup(func() { Cfg = defaults }) | ||
| 16 | + path := filepath.Join(t.TempDir(), "agent.yaml") | ||
| 17 | + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { | ||
| 18 | + t.Fatal(err) | ||
| 19 | + } | ||
| 20 | + _, err := Load(path) | ||
| 21 | + return err | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +// yolo is on unless a file says otherwise, and a file that never mentions it | ||
| 25 | +// keeps the default. | ||
| 26 | +func TestYoloDefaultsToTrue(t *testing.T) { | ||
| 27 | + if err := loadYAML(t, "model: m\n"); err != nil { | ||
| 28 | + t.Fatal(err) | ||
| 29 | + } | ||
| 30 | + if !Cfg.Yolo { | ||
| 31 | + t.Error("yolo = false for a file that does not mention it, want true") | ||
| 32 | + } | ||
| 33 | + if got := Deprecations(); got != nil { | ||
| 34 | + t.Errorf("Deprecations() = %v, want none", got) | ||
| 35 | + } | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +func TestYoloFalseIsRead(t *testing.T) { | ||
| 39 | + if err := loadYAML(t, "yolo: false\nbashTool: true\n"); err != nil { | ||
| 40 | + t.Fatal(err) | ||
| 41 | + } | ||
| 42 | + if Cfg.Yolo || !Cfg.BashTool { | ||
| 43 | + t.Errorf("yolo %v bashTool %v, want false/true", Cfg.Yolo, Cfg.BashTool) | ||
| 44 | + } | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +// The deprecated `permissions: ask` still asks, even beside `yolo: true` — | ||
| 48 | +// the safe reading of two contradictory settings — and the banner says so. | ||
| 49 | +func TestDeprecatedPermissionsAskTurnsYoloOff(t *testing.T) { | ||
| 50 | + if err := loadYAML(t, "yolo: true\npermissions: ask\n"); err != nil { | ||
| 51 | + t.Fatal(err) | ||
| 52 | + } | ||
| 53 | + if Cfg.Yolo { | ||
| 54 | + t.Error("yolo = true with permissions: ask, want false") | ||
| 55 | + } | ||
| 56 | + got := Deprecations() | ||
| 57 | + if len(got) != 1 || !strings.Contains(got[0], "yolo: false") { | ||
| 58 | + t.Errorf("Deprecations() = %v, want one warning pointing at yolo: false", got) | ||
| 59 | + } | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +func TestDeprecatedPermissionsAutoOnlyWarns(t *testing.T) { | ||
| 63 | + if err := loadYAML(t, "permissions: auto\n"); err != nil { | ||
| 64 | + t.Fatal(err) | ||
| 65 | + } | ||
| 66 | + if !Cfg.Yolo { | ||
| 67 | + t.Error("yolo = false with permissions: auto, want true") | ||
| 68 | + } | ||
| 69 | + if got := Deprecations(); len(got) != 1 || !strings.Contains(got[0], "yolo: true") { | ||
| 70 | + t.Errorf("Deprecations() = %v, want one warning pointing at yolo: true", got) | ||
| 71 | + } | ||
| 72 | +} | ||
| 73 | + | ||
| 74 | +// AGENT_YOLO overrides the file both ways — the /ask machinery relies on | ||
| 75 | +// false — and a value that is not a boolean is an error naming the variable. | ||
| 76 | +func TestYoloEnvOverridesTheFile(t *testing.T) { | ||
| 77 | + t.Setenv(YoloEnv, "false") | ||
| 78 | + if err := loadYAML(t, "yolo: true\n"); err != nil { | ||
| 79 | + t.Fatal(err) | ||
| 80 | + } | ||
| 81 | + if Cfg.Yolo { | ||
| 82 | + t.Error("AGENT_YOLO=false did not override yolo: true") | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + t.Setenv(YoloEnv, "true") | ||
| 86 | + if err := loadYAML(t, "yolo: false\n"); err != nil { | ||
| 87 | + t.Fatal(err) | ||
| 88 | + } | ||
| 89 | + if !Cfg.Yolo { | ||
| 90 | + t.Error("AGENT_YOLO=true did not override yolo: false") | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + t.Setenv(YoloEnv, "maybe") | ||
| 94 | + if err := loadYAML(t, "model: m\n"); err == nil || !strings.Contains(err.Error(), YoloEnv) { | ||
| 95 | + t.Errorf("Load with AGENT_YOLO=maybe = %v, want an error naming %s", err, YoloEnv) | ||
| 96 | + } | ||
| 97 | +} | ||
added
internal/consent/consent.go +196 -0 | new file mode 100644 | ||
| @@ -0,0 +1,196 @@ | ||
| 1 | +// Package consent decides whether a tool call may run: the `yolo` switch, the | |
| 2 | +// answers a user can give when it is off, and what a session remembers of | |
| 3 | +// them. Every front end asks its own way — a/s/r/n in the window's box, the | |
| 4 | +// editor's dialog, a line on the terminal — but the rules are this package's, | |
| 5 | +// so they cannot drift apart: | |
| 6 | +// | |
| 7 | +// - yolo: true (the default): the built-in tools run without asking; | |
| 8 | +// - yolo: false: every call that changes something (bash, write_file, | |
| 9 | +// edit_file, fetch_content saving, lsp rename, one plan action) waits for | |
| 10 | +// the user, and so does every MCP tool; | |
| 11 | +// - an MCP tool whose permissions is "ask" asks whatever yolo says. | |
| 12 | +// | |
| 13 | +// A user answers once, for the session, or never: see Answer and Policy. | |
| 14 | +package consent | |
| 15 | + | |
| 16 | +import ( | |
| 17 | + "context" | |
| 18 | + "fmt" | |
| 19 | + "os" | |
| 20 | + "strings" | |
| 21 | + "sync" | |
| 22 | + | |
| 23 | + "mm/internal/config" | |
| 24 | + "mm/internal/ui" | |
| 25 | +) | |
| 26 | + | |
| 27 | +// Answer is what the user chose for one call. | |
| 28 | +type Answer int | |
| 29 | + | |
| 30 | +const ( | |
| 31 | + // RejectOnce refuses this call; the next one asks again. The zero value, | |
| 32 | + // so an abandoned question (cancelled turn, closed dialog) refuses. | |
| 33 | + RejectOnce Answer = iota | |
| 34 | + // AllowOnce runs this call; the next one asks again. | |
| 35 | + AllowOnce | |
| 36 | + // AllowSession runs this call and every later call of the same tool, | |
| 37 | + // for the rest of the session. | |
| 38 | + AllowSession | |
| 39 | + // Never refuses this call and every later call of the same tool, for the | |
| 40 | + // rest of the session, without asking again. | |
| 41 | + Never | |
| 42 | +) | |
| 43 | + | |
| 44 | +// Allowed reports whether the call runs. | |
| 45 | +func (a Answer) Allowed() bool { return a == AllowOnce || a == AllowSession } | |
| 46 | + | |
| 47 | +// Parse reads a typed answer — the letters the window and the terminal show: | |
| 48 | +// a (allow once), s (session), r (reject), n (never), case-insensitive, or | |
| 49 | +// the whole word. ok is false for anything else. | |
| 50 | +// | |
| 51 | +// a, ok := consent.Parse("s") // AllowSession, true | |
| 52 | +func Parse(s string) (Answer, bool) { | |
| 53 | + switch strings.ToLower(strings.TrimSpace(s)) { | |
| 54 | + case "a", "allow", "once", "y", "yes": | |
| 55 | + return AllowOnce, true | |
| 56 | + case "s", "session": | |
| 57 | + return AllowSession, true | |
| 58 | + case "r", "reject", "no": | |
| 59 | + return RejectOnce, true | |
| 60 | + case "n", "never": | |
| 61 | + return Never, true | |
| 62 | + } | |
| 63 | + return RejectOnce, false | |
| 64 | +} | |
| 65 | + | |
| 66 | +// Choices is the one-line legend of the four answers, for a prompt. | |
| 67 | +// | |
| 68 | +// fmt.Printf("run %s? %s ", ev.Title, consent.Choices(ev.Tool)) | |
| 69 | +func Choices(tool string) string { | |
| 70 | + return "[a] allow once · [s] allow " + tool + " for this session · [r] reject · [n] never " + tool | |
| 71 | +} | |
| 72 | + | |
| 73 | +// Policy remembers the session-wide answers, per tool name. A front end keeps | |
| 74 | +// one per session; the zero value is ready to use, and it is safe for the | |
| 75 | +// concurrent tool calls Genkit may make. | |
| 76 | +// | |
| 77 | +// var p consent.Policy | |
| 78 | +// ok := p.Decide(ctx, "bash", func(ctx context.Context) consent.Answer { | |
| 79 | +// return askTheUser(ctx) // only called when nothing is remembered | |
| 80 | +// }) | |
| 81 | +type Policy struct { | |
| 82 | + mu sync.Mutex | |
| 83 | + granted map[string]bool // true = AllowSession, false = Never | |
| 84 | +} | |
| 85 | + | |
| 86 | +// Remembered returns the session-wide answer for tool, if there is one. | |
| 87 | +func (p *Policy) Remembered(tool string) (allowed, known bool) { | |
| 88 | + p.mu.Lock() | |
| 89 | + defer p.mu.Unlock() | |
| 90 | + allowed, known = p.granted[tool] | |
| 91 | + return allowed, known | |
| 92 | +} | |
| 93 | + | |
| 94 | +// Record keeps a session-wide answer (AllowSession, Never) and reports | |
| 95 | +// whether the call runs. Once-only answers are not kept. | |
| 96 | +func (p *Policy) Record(tool string, a Answer) bool { | |
| 97 | + if a == AllowSession || a == Never { | |
| 98 | + p.mu.Lock() | |
| 99 | + if p.granted == nil { | |
| 100 | + p.granted = map[string]bool{} | |
| 101 | + } | |
| 102 | + p.granted[tool] = a == AllowSession | |
| 103 | + p.mu.Unlock() | |
| 104 | + } | |
| 105 | + return a.Allowed() | |
| 106 | +} | |
| 107 | + | |
| 108 | +// Decide returns the remembered answer for tool, or asks and records what the | |
| 109 | +// user chose. ask is not called when the session already decided. | |
| 110 | +func (p *Policy) Decide(ctx context.Context, tool string, ask func(context.Context) Answer) bool { | |
| 111 | + if allowed, known := p.Remembered(tool); known { | |
| 112 | + return allowed | |
| 113 | + } | |
| 114 | + return p.Record(tool, ask(ctx)) | |
| 115 | +} | |
| 116 | + | |
| 117 | +// Required reports whether the built-in tools must ask before running: | |
| 118 | +// `yolo: false` in the configuration (or AGENT_YOLO=false). | |
| 119 | +func Required() bool { return !config.Cfg.Yolo } | |
| 120 | + | |
| 121 | +// Gate is the built-in tools' check on the event path (window, editor, an | |
| 122 | +// agent asked by another): with yolo on the call runs; otherwise the sink | |
| 123 | +// asks its user. | |
| 124 | +// | |
| 125 | +// if !consent.Gate(ctx, s, ev) { | |
| 126 | +// return "[command rejected by the user]" | |
| 127 | +// } | |
| 128 | +func Gate(ctx context.Context, s ui.Sink, ev ui.ToolEvent) bool { | |
| 129 | + if !Required() { | |
| 130 | + return true | |
| 131 | + } | |
| 132 | + return s.Allow(ctx, ev) | |
| 133 | +} | |
| 134 | + | |
| 135 | +// Prompter asks the user of the line mode about one call. The REPL installs | |
| 136 | +// one with SetPrompter; without one (one-shot -p, mm --ask) nobody can answer. | |
| 137 | +type Prompter func(ctx context.Context, ev ui.ToolEvent) bool | |
| 138 | + | |
| 139 | +var ( | |
| 140 | + prompterMu sync.RWMutex | |
| 141 | + prompter Prompter | |
| 142 | + noticeOnce sync.Once | |
| 143 | +) | |
| 144 | + | |
| 145 | +// SetPrompter installs the line mode's prompter (nil removes it) and returns | |
| 146 | +// a function restoring the previous one. | |
| 147 | +// | |
| 148 | +// restore := consent.SetPrompter(repl.allow) | |
| 149 | +// defer restore() | |
| 150 | +func SetPrompter(p Prompter) (restore func()) { | |
| 151 | + prompterMu.Lock() | |
| 152 | + old := prompter | |
| 153 | + prompter = p | |
| 154 | + prompterMu.Unlock() | |
| 155 | + return func() { | |
| 156 | + prompterMu.Lock() | |
| 157 | + prompter = old | |
| 158 | + prompterMu.Unlock() | |
| 159 | + } | |
| 160 | +} | |
| 161 | + | |
| 162 | +// Terminal is Gate for the line mode's tools, which print themselves and have | |
| 163 | +// no sink: with yolo on the call runs, otherwise the prompter asks. | |
| 164 | +// | |
| 165 | +// if !consent.Terminal(ctx, ev) { | |
| 166 | +// return "[command rejected by the user]" | |
| 167 | +// } | |
| 168 | +func Terminal(ctx context.Context, ev ui.ToolEvent) bool { | |
| 169 | + if !Required() { | |
| 170 | + return true | |
| 171 | + } | |
| 172 | + return Prompt(ctx, ev) | |
| 173 | +} | |
| 174 | + | |
| 175 | +// Prompt asks the line mode's user whatever yolo says — the MCP tools marked | |
| 176 | +// "ask" need it. With no prompter installed the call is refused, and stderr | |
| 177 | +// says why once: a script running `mm -p` must fail visibly, not hang on a | |
| 178 | +// question nobody can see. | |
| 179 | +func Prompt(ctx context.Context, ev ui.ToolEvent) bool { | |
| 180 | + prompterMu.RLock() | |
| 181 | + p := prompter | |
| 182 | + prompterMu.RUnlock() | |
| 183 | + if p == nil { | |
| 184 | + CannotAsk() | |
| 185 | + return false | |
| 186 | + } | |
| 187 | + return p(ctx, ev) | |
| 188 | +} | |
| 189 | + | |
| 190 | +// CannotAsk prints, once per process, why tool calls are refused in a mode | |
| 191 | +// that cannot ask (one-shot -p, mm --ask). | |
| 192 | +func CannotAsk() { | |
| 193 | + noticeOnce.Do(func() { | |
| 194 | + fmt.Fprintln(os.Stderr, "[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]") | |
| 195 | + }) | |
| 196 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,196 @@ | |||
| 1 | +// Package consent decides whether a tool call may run: the `yolo` switch, the | ||
| 2 | +// answers a user can give when it is off, and what a session remembers of | ||
| 3 | +// them. Every front end asks its own way — a/s/r/n in the window's box, the | ||
| 4 | +// editor's dialog, a line on the terminal — but the rules are this package's, | ||
| 5 | +// so they cannot drift apart: | ||
| 6 | +// | ||
| 7 | +// - yolo: true (the default): the built-in tools run without asking; | ||
| 8 | +// - yolo: false: every call that changes something (bash, write_file, | ||
| 9 | +// edit_file, fetch_content saving, lsp rename, one plan action) waits for | ||
| 10 | +// the user, and so does every MCP tool; | ||
| 11 | +// - an MCP tool whose permissions is "ask" asks whatever yolo says. | ||
| 12 | +// | ||
| 13 | +// A user answers once, for the session, or never: see Answer and Policy. | ||
| 14 | +package consent | ||
| 15 | + | ||
| 16 | +import ( | ||
| 17 | + "context" | ||
| 18 | + "fmt" | ||
| 19 | + "os" | ||
| 20 | + "strings" | ||
| 21 | + "sync" | ||
| 22 | + | ||
| 23 | + "mm/internal/config" | ||
| 24 | + "mm/internal/ui" | ||
| 25 | +) | ||
| 26 | + | ||
| 27 | +// Answer is what the user chose for one call. | ||
| 28 | +type Answer int | ||
| 29 | + | ||
| 30 | +const ( | ||
| 31 | + // RejectOnce refuses this call; the next one asks again. The zero value, | ||
| 32 | + // so an abandoned question (cancelled turn, closed dialog) refuses. | ||
| 33 | + RejectOnce Answer = iota | ||
| 34 | + // AllowOnce runs this call; the next one asks again. | ||
| 35 | + AllowOnce | ||
| 36 | + // AllowSession runs this call and every later call of the same tool, | ||
| 37 | + // for the rest of the session. | ||
| 38 | + AllowSession | ||
| 39 | + // Never refuses this call and every later call of the same tool, for the | ||
| 40 | + // rest of the session, without asking again. | ||
| 41 | + Never | ||
| 42 | +) | ||
| 43 | + | ||
| 44 | +// Allowed reports whether the call runs. | ||
| 45 | +func (a Answer) Allowed() bool { return a == AllowOnce || a == AllowSession } | ||
| 46 | + | ||
| 47 | +// Parse reads a typed answer — the letters the window and the terminal show: | ||
| 48 | +// a (allow once), s (session), r (reject), n (never), case-insensitive, or | ||
| 49 | +// the whole word. ok is false for anything else. | ||
| 50 | +// | ||
| 51 | +// a, ok := consent.Parse("s") // AllowSession, true | ||
| 52 | +func Parse(s string) (Answer, bool) { | ||
| 53 | + switch strings.ToLower(strings.TrimSpace(s)) { | ||
| 54 | + case "a", "allow", "once", "y", "yes": | ||
| 55 | + return AllowOnce, true | ||
| 56 | + case "s", "session": | ||
| 57 | + return AllowSession, true | ||
| 58 | + case "r", "reject", "no": | ||
| 59 | + return RejectOnce, true | ||
| 60 | + case "n", "never": | ||
| 61 | + return Never, true | ||
| 62 | + } | ||
| 63 | + return RejectOnce, false | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +// Choices is the one-line legend of the four answers, for a prompt. | ||
| 67 | +// | ||
| 68 | +// fmt.Printf("run %s? %s ", ev.Title, consent.Choices(ev.Tool)) | ||
| 69 | +func Choices(tool string) string { | ||
| 70 | + return "[a] allow once · [s] allow " + tool + " for this session · [r] reject · [n] never " + tool | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// Policy remembers the session-wide answers, per tool name. A front end keeps | ||
| 74 | +// one per session; the zero value is ready to use, and it is safe for the | ||
| 75 | +// concurrent tool calls Genkit may make. | ||
| 76 | +// | ||
| 77 | +// var p consent.Policy | ||
| 78 | +// ok := p.Decide(ctx, "bash", func(ctx context.Context) consent.Answer { | ||
| 79 | +// return askTheUser(ctx) // only called when nothing is remembered | ||
| 80 | +// }) | ||
| 81 | +type Policy struct { | ||
| 82 | + mu sync.Mutex | ||
| 83 | + granted map[string]bool // true = AllowSession, false = Never | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +// Remembered returns the session-wide answer for tool, if there is one. | ||
| 87 | +func (p *Policy) Remembered(tool string) (allowed, known bool) { | ||
| 88 | + p.mu.Lock() | ||
| 89 | + defer p.mu.Unlock() | ||
| 90 | + allowed, known = p.granted[tool] | ||
| 91 | + return allowed, known | ||
| 92 | +} | ||
| 93 | + | ||
| 94 | +// Record keeps a session-wide answer (AllowSession, Never) and reports | ||
| 95 | +// whether the call runs. Once-only answers are not kept. | ||
| 96 | +func (p *Policy) Record(tool string, a Answer) bool { | ||
| 97 | + if a == AllowSession || a == Never { | ||
| 98 | + p.mu.Lock() | ||
| 99 | + if p.granted == nil { | ||
| 100 | + p.granted = map[string]bool{} | ||
| 101 | + } | ||
| 102 | + p.granted[tool] = a == AllowSession | ||
| 103 | + p.mu.Unlock() | ||
| 104 | + } | ||
| 105 | + return a.Allowed() | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +// Decide returns the remembered answer for tool, or asks and records what the | ||
| 109 | +// user chose. ask is not called when the session already decided. | ||
| 110 | +func (p *Policy) Decide(ctx context.Context, tool string, ask func(context.Context) Answer) bool { | ||
| 111 | + if allowed, known := p.Remembered(tool); known { | ||
| 112 | + return allowed | ||
| 113 | + } | ||
| 114 | + return p.Record(tool, ask(ctx)) | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +// Required reports whether the built-in tools must ask before running: | ||
| 118 | +// `yolo: false` in the configuration (or AGENT_YOLO=false). | ||
| 119 | +func Required() bool { return !config.Cfg.Yolo } | ||
| 120 | + | ||
| 121 | +// Gate is the built-in tools' check on the event path (window, editor, an | ||
| 122 | +// agent asked by another): with yolo on the call runs; otherwise the sink | ||
| 123 | +// asks its user. | ||
| 124 | +// | ||
| 125 | +// if !consent.Gate(ctx, s, ev) { | ||
| 126 | +// return "[command rejected by the user]" | ||
| 127 | +// } | ||
| 128 | +func Gate(ctx context.Context, s ui.Sink, ev ui.ToolEvent) bool { | ||
| 129 | + if !Required() { | ||
| 130 | + return true | ||
| 131 | + } | ||
| 132 | + return s.Allow(ctx, ev) | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +// Prompter asks the user of the line mode about one call. The REPL installs | ||
| 136 | +// one with SetPrompter; without one (one-shot -p, mm --ask) nobody can answer. | ||
| 137 | +type Prompter func(ctx context.Context, ev ui.ToolEvent) bool | ||
| 138 | + | ||
| 139 | +var ( | ||
| 140 | + prompterMu sync.RWMutex | ||
| 141 | + prompter Prompter | ||
| 142 | + noticeOnce sync.Once | ||
| 143 | +) | ||
| 144 | + | ||
| 145 | +// SetPrompter installs the line mode's prompter (nil removes it) and returns | ||
| 146 | +// a function restoring the previous one. | ||
| 147 | +// | ||
| 148 | +// restore := consent.SetPrompter(repl.allow) | ||
| 149 | +// defer restore() | ||
| 150 | +func SetPrompter(p Prompter) (restore func()) { | ||
| 151 | + prompterMu.Lock() | ||
| 152 | + old := prompter | ||
| 153 | + prompter = p | ||
| 154 | + prompterMu.Unlock() | ||
| 155 | + return func() { | ||
| 156 | + prompterMu.Lock() | ||
| 157 | + prompter = old | ||
| 158 | + prompterMu.Unlock() | ||
| 159 | + } | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +// Terminal is Gate for the line mode's tools, which print themselves and have | ||
| 163 | +// no sink: with yolo on the call runs, otherwise the prompter asks. | ||
| 164 | +// | ||
| 165 | +// if !consent.Terminal(ctx, ev) { | ||
| 166 | +// return "[command rejected by the user]" | ||
| 167 | +// } | ||
| 168 | +func Terminal(ctx context.Context, ev ui.ToolEvent) bool { | ||
| 169 | + if !Required() { | ||
| 170 | + return true | ||
| 171 | + } | ||
| 172 | + return Prompt(ctx, ev) | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | +// Prompt asks the line mode's user whatever yolo says — the MCP tools marked | ||
| 176 | +// "ask" need it. With no prompter installed the call is refused, and stderr | ||
| 177 | +// says why once: a script running `mm -p` must fail visibly, not hang on a | ||
| 178 | +// question nobody can see. | ||
| 179 | +func Prompt(ctx context.Context, ev ui.ToolEvent) bool { | ||
| 180 | + prompterMu.RLock() | ||
| 181 | + p := prompter | ||
| 182 | + prompterMu.RUnlock() | ||
| 183 | + if p == nil { | ||
| 184 | + CannotAsk() | ||
| 185 | + return false | ||
| 186 | + } | ||
| 187 | + return p(ctx, ev) | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +// CannotAsk prints, once per process, why tool calls are refused in a mode | ||
| 191 | +// that cannot ask (one-shot -p, mm --ask). | ||
| 192 | +func CannotAsk() { | ||
| 193 | + noticeOnce.Do(func() { | ||
| 194 | + fmt.Fprintln(os.Stderr, "[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]") | ||
| 195 | + }) | ||
| 196 | +} | ||
added
internal/consent/consent_test.go +140 -0 | new file mode 100644 | ||
| @@ -0,0 +1,140 @@ | ||
| 1 | +package consent | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "testing" | |
| 6 | + | |
| 7 | + "mm/internal/config" | |
| 8 | + "mm/internal/ui" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// withYolo sets config.Cfg.Yolo for one test. | |
| 12 | +func withYolo(t *testing.T, on bool) { | |
| 13 | + t.Helper() | |
| 14 | + old := config.Cfg.Yolo | |
| 15 | + config.Cfg.Yolo = on | |
| 16 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | |
| 17 | +} | |
| 18 | + | |
| 19 | +// askingSink is a ui.Sink whose Allow answers allow and counts the questions. | |
| 20 | +type askingSink struct { | |
| 21 | + allow bool | |
| 22 | + asked int | |
| 23 | +} | |
| 24 | + | |
| 25 | +func (s *askingSink) Text(string) { /* not under test */ } | |
| 26 | +func (s *askingSink) Reasoning(string) { /* not under test */ } | |
| 27 | +func (s *askingSink) ToolStart(ui.ToolEvent) { /* not under test */ } | |
| 28 | +func (s *askingSink) ToolRunning(string) { /* not under test */ } | |
| 29 | +func (s *askingSink) ToolEnd(string, ui.ToolResult) { /* not under test */ } | |
| 30 | +func (s *askingSink) WorkDir() string { return "" } | |
| 31 | +func (s *askingSink) Allow(context.Context, ui.ToolEvent) bool { s.asked++; return s.allow } | |
| 32 | + | |
| 33 | +func TestParseReadsTheFourLettersAndWords(t *testing.T) { | |
| 34 | + cases := map[string]Answer{ | |
| 35 | + "a": AllowOnce, "A": AllowOnce, " once ": AllowOnce, | |
| 36 | + "s": AllowSession, "session": AllowSession, | |
| 37 | + "r": RejectOnce, "reject": RejectOnce, | |
| 38 | + "n": Never, "NEVER": Never, | |
| 39 | + } | |
| 40 | + for in, want := range cases { | |
| 41 | + if got, ok := Parse(in); !ok || got != want { | |
| 42 | + t.Errorf("Parse(%q) = %v, %v; want %v, true", in, got, ok, want) | |
| 43 | + } | |
| 44 | + } | |
| 45 | + if _, ok := Parse("maybe"); ok { | |
| 46 | + t.Error(`Parse("maybe") ok, want refused`) | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | |
| 50 | +func TestOnlyAllowAnswersRun(t *testing.T) { | |
| 51 | + for a, want := range map[Answer]bool{AllowOnce: true, AllowSession: true, RejectOnce: false, Never: false} { | |
| 52 | + if a.Allowed() != want { | |
| 53 | + t.Errorf("%v.Allowed() = %v, want %v", a, a.Allowed(), want) | |
| 54 | + } | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +// Once-only answers ask again on the next call; session answers and "never" | |
| 59 | +// are remembered per tool and stop the questions. | |
| 60 | +func TestPolicyRemembersSessionAnswersOnly(t *testing.T) { | |
| 61 | + var p Policy | |
| 62 | + asked := 0 | |
| 63 | + answer := func(a Answer) func(context.Context) Answer { | |
| 64 | + return func(context.Context) Answer { asked++; return a } | |
| 65 | + } | |
| 66 | + ctx := context.Background() | |
| 67 | + | |
| 68 | + if !p.Decide(ctx, "bash", answer(AllowOnce)) || p.Decide(ctx, "bash", answer(RejectOnce)) { | |
| 69 | + t.Fatal("once answers not applied as given") | |
| 70 | + } | |
| 71 | + if asked != 2 { | |
| 72 | + t.Fatalf("asked %d times for two once answers, want 2", asked) | |
| 73 | + } | |
| 74 | + | |
| 75 | + if !p.Decide(ctx, "bash", answer(AllowSession)) || !p.Decide(ctx, "bash", answer(RejectOnce)) { | |
| 76 | + t.Error("allow for the session not remembered") | |
| 77 | + } | |
| 78 | + if p.Decide(ctx, "edit_file", answer(Never)) || p.Decide(ctx, "edit_file", answer(AllowOnce)) { | |
| 79 | + t.Error("never not remembered") | |
| 80 | + } | |
| 81 | + if asked != 4 { | |
| 82 | + t.Errorf("asked %d times, want 4 (remembered answers do not ask)", asked) | |
| 83 | + } | |
| 84 | + if allowed, known := p.Remembered("write_file"); known || allowed { | |
| 85 | + t.Error("a tool never answered for is remembered") | |
| 86 | + } | |
| 87 | +} | |
| 88 | + | |
| 89 | +func TestGateRunsWithoutAskingWhenYoloIsOn(t *testing.T) { | |
| 90 | + withYolo(t, true) | |
| 91 | + s := &askingSink{} | |
| 92 | + if !Gate(context.Background(), s, ui.ToolEvent{Tool: "bash"}) || s.asked != 0 { | |
| 93 | + t.Errorf("yolo on: allowed=false or asked %d times", s.asked) | |
| 94 | + } | |
| 95 | +} | |
| 96 | + | |
| 97 | +func TestGateAsksTheSinkWhenYoloIsOff(t *testing.T) { | |
| 98 | + withYolo(t, false) | |
| 99 | + s := &askingSink{allow: false} | |
| 100 | + if Gate(context.Background(), s, ui.ToolEvent{Tool: "bash"}) || s.asked != 1 { | |
| 101 | + t.Errorf("yolo off: the sink's refusal was not applied (asked %d)", s.asked) | |
| 102 | + } | |
| 103 | +} | |
| 104 | + | |
| 105 | +// The line mode's tools have no sink: Terminal asks the installed prompter, | |
| 106 | +// and with none (mm -p) refuses rather than running unconfirmed. | |
| 107 | +func TestTerminalUsesThePrompterOrRefuses(t *testing.T) { | |
| 108 | + withYolo(t, false) | |
| 109 | + ev := ui.ToolEvent{Tool: "bash"} | |
| 110 | + if Terminal(context.Background(), ev) { | |
| 111 | + t.Error("no prompter installed: the call ran") | |
| 112 | + } | |
| 113 | + | |
| 114 | + asked := 0 | |
| 115 | + restore := SetPrompter(func(context.Context, ui.ToolEvent) bool { asked++; return true }) | |
| 116 | + if !Terminal(context.Background(), ev) || asked != 1 { | |
| 117 | + t.Errorf("prompter not consulted (asked %d)", asked) | |
| 118 | + } | |
| 119 | + restore() | |
| 120 | + if Terminal(context.Background(), ev) || asked != 1 { | |
| 121 | + t.Error("restore did not remove the prompter") | |
| 122 | + } | |
| 123 | +} | |
| 124 | + | |
| 125 | +func TestTerminalRunsWithoutAskingWhenYoloIsOn(t *testing.T) { | |
| 126 | + withYolo(t, true) | |
| 127 | + if !Terminal(context.Background(), ui.ToolEvent{Tool: "bash"}) { | |
| 128 | + t.Error("yolo on with no prompter: the call was refused") | |
| 129 | + } | |
| 130 | +} | |
| 131 | + | |
| 132 | +// Prompt is for the MCP tools marked "ask": it asks whatever yolo says. | |
| 133 | +func TestPromptAsksEvenWithYoloOn(t *testing.T) { | |
| 134 | + withYolo(t, true) | |
| 135 | + asked := 0 | |
| 136 | + defer SetPrompter(func(context.Context, ui.ToolEvent) bool { asked++; return false })() | |
| 137 | + if Prompt(context.Background(), ui.ToolEvent{Tool: "ctx7/query"}) || asked != 1 { | |
| 138 | + t.Errorf("Prompt with yolo on: asked %d, want 1 and the refusal applied", asked) | |
| 139 | + } | |
| 140 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,140 @@ | |||
| 1 | +package consent | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "testing" | ||
| 6 | + | ||
| 7 | + "mm/internal/config" | ||
| 8 | + "mm/internal/ui" | ||
| 9 | +) | ||
| 10 | + | ||
| 11 | +// withYolo sets config.Cfg.Yolo for one test. | ||
| 12 | +func withYolo(t *testing.T, on bool) { | ||
| 13 | + t.Helper() | ||
| 14 | + old := config.Cfg.Yolo | ||
| 15 | + config.Cfg.Yolo = on | ||
| 16 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +// askingSink is a ui.Sink whose Allow answers allow and counts the questions. | ||
| 20 | +type askingSink struct { | ||
| 21 | + allow bool | ||
| 22 | + asked int | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +func (s *askingSink) Text(string) { /* not under test */ } | ||
| 26 | +func (s *askingSink) Reasoning(string) { /* not under test */ } | ||
| 27 | +func (s *askingSink) ToolStart(ui.ToolEvent) { /* not under test */ } | ||
| 28 | +func (s *askingSink) ToolRunning(string) { /* not under test */ } | ||
| 29 | +func (s *askingSink) ToolEnd(string, ui.ToolResult) { /* not under test */ } | ||
| 30 | +func (s *askingSink) WorkDir() string { return "" } | ||
| 31 | +func (s *askingSink) Allow(context.Context, ui.ToolEvent) bool { s.asked++; return s.allow } | ||
| 32 | + | ||
| 33 | +func TestParseReadsTheFourLettersAndWords(t *testing.T) { | ||
| 34 | + cases := map[string]Answer{ | ||
| 35 | + "a": AllowOnce, "A": AllowOnce, " once ": AllowOnce, | ||
| 36 | + "s": AllowSession, "session": AllowSession, | ||
| 37 | + "r": RejectOnce, "reject": RejectOnce, | ||
| 38 | + "n": Never, "NEVER": Never, | ||
| 39 | + } | ||
| 40 | + for in, want := range cases { | ||
| 41 | + if got, ok := Parse(in); !ok || got != want { | ||
| 42 | + t.Errorf("Parse(%q) = %v, %v; want %v, true", in, got, ok, want) | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + if _, ok := Parse("maybe"); ok { | ||
| 46 | + t.Error(`Parse("maybe") ok, want refused`) | ||
| 47 | + } | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +func TestOnlyAllowAnswersRun(t *testing.T) { | ||
| 51 | + for a, want := range map[Answer]bool{AllowOnce: true, AllowSession: true, RejectOnce: false, Never: false} { | ||
| 52 | + if a.Allowed() != want { | ||
| 53 | + t.Errorf("%v.Allowed() = %v, want %v", a, a.Allowed(), want) | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +// Once-only answers ask again on the next call; session answers and "never" | ||
| 59 | +// are remembered per tool and stop the questions. | ||
| 60 | +func TestPolicyRemembersSessionAnswersOnly(t *testing.T) { | ||
| 61 | + var p Policy | ||
| 62 | + asked := 0 | ||
| 63 | + answer := func(a Answer) func(context.Context) Answer { | ||
| 64 | + return func(context.Context) Answer { asked++; return a } | ||
| 65 | + } | ||
| 66 | + ctx := context.Background() | ||
| 67 | + | ||
| 68 | + if !p.Decide(ctx, "bash", answer(AllowOnce)) || p.Decide(ctx, "bash", answer(RejectOnce)) { | ||
| 69 | + t.Fatal("once answers not applied as given") | ||
| 70 | + } | ||
| 71 | + if asked != 2 { | ||
| 72 | + t.Fatalf("asked %d times for two once answers, want 2", asked) | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + if !p.Decide(ctx, "bash", answer(AllowSession)) || !p.Decide(ctx, "bash", answer(RejectOnce)) { | ||
| 76 | + t.Error("allow for the session not remembered") | ||
| 77 | + } | ||
| 78 | + if p.Decide(ctx, "edit_file", answer(Never)) || p.Decide(ctx, "edit_file", answer(AllowOnce)) { | ||
| 79 | + t.Error("never not remembered") | ||
| 80 | + } | ||
| 81 | + if asked != 4 { | ||
| 82 | + t.Errorf("asked %d times, want 4 (remembered answers do not ask)", asked) | ||
| 83 | + } | ||
| 84 | + if allowed, known := p.Remembered("write_file"); known || allowed { | ||
| 85 | + t.Error("a tool never answered for is remembered") | ||
| 86 | + } | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +func TestGateRunsWithoutAskingWhenYoloIsOn(t *testing.T) { | ||
| 90 | + withYolo(t, true) | ||
| 91 | + s := &askingSink{} | ||
| 92 | + if !Gate(context.Background(), s, ui.ToolEvent{Tool: "bash"}) || s.asked != 0 { | ||
| 93 | + t.Errorf("yolo on: allowed=false or asked %d times", s.asked) | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +func TestGateAsksTheSinkWhenYoloIsOff(t *testing.T) { | ||
| 98 | + withYolo(t, false) | ||
| 99 | + s := &askingSink{allow: false} | ||
| 100 | + if Gate(context.Background(), s, ui.ToolEvent{Tool: "bash"}) || s.asked != 1 { | ||
| 101 | + t.Errorf("yolo off: the sink's refusal was not applied (asked %d)", s.asked) | ||
| 102 | + } | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +// The line mode's tools have no sink: Terminal asks the installed prompter, | ||
| 106 | +// and with none (mm -p) refuses rather than running unconfirmed. | ||
| 107 | +func TestTerminalUsesThePrompterOrRefuses(t *testing.T) { | ||
| 108 | + withYolo(t, false) | ||
| 109 | + ev := ui.ToolEvent{Tool: "bash"} | ||
| 110 | + if Terminal(context.Background(), ev) { | ||
| 111 | + t.Error("no prompter installed: the call ran") | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + asked := 0 | ||
| 115 | + restore := SetPrompter(func(context.Context, ui.ToolEvent) bool { asked++; return true }) | ||
| 116 | + if !Terminal(context.Background(), ev) || asked != 1 { | ||
| 117 | + t.Errorf("prompter not consulted (asked %d)", asked) | ||
| 118 | + } | ||
| 119 | + restore() | ||
| 120 | + if Terminal(context.Background(), ev) || asked != 1 { | ||
| 121 | + t.Error("restore did not remove the prompter") | ||
| 122 | + } | ||
| 123 | +} | ||
| 124 | + | ||
| 125 | +func TestTerminalRunsWithoutAskingWhenYoloIsOn(t *testing.T) { | ||
| 126 | + withYolo(t, true) | ||
| 127 | + if !Terminal(context.Background(), ui.ToolEvent{Tool: "bash"}) { | ||
| 128 | + t.Error("yolo on with no prompter: the call was refused") | ||
| 129 | + } | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +// Prompt is for the MCP tools marked "ask": it asks whatever yolo says. | ||
| 133 | +func TestPromptAsksEvenWithYoloOn(t *testing.T) { | ||
| 134 | + withYolo(t, true) | ||
| 135 | + asked := 0 | ||
| 136 | + defer SetPrompter(func(context.Context, ui.ToolEvent) bool { asked++; return false })() | ||
| 137 | + if Prompt(context.Background(), ui.ToolEvent{Tool: "ctx7/query"}) || asked != 1 { | ||
| 138 | + t.Errorf("Prompt with yolo on: asked %d, want 1 and the refusal applied", asked) | ||
| 139 | + } | ||
| 140 | +} | ||
added
internal/consent/example_test.go +30 -0 | new file mode 100644 | ||
| @@ -0,0 +1,30 @@ | ||
| 1 | +package consent_test | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "fmt" | |
| 6 | + | |
| 7 | + "mm/internal/consent" | |
| 8 | +) | |
| 9 | + | |
| 10 | +// A session's policy asks only when nothing is remembered for the tool: | |
| 11 | +// "never" refuses this call and every later one without a question. | |
| 12 | +func ExamplePolicy_Decide() { | |
| 13 | + var p consent.Policy | |
| 14 | + ask := func(context.Context) consent.Answer { | |
| 15 | + fmt.Println("asking the user…") | |
| 16 | + return consent.Never | |
| 17 | + } | |
| 18 | + fmt.Println(p.Decide(context.Background(), "bash", ask)) | |
| 19 | + fmt.Println(p.Decide(context.Background(), "bash", ask)) | |
| 20 | + // Output: | |
| 21 | + // asking the user… | |
| 22 | + // false | |
| 23 | + // false | |
| 24 | +} | |
| 25 | + | |
| 26 | +func ExampleParse() { | |
| 27 | + a, ok := consent.Parse("s") | |
| 28 | + fmt.Println(a == consent.AllowSession, ok) | |
| 29 | + // Output: true true | |
| 30 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | +package consent_test | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "fmt" | ||
| 6 | + | ||
| 7 | + "mm/internal/consent" | ||
| 8 | +) | ||
| 9 | + | ||
| 10 | +// A session's policy asks only when nothing is remembered for the tool: | ||
| 11 | +// "never" refuses this call and every later one without a question. | ||
| 12 | +func ExamplePolicy_Decide() { | ||
| 13 | + var p consent.Policy | ||
| 14 | + ask := func(context.Context) consent.Answer { | ||
| 15 | + fmt.Println("asking the user…") | ||
| 16 | + return consent.Never | ||
| 17 | + } | ||
| 18 | + fmt.Println(p.Decide(context.Background(), "bash", ask)) | ||
| 19 | + fmt.Println(p.Decide(context.Background(), "bash", ask)) | ||
| 20 | + // Output: | ||
| 21 | + // asking the user… | ||
| 22 | + // false | ||
| 23 | + // false | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +func ExampleParse() { | ||
| 27 | + a, ok := consent.Parse("s") | ||
| 28 | + fmt.Println(a == consent.AllowSession, ok) | ||
| 29 | + // Output: true true | ||
| 30 | +} | ||
added
internal/mcp/consent_test.go +31 -0 | new file mode 100644 | ||
| @@ -0,0 +1,31 @@ | ||
| 1 | +package mcp | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "testing" | |
| 5 | + | |
| 6 | + "mm/internal/config" | |
| 7 | +) | |
| 8 | + | |
| 9 | +// "ask" asks whatever yolo says; "auto" asks only when yolo is off — the | |
| 10 | +// agent's yolo: false covers every tool, MCP included. | |
| 11 | +func TestMustAskFollowsPermissionsAndYolo(t *testing.T) { | |
| 12 | + old := config.Cfg.Yolo | |
| 13 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | |
| 14 | + | |
| 15 | + cases := []struct { | |
| 16 | + yolo bool | |
| 17 | + permissions string | |
| 18 | + want bool | |
| 19 | + }{ | |
| 20 | + {true, config.PermissionsAsk, true}, | |
| 21 | + {true, config.PermissionsAuto, false}, | |
| 22 | + {false, config.PermissionsAsk, true}, | |
| 23 | + {false, config.PermissionsAuto, true}, | |
| 24 | + } | |
| 25 | + for _, c := range cases { | |
| 26 | + config.Cfg.Yolo = c.yolo | |
| 27 | + if got := mustAsk(c.permissions); got != c.want { | |
| 28 | + t.Errorf("yolo %v, permissions %q: mustAsk = %v, want %v", c.yolo, c.permissions, got, c.want) | |
| 29 | + } | |
| 30 | + } | |
| 31 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | +package mcp | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "testing" | ||
| 5 | + | ||
| 6 | + "mm/internal/config" | ||
| 7 | +) | ||
| 8 | + | ||
| 9 | +// "ask" asks whatever yolo says; "auto" asks only when yolo is off — the | ||
| 10 | +// agent's yolo: false covers every tool, MCP included. | ||
| 11 | +func TestMustAskFollowsPermissionsAndYolo(t *testing.T) { | ||
| 12 | + old := config.Cfg.Yolo | ||
| 13 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | ||
| 14 | + | ||
| 15 | + cases := []struct { | ||
| 16 | + yolo bool | ||
| 17 | + permissions string | ||
| 18 | + want bool | ||
| 19 | + }{ | ||
| 20 | + {true, config.PermissionsAsk, true}, | ||
| 21 | + {true, config.PermissionsAuto, false}, | ||
| 22 | + {false, config.PermissionsAsk, true}, | ||
| 23 | + {false, config.PermissionsAuto, true}, | ||
| 24 | + } | ||
| 25 | + for _, c := range cases { | ||
| 26 | + config.Cfg.Yolo = c.yolo | ||
| 27 | + if got := mustAsk(c.permissions); got != c.want { | ||
| 28 | + t.Errorf("yolo %v, permissions %q: mustAsk = %v, want %v", c.yolo, c.permissions, got, c.want) | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | +} | ||
modified
internal/mcp/tool_wrapper.go +57 -53 | @@ -6,6 +6,7 @@ import ( | ||
| 6 | 6 | "strings" |
| 7 | 7 | |
| 8 | 8 | "mm/internal/config" |
| 9 | + "mm/internal/consent" | |
| 9 | 10 | "mm/internal/detector" |
| 10 | 11 | "mm/internal/ui" |
| 11 | 12 | |
| @@ -43,15 +44,15 @@ func IsBuiltinTool(name string) bool { | ||
| 43 | 44 | func SanitizeToolName(serverName, toolName string) (string, error) { |
| 44 | 45 | // Sanitize the tool name: replace special characters |
| 45 | 46 | sanitized := sanitizeName(toolName) |
| 46 | - | |
| 47 | + | |
| 47 | 48 | // Prefix with server name |
| 48 | 49 | fullName := serverName + "_" + sanitized |
| 49 | - | |
| 50 | + | |
| 50 | 51 | // Check for collision with built-in tools |
| 51 | 52 | if IsBuiltinTool(fullName) { |
| 52 | 53 | return "", fmt.Errorf("tool name %q collides with built-in tool", fullName) |
| 53 | 54 | } |
| 54 | - | |
| 55 | + | |
| 55 | 56 | return fullName, nil |
| 56 | 57 | } |
| 57 | 58 | |
| @@ -66,18 +67,18 @@ func sanitizeName(name string) string { | ||
| 66 | 67 | result.WriteRune('_') |
| 67 | 68 | } |
| 68 | 69 | } |
| 69 | - | |
| 70 | + | |
| 70 | 71 | // Remove leading/trailing underscores and collapse multiple underscores |
| 71 | 72 | s := result.String() |
| 72 | 73 | s = strings.Trim(s, "_") |
| 73 | 74 | for strings.Contains(s, "__") { |
| 74 | 75 | s = strings.ReplaceAll(s, "__", "_") |
| 75 | 76 | } |
| 76 | - | |
| 77 | + | |
| 77 | 78 | if s == "" { |
| 78 | 79 | return "tool" |
| 79 | 80 | } |
| 80 | - | |
| 81 | + | |
| 81 | 82 | return s |
| 82 | 83 | } |
| 83 | 84 | |
| @@ -85,10 +86,10 @@ func sanitizeName(name string) string { | ||
| 85 | 86 | // Returns a list of collision errors. |
| 86 | 87 | func CheckCollisions(clients map[string]*ServerClient) []string { |
| 87 | 88 | var collisions []string |
| 88 | - | |
| 89 | + | |
| 89 | 90 | // Collect all tool names |
| 90 | 91 | allNames := make(map[string]string) // toolName -> serverName |
| 91 | - | |
| 92 | + | |
| 92 | 93 | for serverName, sc := range clients { |
| 93 | 94 | for _, tool := range sc.ToolList { |
| 94 | 95 | fullName, err := SanitizeToolName(serverName, tool.Name) |
| @@ -96,7 +97,7 @@ func CheckCollisions(clients map[string]*ServerClient) []string { | ||
| 96 | 97 | collisions = append(collisions, err.Error()) |
| 97 | 98 | continue |
| 98 | 99 | } |
| 99 | - | |
| 100 | + | |
| 100 | 101 | if existingServer, exists := allNames[fullName]; exists { |
| 101 | 102 | collisions = append(collisions, |
| 102 | 103 | fmt.Sprintf("tool name collision: %q from servers %s and %s", |
| @@ -106,7 +107,7 @@ func CheckCollisions(clients map[string]*ServerClient) []string { | ||
| 106 | 107 | } |
| 107 | 108 | } |
| 108 | 109 | } |
| 109 | - | |
| 110 | + | |
| 110 | 111 | return collisions |
| 111 | 112 | } |
| 112 | 113 | |
| @@ -134,12 +135,12 @@ func WrapToolsWithNames( | ||
| 134 | 135 | var toolRefs []ai.ToolRef |
| 135 | 136 | var toolNames []string |
| 136 | 137 | var warnings []string |
| 137 | - | |
| 138 | + | |
| 138 | 139 | // Check for collisions first |
| 139 | 140 | if collisions := CheckCollisions(clients); len(collisions) > 0 { |
| 140 | 141 | return nil, nil, collisions, fmt.Errorf("name collisions detected") |
| 141 | 142 | } |
| 142 | - | |
| 143 | + | |
| 143 | 144 | // Wrap each tool |
| 144 | 145 | for serverName, sc := range clients { |
| 145 | 146 | for _, mcpTool := range sc.ToolList { |
| @@ -149,14 +150,14 @@ func WrapToolsWithNames( | ||
| 149 | 150 | warnings = append(warnings, err.Error()) |
| 150 | 151 | continue |
| 151 | 152 | } |
| 152 | - | |
| 153 | + | |
| 153 | 154 | // Create wrapped tool |
| 154 | 155 | wrapped := wrapTool(g, d, sc, mcpTool, fullName, defaultPermissions) |
| 155 | 156 | toolRefs = append(toolRefs, wrapped) |
| 156 | 157 | toolNames = append(toolNames, fullName) |
| 157 | 158 | } |
| 158 | 159 | } |
| 159 | - | |
| 160 | + | |
| 160 | 161 | return toolRefs, toolNames, warnings, nil |
| 161 | 162 | } |
| 162 | 163 | |
| @@ -177,16 +178,16 @@ func wrapTool( | ||
| 177 | 178 | if permissions == "" { |
| 178 | 179 | permissions = "ask" // Default for MCP tools |
| 179 | 180 | } |
| 180 | - | |
| 181 | + | |
| 181 | 182 | // Create a description that includes the server, original tool info, and required parameters |
| 182 | 183 | description := fmt.Sprintf("MCP tool from server %s: %s", sc.Server.Name, mcpTool.Description) |
| 183 | - | |
| 184 | + | |
| 184 | 185 | // Append required parameters info if any |
| 185 | 186 | if len(mcpTool.InputSchema.Required) > 0 { |
| 186 | 187 | params := strings.Join(mcpTool.InputSchema.Required, ", ") |
| 187 | 188 | description += fmt.Sprintf("\nRequired parameters: %s", params) |
| 188 | 189 | } |
| 189 | - | |
| 190 | + | |
| 190 | 191 | // Add usage hints for specific well-known tools |
| 191 | 192 | if sc.Server.Name == "context7" { |
| 192 | 193 | switch mcpTool.Name { |
| @@ -196,19 +197,25 @@ func wrapTool( | ||
| 196 | 197 | description += "\n\nUsage: Call resolve-library-id first to get libraryId. Example: query-docs(libraryId=\"/vercel/next.js\", query=\"useEffect examples\")" |
| 197 | 198 | } |
| 198 | 199 | } |
| 199 | - | |
| 200 | + | |
| 200 | 201 | return genkit.DefineTool(g, fullName, description, |
| 201 | 202 | func(tctx *ai.ToolContext, in map[string]any) (string, error) { |
| 202 | 203 | // Event path for ACP/window |
| 203 | 204 | if s := ui.ActiveSink(); s != nil { |
| 204 | 205 | return toolEvented(tctx, s, d, sc, mcpTool, fullName, in, permissions) |
| 205 | 206 | } |
| 206 | - | |
| 207 | + | |
| 207 | 208 | // Terminal path |
| 208 | 209 | return toolTerminal(tctx, d, sc, mcpTool, fullName, in, permissions) |
| 209 | 210 | }) |
| 210 | 211 | } |
| 211 | 212 | |
| 213 | +// mustAsk reports whether a call of an MCP tool with these permissions waits | |
| 214 | +// for the user: always for "ask", and for "auto" too when yolo is off. | |
| 215 | +func mustAsk(permissions string) bool { | |
| 216 | + return permissions == config.PermissionsAsk || consent.Required() | |
| 217 | +} | |
| 218 | + | |
| 212 | 219 | // toolEvented handles the tool call for event-consuming front ends (ACP, window). |
| 213 | 220 | func toolEvented( |
| 214 | 221 | ctx context.Context, |
| @@ -222,7 +229,7 @@ func toolEvented( | ||
| 222 | 229 | ) (string, error) { |
| 223 | 230 | // Determine tool kind - always "execute" for MCP tools (we don't trust server hints) |
| 224 | 231 | kind := "execute" |
| 225 | - | |
| 232 | + | |
| 226 | 233 | // Create tool event |
| 227 | 234 | ev := ui.ToolEvent{ |
| 228 | 235 | ID: ui.NextCallID(), |
| @@ -232,35 +239,29 @@ func toolEvented( | ||
| 232 | 239 | Input: in, |
| 233 | 240 | Path: "", // MCP tools don't have a file path |
| 234 | 241 | } |
| 235 | - | |
| 242 | + | |
| 236 | 243 | s.ToolStart(ev) |
| 237 | - | |
| 238 | - // Check permissions if needed | |
| 239 | - if permissions == config.PermissionsAsk { | |
| 240 | - if !s.Allow(ctx, ev) { | |
| 241 | - out := fmt.Sprintf("[%s rejected by user]", fullName) | |
| 242 | - s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | |
| 243 | - return record(d, fullName, mcpTool.Name, out) | |
| 244 | - } | |
| 245 | - } | |
| 246 | - | |
| 247 | - // If auto permissions, no dialog needed | |
| 248 | - if permissions == config.PermissionsAuto { | |
| 249 | - s.ToolRunning(ev.ID) | |
| 244 | + | |
| 245 | + // "ask" asks whatever yolo says; "auto" asks only when yolo is off. | |
| 246 | + if mustAsk(permissions) && !s.Allow(ctx, ev) { | |
| 247 | + out := fmt.Sprintf("[%s rejected by user]", fullName) | |
| 248 | + s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | |
| 249 | + return record(d, fullName, mcpTool.Name, out) | |
| 250 | 250 | } |
| 251 | - | |
| 251 | + s.ToolRunning(ev.ID) | |
| 252 | + | |
| 252 | 253 | // Call the MCP tool |
| 253 | 254 | out, err := callMCPTool(ctx, sc, mcpTool, in) |
| 254 | - | |
| 255 | + | |
| 255 | 256 | failed := err != nil |
| 256 | 257 | if err != nil { |
| 257 | 258 | out = fmt.Sprintf("[mcp error: %v]", err) |
| 258 | 259 | } |
| 259 | - | |
| 260 | + | |
| 260 | 261 | // Truncate and return |
| 261 | 262 | truncated := truncate(out) |
| 262 | 263 | s.ToolEnd(ev.ID, ui.ToolResult{Output: truncated, Failed: failed}) |
| 263 | - | |
| 264 | + | |
| 264 | 265 | return record(d, fullName, mcpTool.Name, truncated) |
| 265 | 266 | } |
| 266 | 267 | |
| @@ -274,17 +275,20 @@ func toolTerminal( | ||
| 274 | 275 | in map[string]any, |
| 275 | 276 | permissions string, |
| 276 | 277 | ) (string, error) { |
| 277 | - // For terminal mode, we don't have a permission dialog | |
| 278 | - // If permissions is "ask", we need to handle it differently | |
| 279 | - // For now, we'll just run it (since terminal mode doesn't have a UI for permission dialogs) | |
| 280 | - | |
| 278 | + // The line mode asks through the REPL's prompt; in -p nobody can answer | |
| 279 | + // and an asking call is refused (consent.Prompt). | |
| 280 | + ev := ui.ToolEvent{Tool: fullName, Title: fmt.Sprintf("%s: %s", sc.Server.Name, mcpTool.Name), Input: in} | |
| 281 | + if mustAsk(permissions) && !consent.Prompt(ctx, ev) { | |
| 282 | + return record(d, fullName, mcpTool.Name, fmt.Sprintf("[%s rejected by user]", fullName)) | |
| 283 | + } | |
| 284 | + | |
| 281 | 285 | // Call the MCP tool |
| 282 | 286 | out, err := callMCPTool(ctx, sc, mcpTool, in) |
| 283 | - | |
| 287 | + | |
| 284 | 288 | if err != nil { |
| 285 | 289 | out = fmt.Sprintf("[mcp error: %v]", err) |
| 286 | 290 | } |
| 287 | - | |
| 291 | + | |
| 288 | 292 | // Truncate and return |
| 289 | 293 | return record(d, fullName, mcpTool.Name, truncate(out)) |
| 290 | 294 | } |
| @@ -303,20 +307,20 @@ func callMCPTool( | ||
| 303 | 307 | return "", fmt.Errorf("missing required parameters: %s", strings.Join(missing, ", ")) |
| 304 | 308 | } |
| 305 | 309 | } |
| 306 | - | |
| 310 | + | |
| 307 | 311 | // Apply timeout from server config |
| 308 | 312 | if sc.Server.Timeout > 0 { |
| 309 | 313 | var cancel context.CancelFunc |
| 310 | 314 | ctx, cancel = context.WithTimeout(ctx, sc.Server.Timeout) |
| 311 | 315 | defer cancel() |
| 312 | 316 | } |
| 313 | - | |
| 317 | + | |
| 314 | 318 | // Call the tool |
| 315 | 319 | result, err := sc.Client.CallTool(ctx, mcpTool.Name, in) |
| 316 | 320 | if err != nil { |
| 317 | 321 | return "", err |
| 318 | 322 | } |
| 319 | - | |
| 323 | + | |
| 320 | 324 | // Extract text content |
| 321 | 325 | var output strings.Builder |
| 322 | 326 | for _, content := range result.Content { |
| @@ -327,7 +331,7 @@ func callMCPTool( | ||
| 327 | 331 | output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type)) |
| 328 | 332 | } |
| 329 | 333 | } |
| 330 | - | |
| 334 | + | |
| 331 | 335 | return output.String(), nil |
| 332 | 336 | } |
| 333 | 337 | |
| @@ -350,18 +354,18 @@ func truncate(s string) string { | ||
| 350 | 354 | if len(r) <= max { |
| 351 | 355 | return s |
| 352 | 356 | } |
| 353 | - | |
| 357 | + | |
| 354 | 358 | // Keep 2000 characters at the end, but never more than half the budget |
| 355 | 359 | tailLen := 2000 |
| 356 | 360 | if max/2 < tailLen { |
| 357 | 361 | tailLen = max / 2 |
| 358 | 362 | } |
| 359 | 363 | headLen := max - tailLen |
| 360 | - | |
| 364 | + | |
| 361 | 365 | head := string(r[:headLen]) |
| 362 | 366 | tail := string(r[len(r)-tailLen:]) |
| 363 | 367 | omitted := len(r) - headLen - tailLen |
| 364 | - | |
| 368 | + | |
| 365 | 369 | return fmt.Sprintf("%s\n\n[... %d characters truncated ...]\n\n%s", head, omitted, tail) |
| 366 | 370 | } |
| 367 | 371 | |
| @@ -374,12 +378,12 @@ func record(d *detector.LoopDetector, toolName, originalToolName, output string) | ||
| 374 | 378 | if !d.Record(detector.Action{ToolName: toolName, Input: input, Output: output}) { |
| 375 | 379 | return output, nil |
| 376 | 380 | } |
| 377 | - | |
| 381 | + | |
| 378 | 382 | // Check if we're stuck in a loop |
| 379 | 383 | if d.Stuck() { |
| 380 | 384 | return "", d.StuckError(toolName) |
| 381 | 385 | } |
| 382 | - | |
| 386 | + | |
| 383 | 387 | // Append loop warning if needed |
| 384 | 388 | return fmt.Sprintf("%s\n\n%s", output, d.LoopError()), nil |
| 385 | 389 | } |
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | "strings" | 6 | "strings" |
| 7 | 7 | ||
| 8 | "mm/internal/config" | 8 | "mm/internal/config" |
| 9 | + "mm/internal/consent" | ||
| 9 | "mm/internal/detector" | 10 | "mm/internal/detector" |
| 10 | "mm/internal/ui" | 11 | "mm/internal/ui" |
| 11 | 12 | ||
| @@ -43,15 +44,15 @@ func IsBuiltinTool(name string) bool { | |||
| 43 | func SanitizeToolName(serverName, toolName string) (string, error) { | 44 | func SanitizeToolName(serverName, toolName string) (string, error) { |
| 44 | // Sanitize the tool name: replace special characters | 45 | // Sanitize the tool name: replace special characters |
| 45 | sanitized := sanitizeName(toolName) | 46 | sanitized := sanitizeName(toolName) |
| 46 | - | 47 | + |
| 47 | // Prefix with server name | 48 | // Prefix with server name |
| 48 | fullName := serverName + "_" + sanitized | 49 | fullName := serverName + "_" + sanitized |
| 49 | - | 50 | + |
| 50 | // Check for collision with built-in tools | 51 | // Check for collision with built-in tools |
| 51 | if IsBuiltinTool(fullName) { | 52 | if IsBuiltinTool(fullName) { |
| 52 | return "", fmt.Errorf("tool name %q collides with built-in tool", fullName) | 53 | return "", fmt.Errorf("tool name %q collides with built-in tool", fullName) |
| 53 | } | 54 | } |
| 54 | - | 55 | + |
| 55 | return fullName, nil | 56 | return fullName, nil |
| 56 | } | 57 | } |
| 57 | 58 | ||
| @@ -66,18 +67,18 @@ func sanitizeName(name string) string { | |||
| 66 | result.WriteRune('_') | 67 | result.WriteRune('_') |
| 67 | } | 68 | } |
| 68 | } | 69 | } |
| 69 | - | 70 | + |
| 70 | // Remove leading/trailing underscores and collapse multiple underscores | 71 | // Remove leading/trailing underscores and collapse multiple underscores |
| 71 | s := result.String() | 72 | s := result.String() |
| 72 | s = strings.Trim(s, "_") | 73 | s = strings.Trim(s, "_") |
| 73 | for strings.Contains(s, "__") { | 74 | for strings.Contains(s, "__") { |
| 74 | s = strings.ReplaceAll(s, "__", "_") | 75 | s = strings.ReplaceAll(s, "__", "_") |
| 75 | } | 76 | } |
| 76 | - | 77 | + |
| 77 | if s == "" { | 78 | if s == "" { |
| 78 | return "tool" | 79 | return "tool" |
| 79 | } | 80 | } |
| 80 | - | 81 | + |
| 81 | return s | 82 | return s |
| 82 | } | 83 | } |
| 83 | 84 | ||
| @@ -85,10 +86,10 @@ func sanitizeName(name string) string { | |||
| 85 | // Returns a list of collision errors. | 86 | // Returns a list of collision errors. |
| 86 | func CheckCollisions(clients map[string]*ServerClient) []string { | 87 | func CheckCollisions(clients map[string]*ServerClient) []string { |
| 87 | var collisions []string | 88 | var collisions []string |
| 88 | - | 89 | + |
| 89 | // Collect all tool names | 90 | // Collect all tool names |
| 90 | allNames := make(map[string]string) // toolName -> serverName | 91 | allNames := make(map[string]string) // toolName -> serverName |
| 91 | - | 92 | + |
| 92 | for serverName, sc := range clients { | 93 | for serverName, sc := range clients { |
| 93 | for _, tool := range sc.ToolList { | 94 | for _, tool := range sc.ToolList { |
| 94 | fullName, err := SanitizeToolName(serverName, tool.Name) | 95 | fullName, err := SanitizeToolName(serverName, tool.Name) |
| @@ -96,7 +97,7 @@ func CheckCollisions(clients map[string]*ServerClient) []string { | |||
| 96 | collisions = append(collisions, err.Error()) | 97 | collisions = append(collisions, err.Error()) |
| 97 | continue | 98 | continue |
| 98 | } | 99 | } |
| 99 | - | 100 | + |
| 100 | if existingServer, exists := allNames[fullName]; exists { | 101 | if existingServer, exists := allNames[fullName]; exists { |
| 101 | collisions = append(collisions, | 102 | collisions = append(collisions, |
| 102 | fmt.Sprintf("tool name collision: %q from servers %s and %s", | 103 | fmt.Sprintf("tool name collision: %q from servers %s and %s", |
| @@ -106,7 +107,7 @@ func CheckCollisions(clients map[string]*ServerClient) []string { | |||
| 106 | } | 107 | } |
| 107 | } | 108 | } |
| 108 | } | 109 | } |
| 109 | - | 110 | + |
| 110 | return collisions | 111 | return collisions |
| 111 | } | 112 | } |
| 112 | 113 | ||
| @@ -134,12 +135,12 @@ func WrapToolsWithNames( | |||
| 134 | var toolRefs []ai.ToolRef | 135 | var toolRefs []ai.ToolRef |
| 135 | var toolNames []string | 136 | var toolNames []string |
| 136 | var warnings []string | 137 | var warnings []string |
| 137 | - | 138 | + |
| 138 | // Check for collisions first | 139 | // Check for collisions first |
| 139 | if collisions := CheckCollisions(clients); len(collisions) > 0 { | 140 | if collisions := CheckCollisions(clients); len(collisions) > 0 { |
| 140 | return nil, nil, collisions, fmt.Errorf("name collisions detected") | 141 | return nil, nil, collisions, fmt.Errorf("name collisions detected") |
| 141 | } | 142 | } |
| 142 | - | 143 | + |
| 143 | // Wrap each tool | 144 | // Wrap each tool |
| 144 | for serverName, sc := range clients { | 145 | for serverName, sc := range clients { |
| 145 | for _, mcpTool := range sc.ToolList { | 146 | for _, mcpTool := range sc.ToolList { |
| @@ -149,14 +150,14 @@ func WrapToolsWithNames( | |||
| 149 | warnings = append(warnings, err.Error()) | 150 | warnings = append(warnings, err.Error()) |
| 150 | continue | 151 | continue |
| 151 | } | 152 | } |
| 152 | - | 153 | + |
| 153 | // Create wrapped tool | 154 | // Create wrapped tool |
| 154 | wrapped := wrapTool(g, d, sc, mcpTool, fullName, defaultPermissions) | 155 | wrapped := wrapTool(g, d, sc, mcpTool, fullName, defaultPermissions) |
| 155 | toolRefs = append(toolRefs, wrapped) | 156 | toolRefs = append(toolRefs, wrapped) |
| 156 | toolNames = append(toolNames, fullName) | 157 | toolNames = append(toolNames, fullName) |
| 157 | } | 158 | } |
| 158 | } | 159 | } |
| 159 | - | 160 | + |
| 160 | return toolRefs, toolNames, warnings, nil | 161 | return toolRefs, toolNames, warnings, nil |
| 161 | } | 162 | } |
| 162 | 163 | ||
| @@ -177,16 +178,16 @@ func wrapTool( | |||
| 177 | if permissions == "" { | 178 | if permissions == "" { |
| 178 | permissions = "ask" // Default for MCP tools | 179 | permissions = "ask" // Default for MCP tools |
| 179 | } | 180 | } |
| 180 | - | 181 | + |
| 181 | // Create a description that includes the server, original tool info, and required parameters | 182 | // Create a description that includes the server, original tool info, and required parameters |
| 182 | description := fmt.Sprintf("MCP tool from server %s: %s", sc.Server.Name, mcpTool.Description) | 183 | description := fmt.Sprintf("MCP tool from server %s: %s", sc.Server.Name, mcpTool.Description) |
| 183 | - | 184 | + |
| 184 | // Append required parameters info if any | 185 | // Append required parameters info if any |
| 185 | if len(mcpTool.InputSchema.Required) > 0 { | 186 | if len(mcpTool.InputSchema.Required) > 0 { |
| 186 | params := strings.Join(mcpTool.InputSchema.Required, ", ") | 187 | params := strings.Join(mcpTool.InputSchema.Required, ", ") |
| 187 | description += fmt.Sprintf("\nRequired parameters: %s", params) | 188 | description += fmt.Sprintf("\nRequired parameters: %s", params) |
| 188 | } | 189 | } |
| 189 | - | 190 | + |
| 190 | // Add usage hints for specific well-known tools | 191 | // Add usage hints for specific well-known tools |
| 191 | if sc.Server.Name == "context7" { | 192 | if sc.Server.Name == "context7" { |
| 192 | switch mcpTool.Name { | 193 | switch mcpTool.Name { |
| @@ -196,19 +197,25 @@ func wrapTool( | |||
| 196 | description += "\n\nUsage: Call resolve-library-id first to get libraryId. Example: query-docs(libraryId=\"/vercel/next.js\", query=\"useEffect examples\")" | 197 | description += "\n\nUsage: Call resolve-library-id first to get libraryId. Example: query-docs(libraryId=\"/vercel/next.js\", query=\"useEffect examples\")" |
| 197 | } | 198 | } |
| 198 | } | 199 | } |
| 199 | - | 200 | + |
| 200 | return genkit.DefineTool(g, fullName, description, | 201 | return genkit.DefineTool(g, fullName, description, |
| 201 | func(tctx *ai.ToolContext, in map[string]any) (string, error) { | 202 | func(tctx *ai.ToolContext, in map[string]any) (string, error) { |
| 202 | // Event path for ACP/window | 203 | // Event path for ACP/window |
| 203 | if s := ui.ActiveSink(); s != nil { | 204 | if s := ui.ActiveSink(); s != nil { |
| 204 | return toolEvented(tctx, s, d, sc, mcpTool, fullName, in, permissions) | 205 | return toolEvented(tctx, s, d, sc, mcpTool, fullName, in, permissions) |
| 205 | } | 206 | } |
| 206 | - | 207 | + |
| 207 | // Terminal path | 208 | // Terminal path |
| 208 | return toolTerminal(tctx, d, sc, mcpTool, fullName, in, permissions) | 209 | return toolTerminal(tctx, d, sc, mcpTool, fullName, in, permissions) |
| 209 | }) | 210 | }) |
| 210 | } | 211 | } |
| 211 | 212 | ||
| 213 | +// mustAsk reports whether a call of an MCP tool with these permissions waits | ||
| 214 | +// for the user: always for "ask", and for "auto" too when yolo is off. | ||
| 215 | +func mustAsk(permissions string) bool { | ||
| 216 | + return permissions == config.PermissionsAsk || consent.Required() | ||
| 217 | +} | ||
| 218 | + | ||
| 212 | // toolEvented handles the tool call for event-consuming front ends (ACP, window). | 219 | // toolEvented handles the tool call for event-consuming front ends (ACP, window). |
| 213 | func toolEvented( | 220 | func toolEvented( |
| 214 | ctx context.Context, | 221 | ctx context.Context, |
| @@ -222,7 +229,7 @@ func toolEvented( | |||
| 222 | ) (string, error) { | 229 | ) (string, error) { |
| 223 | // Determine tool kind - always "execute" for MCP tools (we don't trust server hints) | 230 | // Determine tool kind - always "execute" for MCP tools (we don't trust server hints) |
| 224 | kind := "execute" | 231 | kind := "execute" |
| 225 | - | 232 | + |
| 226 | // Create tool event | 233 | // Create tool event |
| 227 | ev := ui.ToolEvent{ | 234 | ev := ui.ToolEvent{ |
| 228 | ID: ui.NextCallID(), | 235 | ID: ui.NextCallID(), |
| @@ -232,35 +239,29 @@ func toolEvented( | |||
| 232 | Input: in, | 239 | Input: in, |
| 233 | Path: "", // MCP tools don't have a file path | 240 | Path: "", // MCP tools don't have a file path |
| 234 | } | 241 | } |
| 235 | - | 242 | + |
| 236 | s.ToolStart(ev) | 243 | s.ToolStart(ev) |
| 237 | - | 244 | + |
| 238 | - // Check permissions if needed | 245 | + // "ask" asks whatever yolo says; "auto" asks only when yolo is off. |
| 239 | - if permissions == config.PermissionsAsk { | 246 | + if mustAsk(permissions) && !s.Allow(ctx, ev) { |
| 240 | - if !s.Allow(ctx, ev) { | 247 | + out := fmt.Sprintf("[%s rejected by user]", fullName) |
| 241 | - out := fmt.Sprintf("[%s rejected by user]", fullName) | 248 | + s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 242 | - s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 249 | + return record(d, fullName, mcpTool.Name, out) |
| 243 | - return record(d, fullName, mcpTool.Name, out) | ||
| 244 | - } | ||
| 245 | - } | ||
| 246 | - | ||
| 247 | - // If auto permissions, no dialog needed | ||
| 248 | - if permissions == config.PermissionsAuto { | ||
| 249 | - s.ToolRunning(ev.ID) | ||
| 250 | } | 250 | } |
| 251 | - | 251 | + s.ToolRunning(ev.ID) |
| 252 | + | ||
| 252 | // Call the MCP tool | 253 | // Call the MCP tool |
| 253 | out, err := callMCPTool(ctx, sc, mcpTool, in) | 254 | out, err := callMCPTool(ctx, sc, mcpTool, in) |
| 254 | - | 255 | + |
| 255 | failed := err != nil | 256 | failed := err != nil |
| 256 | if err != nil { | 257 | if err != nil { |
| 257 | out = fmt.Sprintf("[mcp error: %v]", err) | 258 | out = fmt.Sprintf("[mcp error: %v]", err) |
| 258 | } | 259 | } |
| 259 | - | 260 | + |
| 260 | // Truncate and return | 261 | // Truncate and return |
| 261 | truncated := truncate(out) | 262 | truncated := truncate(out) |
| 262 | s.ToolEnd(ev.ID, ui.ToolResult{Output: truncated, Failed: failed}) | 263 | s.ToolEnd(ev.ID, ui.ToolResult{Output: truncated, Failed: failed}) |
| 263 | - | 264 | + |
| 264 | return record(d, fullName, mcpTool.Name, truncated) | 265 | return record(d, fullName, mcpTool.Name, truncated) |
| 265 | } | 266 | } |
| 266 | 267 | ||
| @@ -274,17 +275,20 @@ func toolTerminal( | |||
| 274 | in map[string]any, | 275 | in map[string]any, |
| 275 | permissions string, | 276 | permissions string, |
| 276 | ) (string, error) { | 277 | ) (string, error) { |
| 277 | - // For terminal mode, we don't have a permission dialog | 278 | + // The line mode asks through the REPL's prompt; in -p nobody can answer |
| 278 | - // If permissions is "ask", we need to handle it differently | 279 | + // and an asking call is refused (consent.Prompt). |
| 279 | - // For now, we'll just run it (since terminal mode doesn't have a UI for permission dialogs) | 280 | + ev := ui.ToolEvent{Tool: fullName, Title: fmt.Sprintf("%s: %s", sc.Server.Name, mcpTool.Name), Input: in} |
| 280 | - | 281 | + if mustAsk(permissions) && !consent.Prompt(ctx, ev) { |
| 282 | + return record(d, fullName, mcpTool.Name, fmt.Sprintf("[%s rejected by user]", fullName)) | ||
| 283 | + } | ||
| 284 | + | ||
| 281 | // Call the MCP tool | 285 | // Call the MCP tool |
| 282 | out, err := callMCPTool(ctx, sc, mcpTool, in) | 286 | out, err := callMCPTool(ctx, sc, mcpTool, in) |
| 283 | - | 287 | + |
| 284 | if err != nil { | 288 | if err != nil { |
| 285 | out = fmt.Sprintf("[mcp error: %v]", err) | 289 | out = fmt.Sprintf("[mcp error: %v]", err) |
| 286 | } | 290 | } |
| 287 | - | 291 | + |
| 288 | // Truncate and return | 292 | // Truncate and return |
| 289 | return record(d, fullName, mcpTool.Name, truncate(out)) | 293 | return record(d, fullName, mcpTool.Name, truncate(out)) |
| 290 | } | 294 | } |
| @@ -303,20 +307,20 @@ func callMCPTool( | |||
| 303 | return "", fmt.Errorf("missing required parameters: %s", strings.Join(missing, ", ")) | 307 | return "", fmt.Errorf("missing required parameters: %s", strings.Join(missing, ", ")) |
| 304 | } | 308 | } |
| 305 | } | 309 | } |
| 306 | - | 310 | + |
| 307 | // Apply timeout from server config | 311 | // Apply timeout from server config |
| 308 | if sc.Server.Timeout > 0 { | 312 | if sc.Server.Timeout > 0 { |
| 309 | var cancel context.CancelFunc | 313 | var cancel context.CancelFunc |
| 310 | ctx, cancel = context.WithTimeout(ctx, sc.Server.Timeout) | 314 | ctx, cancel = context.WithTimeout(ctx, sc.Server.Timeout) |
| 311 | defer cancel() | 315 | defer cancel() |
| 312 | } | 316 | } |
| 313 | - | 317 | + |
| 314 | // Call the tool | 318 | // Call the tool |
| 315 | result, err := sc.Client.CallTool(ctx, mcpTool.Name, in) | 319 | result, err := sc.Client.CallTool(ctx, mcpTool.Name, in) |
| 316 | if err != nil { | 320 | if err != nil { |
| 317 | return "", err | 321 | return "", err |
| 318 | } | 322 | } |
| 319 | - | 323 | + |
| 320 | // Extract text content | 324 | // Extract text content |
| 321 | var output strings.Builder | 325 | var output strings.Builder |
| 322 | for _, content := range result.Content { | 326 | for _, content := range result.Content { |
| @@ -327,7 +331,7 @@ func callMCPTool( | |||
| 327 | output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type)) | 331 | output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type)) |
| 328 | } | 332 | } |
| 329 | } | 333 | } |
| 330 | - | 334 | + |
| 331 | return output.String(), nil | 335 | return output.String(), nil |
| 332 | } | 336 | } |
| 333 | 337 | ||
| @@ -350,18 +354,18 @@ func truncate(s string) string { | |||
| 350 | if len(r) <= max { | 354 | if len(r) <= max { |
| 351 | return s | 355 | return s |
| 352 | } | 356 | } |
| 353 | - | 357 | + |
| 354 | // Keep 2000 characters at the end, but never more than half the budget | 358 | // Keep 2000 characters at the end, but never more than half the budget |
| 355 | tailLen := 2000 | 359 | tailLen := 2000 |
| 356 | if max/2 < tailLen { | 360 | if max/2 < tailLen { |
| 357 | tailLen = max / 2 | 361 | tailLen = max / 2 |
| 358 | } | 362 | } |
| 359 | headLen := max - tailLen | 363 | headLen := max - tailLen |
| 360 | - | 364 | + |
| 361 | head := string(r[:headLen]) | 365 | head := string(r[:headLen]) |
| 362 | tail := string(r[len(r)-tailLen:]) | 366 | tail := string(r[len(r)-tailLen:]) |
| 363 | omitted := len(r) - headLen - tailLen | 367 | omitted := len(r) - headLen - tailLen |
| 364 | - | 368 | + |
| 365 | return fmt.Sprintf("%s\n\n[... %d characters truncated ...]\n\n%s", head, omitted, tail) | 369 | return fmt.Sprintf("%s\n\n[... %d characters truncated ...]\n\n%s", head, omitted, tail) |
| 366 | } | 370 | } |
| 367 | 371 | ||
| @@ -374,12 +378,12 @@ func record(d *detector.LoopDetector, toolName, originalToolName, output string) | |||
| 374 | if !d.Record(detector.Action{ToolName: toolName, Input: input, Output: output}) { | 378 | if !d.Record(detector.Action{ToolName: toolName, Input: input, Output: output}) { |
| 375 | return output, nil | 379 | return output, nil |
| 376 | } | 380 | } |
| 377 | - | 381 | + |
| 378 | // Check if we're stuck in a loop | 382 | // Check if we're stuck in a loop |
| 379 | if d.Stuck() { | 383 | if d.Stuck() { |
| 380 | return "", d.StuckError(toolName) | 384 | return "", d.StuckError(toolName) |
| 381 | } | 385 | } |
| 382 | - | 386 | + |
| 383 | // Append loop warning if needed | 387 | // Append loop warning if needed |
| 384 | return fmt.Sprintf("%s\n\n%s", output, d.LoopError()), nil | 388 | return fmt.Sprintf("%s\n\n%s", output, d.LoopError()), nil |
| 385 | } | 389 | } |
added
internal/tools/consent.go +25 -0 | new file mode 100644 | ||
| @@ -0,0 +1,25 @@ | ||
| 1 | +package tools | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + | |
| 6 | + "mm/internal/consent" | |
| 7 | + "mm/internal/detector" | |
| 8 | + "mm/internal/spinner" | |
| 9 | + "mm/internal/ui" | |
| 10 | +) | |
| 11 | + | |
| 12 | +// confirmed is the line mode's permission check for one call — the event | |
| 13 | +// path uses consent.Gate with its sink instead. With yolo on the call runs; | |
| 14 | +// off, the REPL asks (or, in -p, nobody can and it is refused). | |
| 15 | +func confirmed(ctx context.Context, tool, title string) bool { | |
| 16 | + return consent.Terminal(ctx, ui.ToolEvent{Tool: tool, Title: title}) | |
| 17 | +} | |
| 18 | + | |
| 19 | +// refuse ends a call the user refused in the line mode: the refusal is | |
| 20 | +// echoed like any output, and recorded — a model that keeps retrying what | |
| 21 | +// the user keeps refusing is looping. | |
| 22 | +func refuse(d *detector.LoopDetector, tool, key, out string) (string, error) { | |
| 23 | + spinner.Suspend(func() { echoOutput(out) }) | |
| 24 | + return record(d, tool, key, out) | |
| 25 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | +package tools | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + | ||
| 6 | + "mm/internal/consent" | ||
| 7 | + "mm/internal/detector" | ||
| 8 | + "mm/internal/spinner" | ||
| 9 | + "mm/internal/ui" | ||
| 10 | +) | ||
| 11 | + | ||
| 12 | +// confirmed is the line mode's permission check for one call — the event | ||
| 13 | +// path uses consent.Gate with its sink instead. With yolo on the call runs; | ||
| 14 | +// off, the REPL asks (or, in -p, nobody can and it is refused). | ||
| 15 | +func confirmed(ctx context.Context, tool, title string) bool { | ||
| 16 | + return consent.Terminal(ctx, ui.ToolEvent{Tool: tool, Title: title}) | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +// refuse ends a call the user refused in the line mode: the refusal is | ||
| 20 | +// echoed like any output, and recorded — a model that keeps retrying what | ||
| 21 | +// the user keeps refusing is looping. | ||
| 22 | +func refuse(d *detector.LoopDetector, tool, key, out string) (string, error) { | ||
| 23 | + spinner.Suspend(func() { echoOutput(out) }) | ||
| 24 | + return record(d, tool, key, out) | ||
| 25 | +} | ||
added
internal/tools/consent_test.go +78 -0 | new file mode 100644 | ||
| @@ -0,0 +1,78 @@ | ||
| 1 | +package tools | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "os" | |
| 6 | + "path/filepath" | |
| 7 | + "testing" | |
| 8 | + | |
| 9 | + "mm/internal/config" | |
| 10 | + "mm/internal/consent" | |
| 11 | + "mm/internal/detector" | |
| 12 | + "mm/internal/ui" | |
| 13 | +) | |
| 14 | + | |
| 15 | +// yoloOff turns yolo off for one test: the tools then ask before running. | |
| 16 | +func yoloOff(t *testing.T) { | |
| 17 | + t.Helper() | |
| 18 | + old := config.Cfg.Yolo | |
| 19 | + config.Cfg.Yolo = false | |
| 20 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | |
| 21 | +} | |
| 22 | + | |
| 23 | +// The default — yolo on — runs bash at once: the sink is never asked. | |
| 24 | +func TestBashRunsWithoutAskingInYoloMode(t *testing.T) { | |
| 25 | + sink := &recordingSink{allow: false} | |
| 26 | + out, err := bashEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), bashInput{Command: "echo hi"}) | |
| 27 | + if err != nil || sink.asked != 0 || out != "hi\n" { | |
| 28 | + t.Errorf("yolo on: out %q err %v asked %d, want hi and no question", out, err, sink.asked) | |
| 29 | + } | |
| 30 | +} | |
| 31 | + | |
| 32 | +// yolo off: the sink asks, and its refusal reaches the model as text. | |
| 33 | +func TestBashAsksWhenYoloIsOff(t *testing.T) { | |
| 34 | + yoloOff(t) | |
| 35 | + sink := &recordingSink{allow: false} | |
| 36 | + out, _ := bashEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), bashInput{Command: "echo hi"}) | |
| 37 | + if sink.asked != 1 || out != "[command rejected by the user]" || !sink.ended.Failed { | |
| 38 | + t.Errorf("yolo off: out %q asked %d ended %+v", out, sink.asked, sink.ended) | |
| 39 | + } | |
| 40 | +} | |
| 41 | + | |
| 42 | +func TestWriteFileAsksWhenYoloIsOff(t *testing.T) { | |
| 43 | + yoloOff(t) | |
| 44 | + dir := t.TempDir() | |
| 45 | + sink := &recordingSink{allow: false, dir: dir} | |
| 46 | + ui.SetActive(sink) | |
| 47 | + t.Cleanup(func() { ui.SetActive(nil) }) | |
| 48 | + | |
| 49 | + out, _ := writeFileEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), writeFileInput{Path: "a.txt", Content: "x"}) | |
| 50 | + if out != "[write_file rejected by the user]" || sink.asked != 1 { | |
| 51 | + t.Errorf("out %q asked %d", out, sink.asked) | |
| 52 | + } | |
| 53 | + if _, err := os.Stat(filepath.Join(dir, "a.txt")); !os.IsNotExist(err) { | |
| 54 | + t.Error("a refused write_file wrote the file") | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +// The line mode's tools have no sink: confirmed goes to the REPL's prompter | |
| 59 | +// when yolo is off, and refuses when there is none (mm -p). | |
| 60 | +func TestLineModeToolsAskThePrompter(t *testing.T) { | |
| 61 | + yoloOff(t) | |
| 62 | + if confirmed(context.Background(), "bash", "bash: ls") { | |
| 63 | + t.Error("no prompter: the call was confirmed") | |
| 64 | + } | |
| 65 | + var seen ui.ToolEvent | |
| 66 | + defer consent.SetPrompter(func(_ context.Context, ev ui.ToolEvent) bool { seen = ev; return true })() | |
| 67 | + if !confirmed(context.Background(), "bash", "bash: ls") || seen.Tool != "bash" || seen.Title != "bash: ls" { | |
| 68 | + t.Errorf("prompter saw %+v", seen) | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +func TestRefuseRecordsTheRefusal(t *testing.T) { | |
| 73 | + d := detector.NewLoopDetector(10, 3) | |
| 74 | + out, err := refuse(d, "bash", "rm -rf x", "[command rejected by the user]") | |
| 75 | + if err != nil || out != "[command rejected by the user]" { | |
| 76 | + t.Errorf("refuse = %q, %v", out, err) | |
| 77 | + } | |
| 78 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | +package tools | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "os" | ||
| 6 | + "path/filepath" | ||
| 7 | + "testing" | ||
| 8 | + | ||
| 9 | + "mm/internal/config" | ||
| 10 | + "mm/internal/consent" | ||
| 11 | + "mm/internal/detector" | ||
| 12 | + "mm/internal/ui" | ||
| 13 | +) | ||
| 14 | + | ||
| 15 | +// yoloOff turns yolo off for one test: the tools then ask before running. | ||
| 16 | +func yoloOff(t *testing.T) { | ||
| 17 | + t.Helper() | ||
| 18 | + old := config.Cfg.Yolo | ||
| 19 | + config.Cfg.Yolo = false | ||
| 20 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +// The default — yolo on — runs bash at once: the sink is never asked. | ||
| 24 | +func TestBashRunsWithoutAskingInYoloMode(t *testing.T) { | ||
| 25 | + sink := &recordingSink{allow: false} | ||
| 26 | + out, err := bashEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), bashInput{Command: "echo hi"}) | ||
| 27 | + if err != nil || sink.asked != 0 || out != "hi\n" { | ||
| 28 | + t.Errorf("yolo on: out %q err %v asked %d, want hi and no question", out, err, sink.asked) | ||
| 29 | + } | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +// yolo off: the sink asks, and its refusal reaches the model as text. | ||
| 33 | +func TestBashAsksWhenYoloIsOff(t *testing.T) { | ||
| 34 | + yoloOff(t) | ||
| 35 | + sink := &recordingSink{allow: false} | ||
| 36 | + out, _ := bashEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), bashInput{Command: "echo hi"}) | ||
| 37 | + if sink.asked != 1 || out != "[command rejected by the user]" || !sink.ended.Failed { | ||
| 38 | + t.Errorf("yolo off: out %q asked %d ended %+v", out, sink.asked, sink.ended) | ||
| 39 | + } | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +func TestWriteFileAsksWhenYoloIsOff(t *testing.T) { | ||
| 43 | + yoloOff(t) | ||
| 44 | + dir := t.TempDir() | ||
| 45 | + sink := &recordingSink{allow: false, dir: dir} | ||
| 46 | + ui.SetActive(sink) | ||
| 47 | + t.Cleanup(func() { ui.SetActive(nil) }) | ||
| 48 | + | ||
| 49 | + out, _ := writeFileEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), writeFileInput{Path: "a.txt", Content: "x"}) | ||
| 50 | + if out != "[write_file rejected by the user]" || sink.asked != 1 { | ||
| 51 | + t.Errorf("out %q asked %d", out, sink.asked) | ||
| 52 | + } | ||
| 53 | + if _, err := os.Stat(filepath.Join(dir, "a.txt")); !os.IsNotExist(err) { | ||
| 54 | + t.Error("a refused write_file wrote the file") | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +// The line mode's tools have no sink: confirmed goes to the REPL's prompter | ||
| 59 | +// when yolo is off, and refuses when there is none (mm -p). | ||
| 60 | +func TestLineModeToolsAskThePrompter(t *testing.T) { | ||
| 61 | + yoloOff(t) | ||
| 62 | + if confirmed(context.Background(), "bash", "bash: ls") { | ||
| 63 | + t.Error("no prompter: the call was confirmed") | ||
| 64 | + } | ||
| 65 | + var seen ui.ToolEvent | ||
| 66 | + defer consent.SetPrompter(func(_ context.Context, ev ui.ToolEvent) bool { seen = ev; return true })() | ||
| 67 | + if !confirmed(context.Background(), "bash", "bash: ls") || seen.Tool != "bash" || seen.Title != "bash: ls" { | ||
| 68 | + t.Errorf("prompter saw %+v", seen) | ||
| 69 | + } | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +func TestRefuseRecordsTheRefusal(t *testing.T) { | ||
| 73 | + d := detector.NewLoopDetector(10, 3) | ||
| 74 | + out, err := refuse(d, "bash", "rm -rf x", "[command rejected by the user]") | ||
| 75 | + if err != nil || out != "[command rejected by the user]" { | ||
| 76 | + t.Errorf("refuse = %q, %v", out, err) | ||
| 77 | + } | ||
| 78 | +} | ||
modified
internal/tools/fetchcontent.go +7 -1 | @@ -8,6 +8,7 @@ import ( | ||
| 8 | 8 | "path/filepath" |
| 9 | 9 | "strings" |
| 10 | 10 | |
| 11 | + "mm/internal/consent" | |
| 11 | 12 | "mm/internal/detector" |
| 12 | 13 | "mm/internal/spinner" |
| 13 | 14 | "mm/internal/ui" |
| @@ -66,6 +67,11 @@ Failures come back as "[fetch_content: error N (…) — reason]": 1 = the page | ||
| 66 | 67 | } |
| 67 | 68 | |
| 68 | 69 | announce("🌐 fetch_content: " + where) |
| 70 | + // Saving writes into the project, like write_file; fetching alone | |
| 71 | + // is a read and never asks. | |
| 72 | + if in.Output != "" && !confirmed(tctx, "fetch_content", "fetch_content: "+where) { | |
| 73 | + return refuse(d, "fetch_content", where, "[fetch_content rejected by the user]") | |
| 74 | + } | |
| 69 | 75 | // A page can take seconds to come: same reasoning as bash's |
| 70 | 76 | // "Running" label, a slow site must not look like a hung agent. |
| 71 | 77 | spinner.Start("Fetching") |
| @@ -101,7 +107,7 @@ func (call fetchContentCall) evented(ctx context.Context, s ui.Sink, in fetchCon | ||
| 101 | 107 | ev.Path = absolute(ui.Resolve(in.Output)) |
| 102 | 108 | } |
| 103 | 109 | s.ToolStart(ev) |
| 104 | - if in.Output != "" && !s.Allow(ctx, ev) { | |
| 110 | + if in.Output != "" && !consent.Gate(ctx, s, ev) { | |
| 105 | 111 | out := "[fetch_content rejected by the user]" |
| 106 | 112 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 107 | 113 | return record(d, "fetch_content", where, out) |
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | "path/filepath" | 8 | "path/filepath" |
| 9 | "strings" | 9 | "strings" |
| 10 | 10 | ||
| 11 | + "mm/internal/consent" | ||
| 11 | "mm/internal/detector" | 12 | "mm/internal/detector" |
| 12 | "mm/internal/spinner" | 13 | "mm/internal/spinner" |
| 13 | "mm/internal/ui" | 14 | "mm/internal/ui" |
| @@ -66,6 +67,11 @@ Failures come back as "[fetch_content: error N (…) — reason]": 1 = the page | |||
| 66 | } | 67 | } |
| 67 | 68 | ||
| 68 | announce("🌐 fetch_content: " + where) | 69 | announce("🌐 fetch_content: " + where) |
| 70 | + // Saving writes into the project, like write_file; fetching alone | ||
| 71 | + // is a read and never asks. | ||
| 72 | + if in.Output != "" && !confirmed(tctx, "fetch_content", "fetch_content: "+where) { | ||
| 73 | + return refuse(d, "fetch_content", where, "[fetch_content rejected by the user]") | ||
| 74 | + } | ||
| 69 | // A page can take seconds to come: same reasoning as bash's | 75 | // A page can take seconds to come: same reasoning as bash's |
| 70 | // "Running" label, a slow site must not look like a hung agent. | 76 | // "Running" label, a slow site must not look like a hung agent. |
| 71 | spinner.Start("Fetching") | 77 | spinner.Start("Fetching") |
| @@ -101,7 +107,7 @@ func (call fetchContentCall) evented(ctx context.Context, s ui.Sink, in fetchCon | |||
| 101 | ev.Path = absolute(ui.Resolve(in.Output)) | 107 | ev.Path = absolute(ui.Resolve(in.Output)) |
| 102 | } | 108 | } |
| 103 | s.ToolStart(ev) | 109 | s.ToolStart(ev) |
| 104 | - if in.Output != "" && !s.Allow(ctx, ev) { | 110 | + if in.Output != "" && !consent.Gate(ctx, s, ev) { |
| 105 | out := "[fetch_content rejected by the user]" | 111 | out := "[fetch_content rejected by the user]" |
| 106 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 112 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 107 | return record(d, "fetch_content", where, out) | 113 | return record(d, "fetch_content", where, out) |
modified
internal/tools/fetchcontent_test.go +1 -0 | @@ -155,6 +155,7 @@ func (k *recordingSink) Allow(context.Context, ui.ToolEvent) bool { | ||
| 155 | 155 | } |
| 156 | 156 | |
| 157 | 157 | func TestFetchContentEventedAsksBeforeSaving(t *testing.T) { |
| 158 | + yoloOff(t) | |
| 158 | 159 | dir := t.TempDir() |
| 159 | 160 | sink := &recordingSink{allow: false, dir: dir} |
| 160 | 161 | ui.SetActive(sink) |
| @@ -155,6 +155,7 @@ func (k *recordingSink) Allow(context.Context, ui.ToolEvent) bool { | |||
| 155 | } | 155 | } |
| 156 | 156 | ||
| 157 | func TestFetchContentEventedAsksBeforeSaving(t *testing.T) { | 157 | func TestFetchContentEventedAsksBeforeSaving(t *testing.T) { |
| 158 | + yoloOff(t) | ||
| 158 | dir := t.TempDir() | 159 | dir := t.TempDir() |
| 159 | sink := &recordingSink{allow: false, dir: dir} | 160 | sink := &recordingSink{allow: false, dir: dir} |
| 160 | ui.SetActive(sink) | 161 | ui.SetActive(sink) |
modified
internal/tools/files.go +11 -3 | @@ -7,6 +7,7 @@ import ( | ||
| 7 | 7 | "path/filepath" |
| 8 | 8 | "strings" |
| 9 | 9 | |
| 10 | + "mm/internal/consent" | |
| 10 | 11 | "mm/internal/detector" |
| 11 | 12 | "mm/internal/fileedit" |
| 12 | 13 | "mm/internal/spinner" |
| @@ -122,7 +123,11 @@ Returns a one-line summary and the diff against the previous content. Writing id | ||
| 122 | 123 | if s := ui.ActiveSink(); s != nil { |
| 123 | 124 | return writeFileEvented(tctx, s, d, in) |
| 124 | 125 | } |
| 125 | - announce(fmt.Sprintf("💾 write_file: %s (%d bytes)", in.Path, len(in.Content))) | |
| 126 | + title := fmt.Sprintf("write_file: %s (%d bytes)", in.Path, len(in.Content)) | |
| 127 | + announce("💾 " + title) | |
| 128 | + if !confirmed(tctx, "write_file", title) { | |
| 129 | + return refuse(d, "write_file", in.Path, "[write_file rejected by the user]") | |
| 130 | + } | |
| 126 | 131 | |
| 127 | 132 | r, err := fileedit.Write(in.Path, in.Content) |
| 128 | 133 | if err != nil { |
| @@ -162,6 +167,9 @@ Nothing is written when any edit fails. Returns a one-line summary and the diff | ||
| 162 | 167 | return editFileEvented(tctx, s, d, in, what) |
| 163 | 168 | } |
| 164 | 169 | announce("📝 edit_file: " + what) |
| 170 | + if !confirmed(tctx, "edit_file", "edit_file: "+what) { | |
| 171 | + return refuse(d, "edit_file", editKey(in), "[edit_file rejected by the user]") | |
| 172 | + } | |
| 165 | 173 | |
| 166 | 174 | r, err := fileedit.Replace(in.Path, in.Edits, in.DryRun) |
| 167 | 175 | if err != nil { |
| @@ -240,7 +248,7 @@ func writeFileEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, | ||
| 240 | 248 | Path: absolute(path), |
| 241 | 249 | } |
| 242 | 250 | s.ToolStart(ev) |
| 243 | - if !s.Allow(ctx, ev) { | |
| 251 | + if !consent.Gate(ctx, s, ev) { | |
| 244 | 252 | out := "[write_file rejected by the user]" |
| 245 | 253 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 246 | 254 | return record(d, "write_file", in.Path, out) |
| @@ -277,7 +285,7 @@ func editFileEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, i | ||
| 277 | 285 | Path: absolute(path), |
| 278 | 286 | } |
| 279 | 287 | s.ToolStart(ev) |
| 280 | - if !s.Allow(ctx, ev) { | |
| 288 | + if !consent.Gate(ctx, s, ev) { | |
| 281 | 289 | out := "[edit_file rejected by the user]" |
| 282 | 290 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 283 | 291 | return record(d, "edit_file", editKey(in), out) |
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | "path/filepath" | 7 | "path/filepath" |
| 8 | "strings" | 8 | "strings" |
| 9 | 9 | ||
| 10 | + "mm/internal/consent" | ||
| 10 | "mm/internal/detector" | 11 | "mm/internal/detector" |
| 11 | "mm/internal/fileedit" | 12 | "mm/internal/fileedit" |
| 12 | "mm/internal/spinner" | 13 | "mm/internal/spinner" |
| @@ -122,7 +123,11 @@ Returns a one-line summary and the diff against the previous content. Writing id | |||
| 122 | if s := ui.ActiveSink(); s != nil { | 123 | if s := ui.ActiveSink(); s != nil { |
| 123 | return writeFileEvented(tctx, s, d, in) | 124 | return writeFileEvented(tctx, s, d, in) |
| 124 | } | 125 | } |
| 125 | - announce(fmt.Sprintf("💾 write_file: %s (%d bytes)", in.Path, len(in.Content))) | 126 | + title := fmt.Sprintf("write_file: %s (%d bytes)", in.Path, len(in.Content)) |
| 127 | + announce("💾 " + title) | ||
| 128 | + if !confirmed(tctx, "write_file", title) { | ||
| 129 | + return refuse(d, "write_file", in.Path, "[write_file rejected by the user]") | ||
| 130 | + } | ||
| 126 | 131 | ||
| 127 | r, err := fileedit.Write(in.Path, in.Content) | 132 | r, err := fileedit.Write(in.Path, in.Content) |
| 128 | if err != nil { | 133 | if err != nil { |
| @@ -162,6 +167,9 @@ Nothing is written when any edit fails. Returns a one-line summary and the diff | |||
| 162 | return editFileEvented(tctx, s, d, in, what) | 167 | return editFileEvented(tctx, s, d, in, what) |
| 163 | } | 168 | } |
| 164 | announce("📝 edit_file: " + what) | 169 | announce("📝 edit_file: " + what) |
| 170 | + if !confirmed(tctx, "edit_file", "edit_file: "+what) { | ||
| 171 | + return refuse(d, "edit_file", editKey(in), "[edit_file rejected by the user]") | ||
| 172 | + } | ||
| 165 | 173 | ||
| 166 | r, err := fileedit.Replace(in.Path, in.Edits, in.DryRun) | 174 | r, err := fileedit.Replace(in.Path, in.Edits, in.DryRun) |
| 167 | if err != nil { | 175 | if err != nil { |
| @@ -240,7 +248,7 @@ func writeFileEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, | |||
| 240 | Path: absolute(path), | 248 | Path: absolute(path), |
| 241 | } | 249 | } |
| 242 | s.ToolStart(ev) | 250 | s.ToolStart(ev) |
| 243 | - if !s.Allow(ctx, ev) { | 251 | + if !consent.Gate(ctx, s, ev) { |
| 244 | out := "[write_file rejected by the user]" | 252 | out := "[write_file rejected by the user]" |
| 245 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 253 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 246 | return record(d, "write_file", in.Path, out) | 254 | return record(d, "write_file", in.Path, out) |
| @@ -277,7 +285,7 @@ func editFileEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, i | |||
| 277 | Path: absolute(path), | 285 | Path: absolute(path), |
| 278 | } | 286 | } |
| 279 | s.ToolStart(ev) | 287 | s.ToolStart(ev) |
| 280 | - if !s.Allow(ctx, ev) { | 288 | + if !consent.Gate(ctx, s, ev) { |
| 281 | out := "[edit_file rejected by the user]" | 289 | out := "[edit_file rejected by the user]" |
| 282 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 290 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 283 | return record(d, "edit_file", editKey(in), out) | 291 | return record(d, "edit_file", editKey(in), out) |
modified
internal/tools/lsp.go +5 -1 | @@ -8,6 +8,7 @@ import ( | ||
| 8 | 8 | "unicode" |
| 9 | 9 | |
| 10 | 10 | "mm/internal/config" |
| 11 | + "mm/internal/consent" | |
| 11 | 12 | "mm/internal/detector" |
| 12 | 13 | "mm/internal/lsp" |
| 13 | 14 | "mm/internal/spinner" |
| @@ -79,6 +80,9 @@ Use it before editing a symbol used in several files (references), to find where | ||
| 79 | 80 | return lspEvented(tctx, s, d, client, in) |
| 80 | 81 | } |
| 81 | 82 | announce("🔎 lsp: " + what) |
| 83 | + if in.Action == lspRename && !confirmed(tctx, "lsp", "lsp: "+what) { | |
| 84 | + return refuse(d, "lsp", what, "[rename rejected by the user]") | |
| 85 | + } | |
| 82 | 86 | spinner.Start("Asking the language server") |
| 83 | 87 | out, _ := runLSP(tctx, client, in) |
| 84 | 88 | spinner.Suspend(func() { echoOutput(out) }) |
| @@ -281,7 +285,7 @@ func lspEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, client | ||
| 281 | 285 | Path: absolute(ui.Resolve(in.Path)), |
| 282 | 286 | } |
| 283 | 287 | s.ToolStart(ev) |
| 284 | - if in.Action == lspRename && !s.Allow(ctx, ev) { | |
| 288 | + if in.Action == lspRename && !consent.Gate(ctx, s, ev) { | |
| 285 | 289 | out := "[rename rejected by the user]" |
| 286 | 290 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 287 | 291 | return record(d, "lsp", what, out) |
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | "unicode" | 8 | "unicode" |
| 9 | 9 | ||
| 10 | "mm/internal/config" | 10 | "mm/internal/config" |
| 11 | + "mm/internal/consent" | ||
| 11 | "mm/internal/detector" | 12 | "mm/internal/detector" |
| 12 | "mm/internal/lsp" | 13 | "mm/internal/lsp" |
| 13 | "mm/internal/spinner" | 14 | "mm/internal/spinner" |
| @@ -79,6 +80,9 @@ Use it before editing a symbol used in several files (references), to find where | |||
| 79 | return lspEvented(tctx, s, d, client, in) | 80 | return lspEvented(tctx, s, d, client, in) |
| 80 | } | 81 | } |
| 81 | announce("🔎 lsp: " + what) | 82 | announce("🔎 lsp: " + what) |
| 83 | + if in.Action == lspRename && !confirmed(tctx, "lsp", "lsp: "+what) { | ||
| 84 | + return refuse(d, "lsp", what, "[rename rejected by the user]") | ||
| 85 | + } | ||
| 82 | spinner.Start("Asking the language server") | 86 | spinner.Start("Asking the language server") |
| 83 | out, _ := runLSP(tctx, client, in) | 87 | out, _ := runLSP(tctx, client, in) |
| 84 | spinner.Suspend(func() { echoOutput(out) }) | 88 | spinner.Suspend(func() { echoOutput(out) }) |
| @@ -281,7 +285,7 @@ func lspEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, client | |||
| 281 | Path: absolute(ui.Resolve(in.Path)), | 285 | Path: absolute(ui.Resolve(in.Path)), |
| 282 | } | 286 | } |
| 283 | s.ToolStart(ev) | 287 | s.ToolStart(ev) |
| 284 | - if in.Action == lspRename && !s.Allow(ctx, ev) { | 288 | + if in.Action == lspRename && !consent.Gate(ctx, s, ev) { |
| 285 | out := "[rename rejected by the user]" | 289 | out := "[rename rejected by the user]" |
| 286 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 290 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 287 | return record(d, "lsp", what, out) | 291 | return record(d, "lsp", what, out) |
modified
internal/tools/plan.go +27 -16 | @@ -9,6 +9,7 @@ import ( | ||
| 9 | 9 | "sync" |
| 10 | 10 | "time" |
| 11 | 11 | |
| 12 | + "mm/internal/consent" | |
| 12 | 13 | "mm/internal/detector" |
| 13 | 14 | "mm/internal/spinner" |
| 14 | 15 | "mm/internal/ui" |
| @@ -33,10 +34,10 @@ import ( | ||
| 33 | 34 | |
| 34 | 35 | // Plan represents a named plan with its tasks. |
| 35 | 36 | type Plan struct { |
| 36 | - Name string `json:"name"` | |
| 37 | + Name string `json:"name"` | |
| 37 | 38 | Tasks []PlanTask `json:"tasks"` |
| 38 | - Current int `json:"current"` // 0 = not started, 1 = first task, etc. | |
| 39 | - CreatedAt string `json:"createdAt"` // ISO timestamp | |
| 39 | + Current int `json:"current"` // 0 = not started, 1 = first task, etc. | |
| 40 | + CreatedAt string `json:"createdAt"` // ISO timestamp | |
| 40 | 41 | } |
| 41 | 42 | |
| 42 | 43 | // PlanTask represents a single task within a plan. |
| @@ -46,7 +47,7 @@ type PlanTask struct { | ||
| 46 | 47 | Status string `json:"status"` // open, in_progress, done, failed |
| 47 | 48 | StartedAt string `json:"startedAt,omitempty"` |
| 48 | 49 | CompletedAt string `json:"completedAt,omitempty"` |
| 49 | - Notes string `json:"notes,omitempty"` | |
| 50 | + Notes string `json:"notes,omitempty"` | |
| 50 | 51 | } |
| 51 | 52 | |
| 52 | 53 | // planInput is the input schema for the plan tool. |
| @@ -73,23 +74,23 @@ type PlanTaskInput struct { | ||
| 73 | 74 | |
| 74 | 75 | // PlanOutput is the output format for plan operations. |
| 75 | 76 | type PlanOutput struct { |
| 76 | - Result string `json:"result"` | |
| 77 | - PlanName string `json:"planName,omitempty"` | |
| 78 | - Plan *Plan `json:"plan,omitempty"` | |
| 77 | + Result string `json:"result"` | |
| 78 | + PlanName string `json:"planName,omitempty"` | |
| 79 | + Plan *Plan `json:"plan,omitempty"` | |
| 79 | 80 | Task *PlanTask `json:"task,omitempty"` |
| 80 | - Status string `json:"status,omitempty"` | |
| 81 | - Error string `json:"error,omitempty"` | |
| 82 | - Current int `json:"current,omitempty"` | |
| 83 | - Total int `json:"total,omitempty"` | |
| 81 | + Status string `json:"status,omitempty"` | |
| 82 | + Error string `json:"error,omitempty"` | |
| 83 | + Current int `json:"current,omitempty"` | |
| 84 | + Total int `json:"total,omitempty"` | |
| 84 | 85 | } |
| 85 | 86 | |
| 86 | 87 | // PlanStore holds plans in memory, keyed by session ID and plan name. |
| 87 | 88 | // This is a package-level variable so plans persist across turns within |
| 88 | 89 | // a single session. Plans are cleared when a new session starts. |
| 89 | 90 | type PlanStore struct { |
| 90 | - mu sync.RWMutex | |
| 91 | - plans map[string]map[string]*Plan // sessionID -> planName -> Plan | |
| 92 | - nextID map[string]int // sessionID -> next task ID | |
| 91 | + mu sync.RWMutex | |
| 92 | + plans map[string]map[string]*Plan // sessionID -> planName -> Plan | |
| 93 | + nextID map[string]int // sessionID -> next task ID | |
| 93 | 94 | } |
| 94 | 95 | |
| 95 | 96 | var planStore = &PlanStore{ |
| @@ -450,6 +451,10 @@ Always show the user the current plan status using the status action.` | ||
| 450 | 451 | fmt.Printf("\n📋 plan: %s %s\n", in.Action, in.Name) |
| 451 | 452 | printMu.Unlock() |
| 452 | 453 | }) |
| 454 | + if planChanges(in.Action) && !confirmed(tctx, "plan", fmt.Sprintf("plan: %s %s", in.Action, in.Name)) { | |
| 455 | + inputJSON, _ := json.Marshal(in) | |
| 456 | + return refuse(d, "plan", string(inputJSON), "[plan: command rejected by the user]") | |
| 457 | + } | |
| 453 | 458 | |
| 454 | 459 | spinner.Start("Running") |
| 455 | 460 | |
| @@ -554,10 +559,16 @@ Always show the user the current plan status using the status action.` | ||
| 554 | 559 | }) |
| 555 | 560 | } |
| 556 | 561 | |
| 562 | +// planChanges reports whether a plan action changes a plan — the ones that | |
| 563 | +// ask for permission when yolo is off. | |
| 564 | +func planChanges(action string) bool { | |
| 565 | + return action == "create" || action == "update" || action == "next" || action == "delete" | |
| 566 | +} | |
| 567 | + | |
| 557 | 568 | // planEvented is the plan tool for event-consuming front ends (ACP). |
| 558 | 569 | func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in planInput) (string, error) { |
| 559 | 570 | eventKind := "read" |
| 560 | - if in.Action == "create" || in.Action == "update" || in.Action == "next" || in.Action == "delete" { | |
| 571 | + if planChanges(in.Action) { | |
| 561 | 572 | eventKind = "edit" |
| 562 | 573 | } |
| 563 | 574 | |
| @@ -573,7 +584,7 @@ func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in pl | ||
| 573 | 584 | |
| 574 | 585 | // For destructive actions, check permission |
| 575 | 586 | if eventKind == "edit" { |
| 576 | - if !s.Allow(ctx, ev) { | |
| 587 | + if !consent.Gate(ctx, s, ev) { | |
| 577 | 588 | out := "[plan: command rejected by the user]" |
| 578 | 589 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 579 | 590 | inputJSON, _ := json.Marshal(in) |
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | "sync" | 9 | "sync" |
| 10 | "time" | 10 | "time" |
| 11 | 11 | ||
| 12 | + "mm/internal/consent" | ||
| 12 | "mm/internal/detector" | 13 | "mm/internal/detector" |
| 13 | "mm/internal/spinner" | 14 | "mm/internal/spinner" |
| 14 | "mm/internal/ui" | 15 | "mm/internal/ui" |
| @@ -33,10 +34,10 @@ import ( | |||
| 33 | 34 | ||
| 34 | // Plan represents a named plan with its tasks. | 35 | // Plan represents a named plan with its tasks. |
| 35 | type Plan struct { | 36 | type Plan struct { |
| 36 | - Name string `json:"name"` | 37 | + Name string `json:"name"` |
| 37 | Tasks []PlanTask `json:"tasks"` | 38 | Tasks []PlanTask `json:"tasks"` |
| 38 | - Current int `json:"current"` // 0 = not started, 1 = first task, etc. | 39 | + Current int `json:"current"` // 0 = not started, 1 = first task, etc. |
| 39 | - CreatedAt string `json:"createdAt"` // ISO timestamp | 40 | + CreatedAt string `json:"createdAt"` // ISO timestamp |
| 40 | } | 41 | } |
| 41 | 42 | ||
| 42 | // PlanTask represents a single task within a plan. | 43 | // PlanTask represents a single task within a plan. |
| @@ -46,7 +47,7 @@ type PlanTask struct { | |||
| 46 | Status string `json:"status"` // open, in_progress, done, failed | 47 | Status string `json:"status"` // open, in_progress, done, failed |
| 47 | StartedAt string `json:"startedAt,omitempty"` | 48 | StartedAt string `json:"startedAt,omitempty"` |
| 48 | CompletedAt string `json:"completedAt,omitempty"` | 49 | CompletedAt string `json:"completedAt,omitempty"` |
| 49 | - Notes string `json:"notes,omitempty"` | 50 | + Notes string `json:"notes,omitempty"` |
| 50 | } | 51 | } |
| 51 | 52 | ||
| 52 | // planInput is the input schema for the plan tool. | 53 | // planInput is the input schema for the plan tool. |
| @@ -73,23 +74,23 @@ type PlanTaskInput struct { | |||
| 73 | 74 | ||
| 74 | // PlanOutput is the output format for plan operations. | 75 | // PlanOutput is the output format for plan operations. |
| 75 | type PlanOutput struct { | 76 | type PlanOutput struct { |
| 76 | - Result string `json:"result"` | 77 | + Result string `json:"result"` |
| 77 | - PlanName string `json:"planName,omitempty"` | 78 | + PlanName string `json:"planName,omitempty"` |
| 78 | - Plan *Plan `json:"plan,omitempty"` | 79 | + Plan *Plan `json:"plan,omitempty"` |
| 79 | Task *PlanTask `json:"task,omitempty"` | 80 | Task *PlanTask `json:"task,omitempty"` |
| 80 | - Status string `json:"status,omitempty"` | 81 | + Status string `json:"status,omitempty"` |
| 81 | - Error string `json:"error,omitempty"` | 82 | + Error string `json:"error,omitempty"` |
| 82 | - Current int `json:"current,omitempty"` | 83 | + Current int `json:"current,omitempty"` |
| 83 | - Total int `json:"total,omitempty"` | 84 | + Total int `json:"total,omitempty"` |
| 84 | } | 85 | } |
| 85 | 86 | ||
| 86 | // PlanStore holds plans in memory, keyed by session ID and plan name. | 87 | // PlanStore holds plans in memory, keyed by session ID and plan name. |
| 87 | // This is a package-level variable so plans persist across turns within | 88 | // This is a package-level variable so plans persist across turns within |
| 88 | // a single session. Plans are cleared when a new session starts. | 89 | // a single session. Plans are cleared when a new session starts. |
| 89 | type PlanStore struct { | 90 | type PlanStore struct { |
| 90 | - mu sync.RWMutex | 91 | + mu sync.RWMutex |
| 91 | - plans map[string]map[string]*Plan // sessionID -> planName -> Plan | 92 | + plans map[string]map[string]*Plan // sessionID -> planName -> Plan |
| 92 | - nextID map[string]int // sessionID -> next task ID | 93 | + nextID map[string]int // sessionID -> next task ID |
| 93 | } | 94 | } |
| 94 | 95 | ||
| 95 | var planStore = &PlanStore{ | 96 | var planStore = &PlanStore{ |
| @@ -450,6 +451,10 @@ Always show the user the current plan status using the status action.` | |||
| 450 | fmt.Printf("\n📋 plan: %s %s\n", in.Action, in.Name) | 451 | fmt.Printf("\n📋 plan: %s %s\n", in.Action, in.Name) |
| 451 | printMu.Unlock() | 452 | printMu.Unlock() |
| 452 | }) | 453 | }) |
| 454 | + if planChanges(in.Action) && !confirmed(tctx, "plan", fmt.Sprintf("plan: %s %s", in.Action, in.Name)) { | ||
| 455 | + inputJSON, _ := json.Marshal(in) | ||
| 456 | + return refuse(d, "plan", string(inputJSON), "[plan: command rejected by the user]") | ||
| 457 | + } | ||
| 453 | 458 | ||
| 454 | spinner.Start("Running") | 459 | spinner.Start("Running") |
| 455 | 460 | ||
| @@ -554,10 +559,16 @@ Always show the user the current plan status using the status action.` | |||
| 554 | }) | 559 | }) |
| 555 | } | 560 | } |
| 556 | 561 | ||
| 562 | +// planChanges reports whether a plan action changes a plan — the ones that | ||
| 563 | +// ask for permission when yolo is off. | ||
| 564 | +func planChanges(action string) bool { | ||
| 565 | + return action == "create" || action == "update" || action == "next" || action == "delete" | ||
| 566 | +} | ||
| 567 | + | ||
| 557 | // planEvented is the plan tool for event-consuming front ends (ACP). | 568 | // planEvented is the plan tool for event-consuming front ends (ACP). |
| 558 | func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in planInput) (string, error) { | 569 | func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in planInput) (string, error) { |
| 559 | eventKind := "read" | 570 | eventKind := "read" |
| 560 | - if in.Action == "create" || in.Action == "update" || in.Action == "next" || in.Action == "delete" { | 571 | + if planChanges(in.Action) { |
| 561 | eventKind = "edit" | 572 | eventKind = "edit" |
| 562 | } | 573 | } |
| 563 | 574 | ||
| @@ -573,7 +584,7 @@ func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in pl | |||
| 573 | 584 | ||
| 574 | // For destructive actions, check permission | 585 | // For destructive actions, check permission |
| 575 | if eventKind == "edit" { | 586 | if eventKind == "edit" { |
| 576 | - if !s.Allow(ctx, ev) { | 587 | + if !consent.Gate(ctx, s, ev) { |
| 577 | out := "[plan: command rejected by the user]" | 588 | out := "[plan: command rejected by the user]" |
| 578 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 589 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 579 | inputJSON, _ := json.Marshal(in) | 590 | inputJSON, _ := json.Marshal(in) |
modified
internal/tools/tools.go +5 -1 | @@ -18,6 +18,7 @@ import ( | ||
| 18 | 18 | "time" |
| 19 | 19 | |
| 20 | 20 | "mm/internal/config" |
| 21 | + "mm/internal/consent" | |
| 21 | 22 | "mm/internal/detector" |
| 22 | 23 | "mm/internal/skills" |
| 23 | 24 | "mm/internal/spinner" |
| @@ -221,6 +222,9 @@ Read before you write: if you do not know a path or the content of a file, run a | ||
| 221 | 222 | fmt.Printf("\n🛠️ bash: %s\n", in.Command) |
| 222 | 223 | printMu.Unlock() |
| 223 | 224 | }) |
| 225 | + if !confirmed(tctx, "bash", "bash: "+in.Command) { | |
| 226 | + return refuse(d, "bash", in.Command, "[command rejected by the user]") | |
| 227 | + } | |
| 224 | 228 | |
| 225 | 229 | // The command has its own 30s budget, and a slow one looks exactly |
| 226 | 230 | // like a hung agent. Retitle for the duration, then hand the line |
| @@ -265,7 +269,7 @@ func bashEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in ba | ||
| 265 | 269 | } |
| 266 | 270 | s.ToolStart(ev) |
| 267 | 271 | |
| 268 | - if !s.Allow(ctx, ev) { | |
| 272 | + if !consent.Gate(ctx, s, ev) { | |
| 269 | 273 | out := "[command rejected by the user]" |
| 270 | 274 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 271 | 275 | // Recorded like any other outcome: a model that keeps retrying a |
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | "time" | 18 | "time" |
| 19 | 19 | ||
| 20 | "mm/internal/config" | 20 | "mm/internal/config" |
| 21 | + "mm/internal/consent" | ||
| 21 | "mm/internal/detector" | 22 | "mm/internal/detector" |
| 22 | "mm/internal/skills" | 23 | "mm/internal/skills" |
| 23 | "mm/internal/spinner" | 24 | "mm/internal/spinner" |
| @@ -221,6 +222,9 @@ Read before you write: if you do not know a path or the content of a file, run a | |||
| 221 | fmt.Printf("\n🛠️ bash: %s\n", in.Command) | 222 | fmt.Printf("\n🛠️ bash: %s\n", in.Command) |
| 222 | printMu.Unlock() | 223 | printMu.Unlock() |
| 223 | }) | 224 | }) |
| 225 | + if !confirmed(tctx, "bash", "bash: "+in.Command) { | ||
| 226 | + return refuse(d, "bash", in.Command, "[command rejected by the user]") | ||
| 227 | + } | ||
| 224 | 228 | ||
| 225 | // The command has its own 30s budget, and a slow one looks exactly | 229 | // The command has its own 30s budget, and a slow one looks exactly |
| 226 | // like a hung agent. Retitle for the duration, then hand the line | 230 | // like a hung agent. Retitle for the duration, then hand the line |
| @@ -265,7 +269,7 @@ func bashEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in ba | |||
| 265 | } | 269 | } |
| 266 | s.ToolStart(ev) | 270 | s.ToolStart(ev) |
| 267 | 271 | ||
| 268 | - if !s.Allow(ctx, ev) { | 272 | + if !consent.Gate(ctx, s, ev) { |
| 269 | out := "[command rejected by the user]" | 273 | out := "[command rejected by the user]" |
| 270 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | 274 | s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) |
| 271 | // Recorded like any other outcome: a model that keeps retrying a | 275 | // Recorded like any other outcome: a model that keeps retrying a |
modified
internal/tui/draw.go +2 -2 | @@ -89,7 +89,7 @@ func (v *View) drawRule(p painter) { | ||
| 89 | 89 | func (v *View) ruleLabel() string { |
| 90 | 90 | switch { |
| 91 | 91 | case v.ask != nil: |
| 92 | - return " a allow once · s allow for this session · r / Esc reject " | |
| 92 | + return " a allow once · s allow for this session · r / Esc reject · n never " | |
| 93 | 93 | case v.busy != "": |
| 94 | 94 | return fmt.Sprintf(" %c %s — Esc stops it · Enter sends the next question ", v.spinner(), v.busy) |
| 95 | 95 | case !v.onInput: |
| @@ -161,7 +161,7 @@ func (v *View) drawInputRow(p painter, row int, r inputRow) { | ||
| 161 | 161 | func (v *View) drawPermission(p painter) { |
| 162 | 162 | v.screen.HideCursor() |
| 163 | 163 | p.textLimited(0, 0, p.width(), "? "+v.ask.title, stAsk) |
| 164 | - p.textLimited(2, 1, max(p.width()-2, 0), "[a] allow once [s] allow "+v.ask.tool+" for this session [r] reject", stBody) | |
| 164 | + p.textLimited(2, 1, max(p.width()-2, 0), "[a] allow once [s] allow "+v.ask.tool+" for this session [r] reject [n] never "+v.ask.tool, stBody) | |
| 165 | 165 | } |
| 166 | 166 | |
| 167 | 167 | // drawStatus is the bottom bar: key hints left, version and counters right. |
| @@ -89,7 +89,7 @@ func (v *View) drawRule(p painter) { | |||
| 89 | func (v *View) ruleLabel() string { | 89 | func (v *View) ruleLabel() string { |
| 90 | switch { | 90 | switch { |
| 91 | case v.ask != nil: | 91 | case v.ask != nil: |
| 92 | - return " a allow once · s allow for this session · r / Esc reject " | 92 | + return " a allow once · s allow for this session · r / Esc reject · n never " |
| 93 | case v.busy != "": | 93 | case v.busy != "": |
| 94 | return fmt.Sprintf(" %c %s — Esc stops it · Enter sends the next question ", v.spinner(), v.busy) | 94 | return fmt.Sprintf(" %c %s — Esc stops it · Enter sends the next question ", v.spinner(), v.busy) |
| 95 | case !v.onInput: | 95 | case !v.onInput: |
| @@ -161,7 +161,7 @@ func (v *View) drawInputRow(p painter, row int, r inputRow) { | |||
| 161 | func (v *View) drawPermission(p painter) { | 161 | func (v *View) drawPermission(p painter) { |
| 162 | v.screen.HideCursor() | 162 | v.screen.HideCursor() |
| 163 | p.textLimited(0, 0, p.width(), "? "+v.ask.title, stAsk) | 163 | p.textLimited(0, 0, p.width(), "? "+v.ask.title, stAsk) |
| 164 | - p.textLimited(2, 1, max(p.width()-2, 0), "[a] allow once [s] allow "+v.ask.tool+" for this session [r] reject", stBody) | 164 | + p.textLimited(2, 1, max(p.width()-2, 0), "[a] allow once [s] allow "+v.ask.tool+" for this session [r] reject [n] never "+v.ask.tool, stBody) |
| 165 | } | 165 | } |
| 166 | 166 | ||
| 167 | // drawStatus is the bottom bar: key hints left, version and counters right. | 167 | // drawStatus is the bottom bar: key hints left, version and counters right. |
modified
internal/tui/events.go +5 -2 | @@ -53,8 +53,9 @@ func (v *View) handleKey(ev *tcell.EventKey) { | ||
| 53 | 53 | } |
| 54 | 54 | |
| 55 | 55 | // answerPermission maps a key onto the pending permission: a once, s for |
| 56 | -// the session, r or Escape reject. Anything else is ignored — the box is | |
| 57 | -// not editable while the question stands. | |
| 56 | +// the session, r or Escape reject this call, n never (this tool, for the | |
| 57 | +// session). Anything else is ignored — the box is not editable while the | |
| 58 | +// question stands. | |
| 58 | 59 | func (v *View) answerPermission(ev *tcell.EventKey) { |
| 59 | 60 | var answer permissionAnswer |
| 60 | 61 | switch { |
| @@ -64,6 +65,8 @@ func (v *View) answerPermission(ev *tcell.EventKey) { | ||
| 64 | 65 | answer = permAllowSession |
| 65 | 66 | case ev.Key() == tcell.KeyRune && (ev.Rune() == 'r' || ev.Rune() == 'R'), ev.Key() == tcell.KeyEscape: |
| 66 | 67 | answer = permReject |
| 68 | + case ev.Key() == tcell.KeyRune && (ev.Rune() == 'n' || ev.Rune() == 'N'): | |
| 69 | + answer = permNever | |
| 67 | 70 | default: |
| 68 | 71 | return |
| 69 | 72 | } |
| @@ -53,8 +53,9 @@ func (v *View) handleKey(ev *tcell.EventKey) { | |||
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | // answerPermission maps a key onto the pending permission: a once, s for | 55 | // answerPermission maps a key onto the pending permission: a once, s for |
| 56 | -// the session, r or Escape reject. Anything else is ignored — the box is | 56 | +// the session, r or Escape reject this call, n never (this tool, for the |
| 57 | -// not editable while the question stands. | 57 | +// session). Anything else is ignored — the box is not editable while the |
| 58 | +// question stands. | ||
| 58 | func (v *View) answerPermission(ev *tcell.EventKey) { | 59 | func (v *View) answerPermission(ev *tcell.EventKey) { |
| 59 | var answer permissionAnswer | 60 | var answer permissionAnswer |
| 60 | switch { | 61 | switch { |
| @@ -64,6 +65,8 @@ func (v *View) answerPermission(ev *tcell.EventKey) { | |||
| 64 | answer = permAllowSession | 65 | answer = permAllowSession |
| 65 | case ev.Key() == tcell.KeyRune && (ev.Rune() == 'r' || ev.Rune() == 'R'), ev.Key() == tcell.KeyEscape: | 66 | case ev.Key() == tcell.KeyRune && (ev.Rune() == 'r' || ev.Rune() == 'R'), ev.Key() == tcell.KeyEscape: |
| 66 | answer = permReject | 67 | answer = permReject |
| 68 | + case ev.Key() == tcell.KeyRune && (ev.Rune() == 'n' || ev.Rune() == 'N'): | ||
| 69 | + answer = permNever | ||
| 67 | default: | 70 | default: |
| 68 | return | 71 | return |
| 69 | } | 72 | } |
modified
internal/tui/sink.go +12 -19 | @@ -4,13 +4,15 @@ import ( | ||
| 4 | 4 | "context" |
| 5 | 5 | |
| 6 | 6 | "mm/internal/config" |
| 7 | + "mm/internal/consent" | |
| 7 | 8 | "mm/internal/ui" |
| 8 | 9 | ) |
| 9 | 10 | |
| 10 | 11 | // sink is the window's ui.Sink: the tools' events become transcript entries |
| 11 | 12 | // and a wake-up, from whatever goroutine they arrive on. Allow is the one |
| 12 | -// event with an answer: with `permissions: ask` the tool waits for the user | |
| 13 | -// (a/s/r in the box); with `auto`, today's terminal behaviour, it runs. | |
| 13 | +// event with an answer: the tool waits for the user (a/s/r/n in the box). | |
| 14 | +// Whether it asks at all is decided before — consent.Gate (yolo) for the | |
| 15 | +// built-in tools, the MCP wrapper for its own. | |
| 14 | 16 | type sink struct{ r *runner } |
| 15 | 17 | |
| 16 | 18 | var _ ui.Sink = (*sink)(nil) |
| @@ -36,24 +38,15 @@ func (k *sink) ToolStart(ev ui.ToolEvent) { | ||
| 36 | 38 | k.r.wake() |
| 37 | 39 | } |
| 38 | 40 | |
| 39 | -// Allow gates bash and the file tools. read_skill never asks (it reads what | |
| 40 | -// the agent ships — the tools skip Allow for it). "Allow for this session" | |
| 41 | -// is remembered per tool, as the ACP façade does. | |
| 41 | +// Allow asks the user about one call. read_skill and read_file never get | |
| 42 | +// here (they skip Allow). "For this session" and "never" are remembered per | |
| 43 | +// tool by the runner's consent.Policy, as the ACP façade does. | |
| 42 | 44 | func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { |
| 43 | - if config.Cfg.Permissions != config.PermissionsAsk || k.r.allowAlways[ev.Tool] { | |
| 44 | - return true | |
| 45 | - } | |
| 46 | - k.r.t.SetStatus(ev.ID, StatusWaiting) | |
| 47 | - k.r.wake() | |
| 48 | - switch k.r.request(ctx, ev) { | |
| 49 | - case permAllowOnce: | |
| 50 | - return true | |
| 51 | - case permAllowSession: | |
| 52 | - k.r.allowAlways[ev.Tool] = true | |
| 53 | - return true | |
| 54 | - default: | |
| 55 | - return false | |
| 56 | - } | |
| 45 | + return k.r.policy.Decide(ctx, ev.Tool, func(ctx context.Context) consent.Answer { | |
| 46 | + k.r.t.SetStatus(ev.ID, StatusWaiting) | |
| 47 | + k.r.wake() | |
| 48 | + return k.r.request(ctx, ev) | |
| 49 | + }) | |
| 57 | 50 | } |
| 58 | 51 | |
| 59 | 52 | func (k *sink) ToolRunning(id string) { |
| @@ -4,13 +4,15 @@ import ( | |||
| 4 | "context" | 4 | "context" |
| 5 | 5 | ||
| 6 | "mm/internal/config" | 6 | "mm/internal/config" |
| 7 | + "mm/internal/consent" | ||
| 7 | "mm/internal/ui" | 8 | "mm/internal/ui" |
| 8 | ) | 9 | ) |
| 9 | 10 | ||
| 10 | // sink is the window's ui.Sink: the tools' events become transcript entries | 11 | // sink is the window's ui.Sink: the tools' events become transcript entries |
| 11 | // and a wake-up, from whatever goroutine they arrive on. Allow is the one | 12 | // and a wake-up, from whatever goroutine they arrive on. Allow is the one |
| 12 | -// event with an answer: with `permissions: ask` the tool waits for the user | 13 | +// event with an answer: the tool waits for the user (a/s/r/n in the box). |
| 13 | -// (a/s/r in the box); with `auto`, today's terminal behaviour, it runs. | 14 | +// Whether it asks at all is decided before — consent.Gate (yolo) for the |
| 15 | +// built-in tools, the MCP wrapper for its own. | ||
| 14 | type sink struct{ r *runner } | 16 | type sink struct{ r *runner } |
| 15 | 17 | ||
| 16 | var _ ui.Sink = (*sink)(nil) | 18 | var _ ui.Sink = (*sink)(nil) |
| @@ -36,24 +38,15 @@ func (k *sink) ToolStart(ev ui.ToolEvent) { | |||
| 36 | k.r.wake() | 38 | k.r.wake() |
| 37 | } | 39 | } |
| 38 | 40 | ||
| 39 | -// Allow gates bash and the file tools. read_skill never asks (it reads what | 41 | +// Allow asks the user about one call. read_skill and read_file never get |
| 40 | -// the agent ships — the tools skip Allow for it). "Allow for this session" | 42 | +// here (they skip Allow). "For this session" and "never" are remembered per |
| 41 | -// is remembered per tool, as the ACP façade does. | 43 | +// tool by the runner's consent.Policy, as the ACP façade does. |
| 42 | func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { | 44 | func (k *sink) Allow(ctx context.Context, ev ui.ToolEvent) bool { |
| 43 | - if config.Cfg.Permissions != config.PermissionsAsk || k.r.allowAlways[ev.Tool] { | 45 | + return k.r.policy.Decide(ctx, ev.Tool, func(ctx context.Context) consent.Answer { |
| 44 | - return true | 46 | + k.r.t.SetStatus(ev.ID, StatusWaiting) |
| 45 | - } | 47 | + k.r.wake() |
| 46 | - k.r.t.SetStatus(ev.ID, StatusWaiting) | 48 | + return k.r.request(ctx, ev) |
| 47 | - k.r.wake() | 49 | + }) |
| 48 | - switch k.r.request(ctx, ev) { | ||
| 49 | - case permAllowOnce: | ||
| 50 | - return true | ||
| 51 | - case permAllowSession: | ||
| 52 | - k.r.allowAlways[ev.Tool] = true | ||
| 53 | - return true | ||
| 54 | - default: | ||
| 55 | - return false | ||
| 56 | - } | ||
| 57 | } | 50 | } |
| 58 | 51 | ||
| 59 | func (k *sink) ToolRunning(id string) { | 52 | func (k *sink) ToolRunning(id string) { |
modified
internal/tui/turn.go +10 -10 | @@ -10,6 +10,7 @@ import ( | ||
| 10 | 10 | "mm/internal/ask" |
| 11 | 11 | "mm/internal/compact" |
| 12 | 12 | "mm/internal/config" |
| 13 | + "mm/internal/consent" | |
| 13 | 14 | "mm/internal/detector" |
| 14 | 15 | "mm/internal/engine" |
| 15 | 16 | "mm/internal/mention" |
| @@ -109,9 +110,9 @@ type runner struct { | ||
| 109 | 110 | results chan outcome |
| 110 | 111 | pending Prompt // input to run once the cancelled turn has settled |
| 111 | 112 | |
| 112 | - askMu sync.Mutex | |
| 113 | - ask *permission | |
| 114 | - allowAlways map[string]bool | |
| 113 | + askMu sync.Mutex | |
| 114 | + ask *permission | |
| 115 | + policy consent.Policy // "for this session" and "never", per tool | |
| 115 | 116 | |
| 116 | 117 | // streamed says the sink saw at least one chunk this turn. An answer |
| 117 | 118 | // that did not stream (a test model, or a server answering in one block |
| @@ -121,13 +122,12 @@ type runner struct { | ||
| 121 | 122 | |
| 122 | 123 | func newRunner(opts Options, t *Transcript, wake func()) *runner { |
| 123 | 124 | r := &runner{ |
| 124 | - opts: opts, | |
| 125 | - t: t, | |
| 126 | - wake: wake, | |
| 127 | - keep: store.NewKeeper(opts.Cwd), | |
| 128 | - messages: session.Fresh(opts.System), | |
| 129 | - results: make(chan outcome, 1), | |
| 130 | - allowAlways: map[string]bool{}, | |
| 125 | + opts: opts, | |
| 126 | + t: t, | |
| 127 | + wake: wake, | |
| 128 | + keep: store.NewKeeper(opts.Cwd), | |
| 129 | + messages: session.Fresh(opts.System), | |
| 130 | + results: make(chan outcome, 1), | |
| 131 | 131 | } |
| 132 | 132 | r.busy.Store("") |
| 133 | 133 | return r |
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | "mm/internal/ask" | 10 | "mm/internal/ask" |
| 11 | "mm/internal/compact" | 11 | "mm/internal/compact" |
| 12 | "mm/internal/config" | 12 | "mm/internal/config" |
| 13 | + "mm/internal/consent" | ||
| 13 | "mm/internal/detector" | 14 | "mm/internal/detector" |
| 14 | "mm/internal/engine" | 15 | "mm/internal/engine" |
| 15 | "mm/internal/mention" | 16 | "mm/internal/mention" |
| @@ -109,9 +110,9 @@ type runner struct { | |||
| 109 | results chan outcome | 110 | results chan outcome |
| 110 | pending Prompt // input to run once the cancelled turn has settled | 111 | pending Prompt // input to run once the cancelled turn has settled |
| 111 | 112 | ||
| 112 | - askMu sync.Mutex | 113 | + askMu sync.Mutex |
| 113 | - ask *permission | 114 | + ask *permission |
| 114 | - allowAlways map[string]bool | 115 | + policy consent.Policy // "for this session" and "never", per tool |
| 115 | 116 | ||
| 116 | // streamed says the sink saw at least one chunk this turn. An answer | 117 | // streamed says the sink saw at least one chunk this turn. An answer |
| 117 | // that did not stream (a test model, or a server answering in one block | 118 | // that did not stream (a test model, or a server answering in one block |
| @@ -121,13 +122,12 @@ type runner struct { | |||
| 121 | 122 | ||
| 122 | func newRunner(opts Options, t *Transcript, wake func()) *runner { | 123 | func newRunner(opts Options, t *Transcript, wake func()) *runner { |
| 123 | r := &runner{ | 124 | r := &runner{ |
| 124 | - opts: opts, | 125 | + opts: opts, |
| 125 | - t: t, | 126 | + t: t, |
| 126 | - wake: wake, | 127 | + wake: wake, |
| 127 | - keep: store.NewKeeper(opts.Cwd), | 128 | + keep: store.NewKeeper(opts.Cwd), |
| 128 | - messages: session.Fresh(opts.System), | 129 | + messages: session.Fresh(opts.System), |
| 129 | - results: make(chan outcome, 1), | 130 | + results: make(chan outcome, 1), |
| 130 | - allowAlways: map[string]bool{}, | ||
| 131 | } | 131 | } |
| 132 | r.busy.Store("") | 132 | r.busy.Store("") |
| 133 | return r | 133 | return r |
modified
internal/tui/turn_test.go +19 -13 | @@ -225,11 +225,11 @@ func TestRunnerCompact(t *testing.T) { | ||
| 225 | 225 | } |
| 226 | 226 | } |
| 227 | 227 | |
| 228 | -// With permissions: ask, Allow posts a question, waits for the answer, and | |
| 229 | -// remembers "for this session" per tool; a cancelled turn rejects. | |
| 228 | +// Allow posts a question, waits for the answer, and remembers "for this | |
| 229 | +// session" and "never" per tool; a cancelled turn rejects. Whether to ask at | |
| 230 | +// all (yolo) is the caller's decision: consent.Gate. | |
| 230 | 231 | func TestSinkAllowAsk(t *testing.T) { |
| 231 | 232 | r, tr := newTestRunner(t, &fakeModel{}) |
| 232 | - config.Cfg.Permissions = config.PermissionsAsk | |
| 233 | 233 | k := &sink{r: r} |
| 234 | 234 | ev := ui.ToolEvent{ID: "call_1", Tool: "bash", Title: "bash: ls"} |
| 235 | 235 | tr.ToolStart(ev) |
| @@ -245,7 +245,8 @@ func TestSinkAllowAsk(t *testing.T) { | ||
| 245 | 245 | t.Fatalf("no question posted: %+v, status %q", ask, tr.Entries()[0].Status) |
| 246 | 246 | } |
| 247 | 247 | r.answer(permAllowSession) |
| 248 | - if !<-got || !r.allowAlways["bash"] || r.currentAsk() != nil { | |
| 248 | + ran := <-got | |
| 249 | + if allowed, known := r.policy.Remembered("bash"); !ran || !allowed || !known || r.currentAsk() != nil { | |
| 249 | 250 | t.Error("allow-for-session was not honoured") |
| 250 | 251 | } |
| 251 | 252 | if !k.Allow(context.Background(), ev) { |
| @@ -254,12 +255,15 @@ func TestSinkAllowAsk(t *testing.T) { | ||
| 254 | 255 | |
| 255 | 256 | // Reject, on another tool. |
| 256 | 257 | ev2 := ui.ToolEvent{ID: "call_2", Tool: "edit_file", Title: "edit_file: x"} |
| 257 | - go func() { got <- k.Allow(context.Background(), ev2) }() | |
| 258 | - for r.currentAsk() == nil { | |
| 259 | - time.Sleep(time.Millisecond) | |
| 258 | + answerNext := func(a permissionAnswer) bool { | |
| 259 | + go func() { got <- k.Allow(context.Background(), ev2) }() | |
| 260 | + for r.currentAsk() == nil { | |
| 261 | + time.Sleep(time.Millisecond) | |
| 262 | + } | |
| 263 | + r.answer(a) | |
| 264 | + return <-got | |
| 260 | 265 | } |
| 261 | - r.answer(permReject) | |
| 262 | - if <-got { | |
| 266 | + if answerNext(permReject) { | |
| 263 | 267 | t.Error("reject allowed the tool") |
| 264 | 268 | } |
| 265 | 269 | |
| @@ -274,10 +278,12 @@ func TestSinkAllowAsk(t *testing.T) { | ||
| 274 | 278 | t.Error("a cancelled turn did not reject and clear the question") |
| 275 | 279 | } |
| 276 | 280 | |
| 277 | - // auto: never asks. | |
| 278 | - config.Cfg.Permissions = config.PermissionsAuto | |
| 279 | - if !k.Allow(context.Background(), ui.ToolEvent{ID: "c", Tool: "write_file"}) || r.currentAsk() != nil { | |
| 280 | - t.Error("auto asked") | |
| 281 | + // Never: refused now, and refused later without a question. | |
| 282 | + if answerNext(permNever) { | |
| 283 | + t.Error("never allowed the tool") | |
| 284 | + } | |
| 285 | + if k.Allow(context.Background(), ev2) || r.currentAsk() != nil { | |
| 286 | + t.Error("never was not remembered") | |
| 281 | 287 | } |
| 282 | 288 | if k.WorkDir() != r.opts.Cwd { |
| 283 | 289 | t.Error("WorkDir is not the start directory") |
| @@ -225,11 +225,11 @@ func TestRunnerCompact(t *testing.T) { | |||
| 225 | } | 225 | } |
| 226 | } | 226 | } |
| 227 | 227 | ||
| 228 | -// With permissions: ask, Allow posts a question, waits for the answer, and | 228 | +// Allow posts a question, waits for the answer, and remembers "for this |
| 229 | -// remembers "for this session" per tool; a cancelled turn rejects. | 229 | +// session" and "never" per tool; a cancelled turn rejects. Whether to ask at |
| 230 | +// all (yolo) is the caller's decision: consent.Gate. | ||
| 230 | func TestSinkAllowAsk(t *testing.T) { | 231 | func TestSinkAllowAsk(t *testing.T) { |
| 231 | r, tr := newTestRunner(t, &fakeModel{}) | 232 | r, tr := newTestRunner(t, &fakeModel{}) |
| 232 | - config.Cfg.Permissions = config.PermissionsAsk | ||
| 233 | k := &sink{r: r} | 233 | k := &sink{r: r} |
| 234 | ev := ui.ToolEvent{ID: "call_1", Tool: "bash", Title: "bash: ls"} | 234 | ev := ui.ToolEvent{ID: "call_1", Tool: "bash", Title: "bash: ls"} |
| 235 | tr.ToolStart(ev) | 235 | tr.ToolStart(ev) |
| @@ -245,7 +245,8 @@ func TestSinkAllowAsk(t *testing.T) { | |||
| 245 | t.Fatalf("no question posted: %+v, status %q", ask, tr.Entries()[0].Status) | 245 | t.Fatalf("no question posted: %+v, status %q", ask, tr.Entries()[0].Status) |
| 246 | } | 246 | } |
| 247 | r.answer(permAllowSession) | 247 | r.answer(permAllowSession) |
| 248 | - if !<-got || !r.allowAlways["bash"] || r.currentAsk() != nil { | 248 | + ran := <-got |
| 249 | + if allowed, known := r.policy.Remembered("bash"); !ran || !allowed || !known || r.currentAsk() != nil { | ||
| 249 | t.Error("allow-for-session was not honoured") | 250 | t.Error("allow-for-session was not honoured") |
| 250 | } | 251 | } |
| 251 | if !k.Allow(context.Background(), ev) { | 252 | if !k.Allow(context.Background(), ev) { |
| @@ -254,12 +255,15 @@ func TestSinkAllowAsk(t *testing.T) { | |||
| 254 | 255 | ||
| 255 | // Reject, on another tool. | 256 | // Reject, on another tool. |
| 256 | ev2 := ui.ToolEvent{ID: "call_2", Tool: "edit_file", Title: "edit_file: x"} | 257 | ev2 := ui.ToolEvent{ID: "call_2", Tool: "edit_file", Title: "edit_file: x"} |
| 257 | - go func() { got <- k.Allow(context.Background(), ev2) }() | 258 | + answerNext := func(a permissionAnswer) bool { |
| 258 | - for r.currentAsk() == nil { | 259 | + go func() { got <- k.Allow(context.Background(), ev2) }() |
| 259 | - time.Sleep(time.Millisecond) | 260 | + for r.currentAsk() == nil { |
| 261 | + time.Sleep(time.Millisecond) | ||
| 262 | + } | ||
| 263 | + r.answer(a) | ||
| 264 | + return <-got | ||
| 260 | } | 265 | } |
| 261 | - r.answer(permReject) | 266 | + if answerNext(permReject) { |
| 262 | - if <-got { | ||
| 263 | t.Error("reject allowed the tool") | 267 | t.Error("reject allowed the tool") |
| 264 | } | 268 | } |
| 265 | 269 | ||
| @@ -274,10 +278,12 @@ func TestSinkAllowAsk(t *testing.T) { | |||
| 274 | t.Error("a cancelled turn did not reject and clear the question") | 278 | t.Error("a cancelled turn did not reject and clear the question") |
| 275 | } | 279 | } |
| 276 | 280 | ||
| 277 | - // auto: never asks. | 281 | + // Never: refused now, and refused later without a question. |
| 278 | - config.Cfg.Permissions = config.PermissionsAuto | 282 | + if answerNext(permNever) { |
| 279 | - if !k.Allow(context.Background(), ui.ToolEvent{ID: "c", Tool: "write_file"}) || r.currentAsk() != nil { | 283 | + t.Error("never allowed the tool") |
| 280 | - t.Error("auto asked") | 284 | + } |
| 285 | + if k.Allow(context.Background(), ev2) || r.currentAsk() != nil { | ||
| 286 | + t.Error("never was not remembered") | ||
| 281 | } | 287 | } |
| 282 | if k.WorkDir() != r.opts.Cwd { | 288 | if k.WorkDir() != r.opts.Cwd { |
| 283 | t.Error("WorkDir is not the start directory") | 289 | t.Error("WorkDir is not the start directory") |
modified
internal/tui/view.go +10 -7 | @@ -4,6 +4,7 @@ import ( | ||
| 4 | 4 | "strings" |
| 5 | 5 | "time" |
| 6 | 6 | |
| 7 | + "mm/internal/consent" | |
| 7 | 8 | "mm/internal/skills" |
| 8 | 9 | |
| 9 | 10 | "github.com/gdamore/tcell/v2" |
| @@ -17,21 +18,23 @@ const inputHeight = 3 | ||
| 17 | 18 | // status bar. The conversation gets the rest. |
| 18 | 19 | const chromeRows = 1 + 1 + inputHeight + 1 |
| 19 | 20 | |
| 20 | -// permission is a tool call waiting for the user's yes or no. It is shown in | |
| 21 | -// place of the input box; a/s/r answer it through hooks.answer (events.go). | |
| 21 | +// permission is a tool call waiting for the user's answer. It is shown in | |
| 22 | +// place of the input box; a/s/r/n answer it through hooks.answer (events.go). | |
| 22 | 23 | type permission struct { |
| 23 | 24 | tool string |
| 24 | 25 | title string |
| 25 | 26 | reply chan permissionAnswer // the runner's, buffered; the view never writes it |
| 26 | 27 | } |
| 27 | 28 | |
| 28 | -// permissionAnswer is what the user chose. | |
| 29 | -type permissionAnswer int | |
| 29 | +// permissionAnswer is what the user chose — consent's four answers, under the | |
| 30 | +// names the window has always used. | |
| 31 | +type permissionAnswer = consent.Answer | |
| 30 | 32 | |
| 31 | 33 | const ( |
| 32 | - permReject permissionAnswer = iota | |
| 33 | - permAllowOnce | |
| 34 | - permAllowSession | |
| 34 | + permReject = consent.RejectOnce | |
| 35 | + permAllowOnce = consent.AllowOnce | |
| 36 | + permAllowSession = consent.AllowSession | |
| 37 | + permNever = consent.Never | |
| 35 | 38 | ) |
| 36 | 39 | |
| 37 | 40 | // hooks is what the window asks the front end to do. The view owns the |
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | "strings" | 4 | "strings" |
| 5 | "time" | 5 | "time" |
| 6 | 6 | ||
| 7 | + "mm/internal/consent" | ||
| 7 | "mm/internal/skills" | 8 | "mm/internal/skills" |
| 8 | 9 | ||
| 9 | "github.com/gdamore/tcell/v2" | 10 | "github.com/gdamore/tcell/v2" |
| @@ -17,21 +18,23 @@ const inputHeight = 3 | |||
| 17 | // status bar. The conversation gets the rest. | 18 | // status bar. The conversation gets the rest. |
| 18 | const chromeRows = 1 + 1 + inputHeight + 1 | 19 | const chromeRows = 1 + 1 + inputHeight + 1 |
| 19 | 20 | ||
| 20 | -// permission is a tool call waiting for the user's yes or no. It is shown in | 21 | +// permission is a tool call waiting for the user's answer. It is shown in |
| 21 | -// place of the input box; a/s/r answer it through hooks.answer (events.go). | 22 | +// place of the input box; a/s/r/n answer it through hooks.answer (events.go). |
| 22 | type permission struct { | 23 | type permission struct { |
| 23 | tool string | 24 | tool string |
| 24 | title string | 25 | title string |
| 25 | reply chan permissionAnswer // the runner's, buffered; the view never writes it | 26 | reply chan permissionAnswer // the runner's, buffered; the view never writes it |
| 26 | } | 27 | } |
| 27 | 28 | ||
| 28 | -// permissionAnswer is what the user chose. | 29 | +// permissionAnswer is what the user chose — consent's four answers, under the |
| 29 | -type permissionAnswer int | 30 | +// names the window has always used. |
| 31 | +type permissionAnswer = consent.Answer | ||
| 30 | 32 | ||
| 31 | const ( | 33 | const ( |
| 32 | - permReject permissionAnswer = iota | 34 | + permReject = consent.RejectOnce |
| 33 | - permAllowOnce | 35 | + permAllowOnce = consent.AllowOnce |
| 34 | - permAllowSession | 36 | + permAllowSession = consent.AllowSession |
| 37 | + permNever = consent.Never | ||
| 35 | ) | 38 | ) |
| 36 | 39 | ||
| 37 | // hooks is what the window asks the front end to do. The view owns the | 40 | // hooks is what the window asks the front end to do. The view owns the |
modified
internal/tui/view_test.go +1 -1 | @@ -318,7 +318,7 @@ func TestPermissionKeys(t *testing.T) { | ||
| 318 | 318 | key tcell.Key |
| 319 | 319 | r rune |
| 320 | 320 | want permissionAnswer |
| 321 | - }{{tcell.KeyRune, 'a', permAllowOnce}, {tcell.KeyRune, 's', permAllowSession}, {tcell.KeyRune, 'r', permReject}, {tcell.KeyEscape, 0, permReject}} { | |
| 321 | + }{{tcell.KeyRune, 'a', permAllowOnce}, {tcell.KeyRune, 's', permAllowSession}, {tcell.KeyRune, 'r', permReject}, {tcell.KeyEscape, 0, permReject}, {tcell.KeyRune, 'n', permNever}} { | |
| 322 | 322 | hs.answers = nil |
| 323 | 323 | hs.v.ask = &permission{tool: "bash", title: "bash: rm -rf build"} |
| 324 | 324 | if text := hs.screenText(); !strings.Contains(text, "? bash: rm -rf build") || !strings.Contains(text, "[s] allow bash for this session") { |
| @@ -318,7 +318,7 @@ func TestPermissionKeys(t *testing.T) { | |||
| 318 | key tcell.Key | 318 | key tcell.Key |
| 319 | r rune | 319 | r rune |
| 320 | want permissionAnswer | 320 | want permissionAnswer |
| 321 | - }{{tcell.KeyRune, 'a', permAllowOnce}, {tcell.KeyRune, 's', permAllowSession}, {tcell.KeyRune, 'r', permReject}, {tcell.KeyEscape, 0, permReject}} { | 321 | + }{{tcell.KeyRune, 'a', permAllowOnce}, {tcell.KeyRune, 's', permAllowSession}, {tcell.KeyRune, 'r', permReject}, {tcell.KeyEscape, 0, permReject}, {tcell.KeyRune, 'n', permNever}} { |
| 322 | hs.answers = nil | 322 | hs.answers = nil |
| 323 | hs.v.ask = &permission{tool: "bash", title: "bash: rm -rf build"} | 323 | hs.v.ask = &permission{tool: "bash", title: "bash: rm -rf build"} |
| 324 | if text := hs.screenText(); !strings.Contains(text, "? bash: rm -rf build") || !strings.Contains(text, "[s] allow bash for this session") { | 324 | if text := hs.screenText(); !strings.Contains(text, "? bash: rm -rf build") || !strings.Contains(text, "[s] allow bash for this session") { |
modified
main.go +23 -13 | @@ -70,7 +70,7 @@ func main() { | ||
| 70 | 70 | // |
| 71 | 71 | // The flag package gives us "-h" for free; a second argument is a typo, and |
| 72 | 72 | // saying so is better than ignoring it. |
| 73 | - | |
| 73 | + | |
| 74 | 74 | // Handle --prompt/-p flag which can appear after the config file argument |
| 75 | 75 | // We manually extract it before flag.Parse() so both orders work: |
| 76 | 76 | // mm --prompt "hello" agent.yaml AND mm agent.yaml --prompt "hello" |
| @@ -100,7 +100,7 @@ func main() { | ||
| 100 | 100 | } |
| 101 | 101 | // Replace os.Args for flag.Parse() |
| 102 | 102 | os.Args = append([]string{os.Args[0]}, remainingArgs...) |
| 103 | - | |
| 103 | + | |
| 104 | 104 | configPath := flag.String("config", "", "path to the YAML config file (default: $AGENT_CONFIG, then ./agent.yaml)") |
| 105 | 105 | acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL") |
| 106 | 106 | tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal") |
| @@ -111,7 +111,7 @@ func main() { | ||
| 111 | 111 | askAgent := flag.String("ask", "", "ask the agent `name` (a key of mmConfigs) the -p prompt, or stdin; print only its answer on stdout, and exit") |
| 112 | 112 | askWithContext := flag.String("ask-with-context", "", "same as --ask, with the conversation read from stdin (question with -p, or after a \"---\" line)") |
| 113 | 113 | flag.Parse() |
| 114 | - | |
| 114 | + | |
| 115 | 115 | // If prompt was extracted manually, use it (overrides flag value) |
| 116 | 116 | if promptArg != "" { |
| 117 | 117 | *promptMode = promptArg |
| @@ -122,7 +122,7 @@ func main() { | ||
| 122 | 122 | if askContextArg != "" { |
| 123 | 123 | *askWithContext = askContextArg |
| 124 | 124 | } |
| 125 | - | |
| 125 | + | |
| 126 | 126 | // Track if --config was explicitly set via the flag |
| 127 | 127 | configExplicit := false |
| 128 | 128 | flag.Visit(func(f *flag.Flag) { |
| @@ -389,26 +389,26 @@ func assembleMCP(e *engine.Engine, d *detector.LoopDetector, set *toolSet) { | ||
| 389 | 389 | // sbx injects MCP_GATEWAY_URL and handles OAuth tokens via its gateway |
| 390 | 390 | // The gateway substitutes sentinel tokens with real OAuth tokens |
| 391 | 391 | gatewayURL := os.Getenv("MCP_GATEWAY_URL") |
| 392 | - | |
| 392 | + | |
| 393 | 393 | servers := config.Cfg.MCP.Servers |
| 394 | - | |
| 394 | + | |
| 395 | 395 | // If gateway URL is set and no servers configured, use gateway only |
| 396 | 396 | // If gateway URL is set and servers configured, add gateway as additional server |
| 397 | 397 | if gatewayURL != "" { |
| 398 | 398 | gatewayServer := config.MCPServer{ |
| 399 | - Name: "sbx-gateway", | |
| 399 | + Name: "sbx-gateway", | |
| 400 | 400 | Transport: "http", |
| 401 | - URL: gatewayURL, | |
| 402 | - Timeout: config.Cfg.MCP.DiscoveryTimeout, | |
| 401 | + URL: gatewayURL, | |
| 402 | + Timeout: config.Cfg.MCP.DiscoveryTimeout, | |
| 403 | 403 | Auth: config.MCPAuthConfig{ |
| 404 | - Type: "bearer", | |
| 404 | + Type: "bearer", | |
| 405 | 405 | TokenEnv: "MCP_SENTINEL_TOKEN_NAME", // sbx injects this, not MCP_GATEWAY_TOKEN |
| 406 | 406 | }, |
| 407 | 407 | Headers: map[string]string{ |
| 408 | 408 | "Accept": "application/json, text/event-stream", |
| 409 | 409 | }, |
| 410 | 410 | } |
| 411 | - | |
| 411 | + | |
| 412 | 412 | // If no servers configured, use gateway only |
| 413 | 413 | if len(servers) == 0 { |
| 414 | 414 | servers = []config.MCPServer{gatewayServer} |
| @@ -537,6 +537,7 @@ func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet | ||
| 537 | 537 | info.Warnings = append(info.Warnings, |
| 538 | 538 | "no tool is declared (bashTool and editTools are both off, and there are no skills): the model can only answer, not act") |
| 539 | 539 | } |
| 540 | + info.Warnings = append(info.Warnings, config.Deprecations()...) | |
| 540 | 541 | for _, w := range info.Warnings { |
| 541 | 542 | say("%s[warning: %s]%s", dim, w, off) |
| 542 | 543 | } |
| @@ -547,8 +548,17 @@ func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet | ||
| 547 | 548 | // The version leads the line: it is the first thing anyone is asked for |
| 548 | 549 | // when something behaves oddly, and the banner is the one output every |
| 549 | 550 | // front end shows — the window keeps it as the conversation's first line. |
| 550 | - say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d%s", | |
| 551 | - dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, off) | |
| 551 | + say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d | %s%s", | |
| 552 | + dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, yoloColumn(), off) | |
| 553 | +} | |
| 554 | + | |
| 555 | +// yoloColumn says in the banner whether the tools run unasked — the one | |
| 556 | +// setting nobody should discover by surprise, in either direction. | |
| 557 | +func yoloColumn() string { | |
| 558 | + if config.Cfg.Yolo { | |
| 559 | + return "yolo: on" | |
| 560 | + } | |
| 561 | + return "yolo: off (tools ask)" | |
| 552 | 562 | } |
| 553 | 563 | |
| 554 | 564 | // commandNames is the slash commands' names, for the shadowing check. |
| @@ -70,7 +70,7 @@ func main() { | |||
| 70 | // | 70 | // |
| 71 | // The flag package gives us "-h" for free; a second argument is a typo, and | 71 | // The flag package gives us "-h" for free; a second argument is a typo, and |
| 72 | // saying so is better than ignoring it. | 72 | // saying so is better than ignoring it. |
| 73 | - | 73 | + |
| 74 | // Handle --prompt/-p flag which can appear after the config file argument | 74 | // Handle --prompt/-p flag which can appear after the config file argument |
| 75 | // We manually extract it before flag.Parse() so both orders work: | 75 | // We manually extract it before flag.Parse() so both orders work: |
| 76 | // mm --prompt "hello" agent.yaml AND mm agent.yaml --prompt "hello" | 76 | // mm --prompt "hello" agent.yaml AND mm agent.yaml --prompt "hello" |
| @@ -100,7 +100,7 @@ func main() { | |||
| 100 | } | 100 | } |
| 101 | // Replace os.Args for flag.Parse() | 101 | // Replace os.Args for flag.Parse() |
| 102 | os.Args = append([]string{os.Args[0]}, remainingArgs...) | 102 | os.Args = append([]string{os.Args[0]}, remainingArgs...) |
| 103 | - | 103 | + |
| 104 | configPath := flag.String("config", "", "path to the YAML config file (default: $AGENT_CONFIG, then ./agent.yaml)") | 104 | configPath := flag.String("config", "", "path to the YAML config file (default: $AGENT_CONFIG, then ./agent.yaml)") |
| 105 | acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL") | 105 | acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL") |
| 106 | tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal") | 106 | tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal") |
| @@ -111,7 +111,7 @@ func main() { | |||
| 111 | askAgent := flag.String("ask", "", "ask the agent `name` (a key of mmConfigs) the -p prompt, or stdin; print only its answer on stdout, and exit") | 111 | askAgent := flag.String("ask", "", "ask the agent `name` (a key of mmConfigs) the -p prompt, or stdin; print only its answer on stdout, and exit") |
| 112 | askWithContext := flag.String("ask-with-context", "", "same as --ask, with the conversation read from stdin (question with -p, or after a \"---\" line)") | 112 | askWithContext := flag.String("ask-with-context", "", "same as --ask, with the conversation read from stdin (question with -p, or after a \"---\" line)") |
| 113 | flag.Parse() | 113 | flag.Parse() |
| 114 | - | 114 | + |
| 115 | // If prompt was extracted manually, use it (overrides flag value) | 115 | // If prompt was extracted manually, use it (overrides flag value) |
| 116 | if promptArg != "" { | 116 | if promptArg != "" { |
| 117 | *promptMode = promptArg | 117 | *promptMode = promptArg |
| @@ -122,7 +122,7 @@ func main() { | |||
| 122 | if askContextArg != "" { | 122 | if askContextArg != "" { |
| 123 | *askWithContext = askContextArg | 123 | *askWithContext = askContextArg |
| 124 | } | 124 | } |
| 125 | - | 125 | + |
| 126 | // Track if --config was explicitly set via the flag | 126 | // Track if --config was explicitly set via the flag |
| 127 | configExplicit := false | 127 | configExplicit := false |
| 128 | flag.Visit(func(f *flag.Flag) { | 128 | flag.Visit(func(f *flag.Flag) { |
| @@ -389,26 +389,26 @@ func assembleMCP(e *engine.Engine, d *detector.LoopDetector, set *toolSet) { | |||
| 389 | // sbx injects MCP_GATEWAY_URL and handles OAuth tokens via its gateway | 389 | // sbx injects MCP_GATEWAY_URL and handles OAuth tokens via its gateway |
| 390 | // The gateway substitutes sentinel tokens with real OAuth tokens | 390 | // The gateway substitutes sentinel tokens with real OAuth tokens |
| 391 | gatewayURL := os.Getenv("MCP_GATEWAY_URL") | 391 | gatewayURL := os.Getenv("MCP_GATEWAY_URL") |
| 392 | - | 392 | + |
| 393 | servers := config.Cfg.MCP.Servers | 393 | servers := config.Cfg.MCP.Servers |
| 394 | - | 394 | + |
| 395 | // If gateway URL is set and no servers configured, use gateway only | 395 | // If gateway URL is set and no servers configured, use gateway only |
| 396 | // If gateway URL is set and servers configured, add gateway as additional server | 396 | // If gateway URL is set and servers configured, add gateway as additional server |
| 397 | if gatewayURL != "" { | 397 | if gatewayURL != "" { |
| 398 | gatewayServer := config.MCPServer{ | 398 | gatewayServer := config.MCPServer{ |
| 399 | - Name: "sbx-gateway", | 399 | + Name: "sbx-gateway", |
| 400 | Transport: "http", | 400 | Transport: "http", |
| 401 | - URL: gatewayURL, | 401 | + URL: gatewayURL, |
| 402 | - Timeout: config.Cfg.MCP.DiscoveryTimeout, | 402 | + Timeout: config.Cfg.MCP.DiscoveryTimeout, |
| 403 | Auth: config.MCPAuthConfig{ | 403 | Auth: config.MCPAuthConfig{ |
| 404 | - Type: "bearer", | 404 | + Type: "bearer", |
| 405 | TokenEnv: "MCP_SENTINEL_TOKEN_NAME", // sbx injects this, not MCP_GATEWAY_TOKEN | 405 | TokenEnv: "MCP_SENTINEL_TOKEN_NAME", // sbx injects this, not MCP_GATEWAY_TOKEN |
| 406 | }, | 406 | }, |
| 407 | Headers: map[string]string{ | 407 | Headers: map[string]string{ |
| 408 | "Accept": "application/json, text/event-stream", | 408 | "Accept": "application/json, text/event-stream", |
| 409 | }, | 409 | }, |
| 410 | } | 410 | } |
| 411 | - | 411 | + |
| 412 | // If no servers configured, use gateway only | 412 | // If no servers configured, use gateway only |
| 413 | if len(servers) == 0 { | 413 | if len(servers) == 0 { |
| 414 | servers = []config.MCPServer{gatewayServer} | 414 | servers = []config.MCPServer{gatewayServer} |
| @@ -537,6 +537,7 @@ func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet | |||
| 537 | info.Warnings = append(info.Warnings, | 537 | info.Warnings = append(info.Warnings, |
| 538 | "no tool is declared (bashTool and editTools are both off, and there are no skills): the model can only answer, not act") | 538 | "no tool is declared (bashTool and editTools are both off, and there are no skills): the model can only answer, not act") |
| 539 | } | 539 | } |
| 540 | + info.Warnings = append(info.Warnings, config.Deprecations()...) | ||
| 540 | for _, w := range info.Warnings { | 541 | for _, w := range info.Warnings { |
| 541 | say("%s[warning: %s]%s", dim, w, off) | 542 | say("%s[warning: %s]%s", dim, w, off) |
| 542 | } | 543 | } |
| @@ -547,8 +548,17 @@ func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet | |||
| 547 | // The version leads the line: it is the first thing anyone is asked for | 548 | // The version leads the line: it is the first thing anyone is asked for |
| 548 | // when something behaves oddly, and the banner is the one output every | 549 | // when something behaves oddly, and the banner is the one output every |
| 549 | // front end shows — the window keeps it as the conversation's first line. | 550 | // front end shows — the window keeps it as the conversation's first line. |
| 550 | - say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d%s", | 551 | + say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d | %s%s", |
| 551 | - dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, off) | 552 | + dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, yoloColumn(), off) |
| 553 | +} | ||
| 554 | + | ||
| 555 | +// yoloColumn says in the banner whether the tools run unasked — the one | ||
| 556 | +// setting nobody should discover by surprise, in either direction. | ||
| 557 | +func yoloColumn() string { | ||
| 558 | + if config.Cfg.Yolo { | ||
| 559 | + return "yolo: on" | ||
| 560 | + } | ||
| 561 | + return "yolo: off (tools ask)" | ||
| 552 | } | 562 | } |
| 553 | 563 | ||
| 554 | // commandNames is the slash commands' names, for the shadowing check. | 564 | // commandNames is the slash commands' names, for the shadowing check. |
added
main_test.go +22 -0 | new file mode 100644 | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "testing" | |
| 5 | + | |
| 6 | + "mm/internal/config" | |
| 7 | +) | |
| 8 | + | |
| 9 | +// The banner says which way yolo is set, both ways. | |
| 10 | +func TestYoloColumn(t *testing.T) { | |
| 11 | + old := config.Cfg.Yolo | |
| 12 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | |
| 13 | + | |
| 14 | + config.Cfg.Yolo = true | |
| 15 | + if got := yoloColumn(); got != "yolo: on" { | |
| 16 | + t.Errorf("yolo on: %q", got) | |
| 17 | + } | |
| 18 | + config.Cfg.Yolo = false | |
| 19 | + if got := yoloColumn(); got != "yolo: off (tools ask)" { | |
| 20 | + t.Errorf("yolo off: %q", got) | |
| 21 | + } | |
| 22 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | +package main | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "testing" | ||
| 5 | + | ||
| 6 | + "mm/internal/config" | ||
| 7 | +) | ||
| 8 | + | ||
| 9 | +// The banner says which way yolo is set, both ways. | ||
| 10 | +func TestYoloColumn(t *testing.T) { | ||
| 11 | + old := config.Cfg.Yolo | ||
| 12 | + t.Cleanup(func() { config.Cfg.Yolo = old }) | ||
| 13 | + | ||
| 14 | + config.Cfg.Yolo = true | ||
| 15 | + if got := yoloColumn(); got != "yolo: on" { | ||
| 16 | + t.Errorf("yolo on: %q", got) | ||
| 17 | + } | ||
| 18 | + config.Cfg.Yolo = false | ||
| 19 | + if got := yoloColumn(); got != "yolo: off (tools ask)" { | ||
| 20 | + t.Errorf("yolo off: %q", got) | ||
| 21 | + } | ||
| 22 | +} | ||