@oh-my-pi/pi-coding-agent 15.4.3 → 15.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +81 -5
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/auth-broker-cli.d.ts +1 -1
  4. package/dist/types/commands/launch.d.ts +8 -0
  5. package/dist/types/config/settings-schema.d.ts +42 -1
  6. package/dist/types/edit/index.d.ts +2 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
  8. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  9. package/dist/types/hashline/executor.d.ts +6 -3
  10. package/dist/types/lsp/index.d.ts +9 -1
  11. package/dist/types/mcp/client.d.ts +2 -1
  12. package/dist/types/mcp/oauth-discovery.d.ts +4 -3
  13. package/dist/types/mcp/timeout.d.ts +9 -0
  14. package/dist/types/mcp/types.d.ts +1 -1
  15. package/dist/types/sdk.d.ts +2 -0
  16. package/dist/types/session/streaming-output.d.ts +1 -1
  17. package/dist/types/task/index.d.ts +2 -0
  18. package/dist/types/task/types.d.ts +4 -0
  19. package/dist/types/tools/approval.d.ts +46 -0
  20. package/dist/types/tools/ask.d.ts +1 -0
  21. package/dist/types/tools/ast-edit.d.ts +2 -0
  22. package/dist/types/tools/ast-grep.d.ts +1 -0
  23. package/dist/types/tools/bash.d.ts +11 -1
  24. package/dist/types/tools/browser.d.ts +2 -0
  25. package/dist/types/tools/calculator.d.ts +1 -0
  26. package/dist/types/tools/checkpoint.d.ts +2 -0
  27. package/dist/types/tools/debug.d.ts +9 -1
  28. package/dist/types/tools/eval.d.ts +2 -0
  29. package/dist/types/tools/find.d.ts +10 -0
  30. package/dist/types/tools/gh.d.ts +2 -1
  31. package/dist/types/tools/hindsight-recall.d.ts +1 -0
  32. package/dist/types/tools/hindsight-reflect.d.ts +1 -0
  33. package/dist/types/tools/hindsight-retain.d.ts +1 -0
  34. package/dist/types/tools/inspect-image.d.ts +1 -0
  35. package/dist/types/tools/irc.d.ts +1 -0
  36. package/dist/types/tools/job.d.ts +1 -0
  37. package/dist/types/tools/read.d.ts +1 -0
  38. package/dist/types/tools/recipe/index.d.ts +1 -0
  39. package/dist/types/tools/render-mermaid.d.ts +1 -0
  40. package/dist/types/tools/resolve.d.ts +1 -0
  41. package/dist/types/tools/search-tool-bm25.d.ts +1 -0
  42. package/dist/types/tools/search.d.ts +1 -0
  43. package/dist/types/tools/ssh.d.ts +2 -0
  44. package/dist/types/tools/todo-write.d.ts +1 -0
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/tools/yield.d.ts +1 -0
  47. package/dist/types/web/search/index.d.ts +1 -0
  48. package/package.json +7 -7
  49. package/src/cli/args.ts +14 -0
  50. package/src/cli/auth-broker-cli.ts +171 -22
  51. package/src/commands/auth-broker.ts +3 -0
  52. package/src/commands/launch.ts +16 -0
  53. package/src/config/mcp-schema.json +2 -2
  54. package/src/config/model-registry.ts +19 -4
  55. package/src/config/prompt-templates.ts +0 -125
  56. package/src/config/settings-schema.ts +59 -1
  57. package/src/config/settings.ts +2 -1
  58. package/src/dap/session.ts +35 -2
  59. package/src/discovery/builtin.ts +2 -2
  60. package/src/discovery/mcp-json.ts +1 -1
  61. package/src/edit/index.ts +26 -0
  62. package/src/edit/modes/patch.ts +1 -1
  63. package/src/edit/streaming.ts +12 -2
  64. package/src/exec/bash-executor.ts +6 -2
  65. package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
  66. package/src/extensibility/custom-tools/types.ts +16 -2
  67. package/src/extensibility/extensions/wrapper.ts +36 -1
  68. package/src/extensibility/hooks/types.ts +8 -1
  69. package/src/hashline/apply.ts +47 -2
  70. package/src/hashline/executor.ts +46 -24
  71. package/src/internal-urls/docs-index.generated.ts +8 -7
  72. package/src/lsp/edits.ts +82 -29
  73. package/src/lsp/index.ts +38 -1
  74. package/src/lsp/utils.ts +1 -1
  75. package/src/main.ts +6 -0
  76. package/src/mcp/client.ts +8 -6
  77. package/src/mcp/oauth-discovery.ts +120 -32
  78. package/src/mcp/oauth-flow.ts +34 -6
  79. package/src/mcp/timeout.ts +59 -0
  80. package/src/mcp/transports/http.ts +42 -44
  81. package/src/mcp/transports/stdio.ts +8 -5
  82. package/src/mcp/types.ts +1 -1
  83. package/src/modes/components/hook-editor.ts +11 -3
  84. package/src/modes/components/mcp-add-wizard.ts +6 -2
  85. package/src/modes/components/model-selector.ts +33 -11
  86. package/src/modes/controllers/command-controller.ts +6 -4
  87. package/src/modes/controllers/mcp-command-controller.ts +8 -4
  88. package/src/prompts/review-custom-request.md +22 -0
  89. package/src/prompts/review-headless-request.md +16 -0
  90. package/src/prompts/review-request.md +2 -3
  91. package/src/prompts/system/project-prompt.md +4 -0
  92. package/src/prompts/tools/debug.md +1 -0
  93. package/src/prompts/tools/find.md +4 -2
  94. package/src/prompts/tools/hashline.md +43 -93
  95. package/src/sdk.ts +47 -73
  96. package/src/session/agent-session.ts +93 -27
  97. package/src/session/streaming-output.ts +1 -1
  98. package/src/slash-commands/helpers/usage-report.ts +3 -1
  99. package/src/task/executor.ts +11 -0
  100. package/src/task/index.ts +19 -0
  101. package/src/task/render.ts +12 -2
  102. package/src/task/types.ts +4 -0
  103. package/src/tools/approval.ts +185 -0
  104. package/src/tools/ask.ts +1 -0
  105. package/src/tools/ast-edit.ts +25 -1
  106. package/src/tools/ast-grep.ts +1 -0
  107. package/src/tools/bash.ts +69 -1
  108. package/src/tools/browser/tab-supervisor.ts +1 -1
  109. package/src/tools/browser.ts +15 -0
  110. package/src/tools/calculator.ts +1 -0
  111. package/src/tools/checkpoint.ts +2 -0
  112. package/src/tools/debug.ts +38 -0
  113. package/src/tools/eval.ts +15 -0
  114. package/src/tools/find.ts +17 -8
  115. package/src/tools/gh.ts +21 -1
  116. package/src/tools/hindsight-recall.ts +1 -0
  117. package/src/tools/hindsight-reflect.ts +1 -0
  118. package/src/tools/hindsight-retain.ts +1 -0
  119. package/src/tools/image-gen.ts +1 -0
  120. package/src/tools/inspect-image.ts +1 -0
  121. package/src/tools/irc.ts +1 -0
  122. package/src/tools/job.ts +1 -0
  123. package/src/tools/path-utils.ts +14 -1
  124. package/src/tools/read.ts +1 -0
  125. package/src/tools/recipe/index.ts +1 -0
  126. package/src/tools/render-mermaid.ts +1 -0
  127. package/src/tools/report-tool-issue.ts +1 -0
  128. package/src/tools/resolve.ts +1 -0
  129. package/src/tools/review.ts +1 -0
  130. package/src/tools/search-tool-bm25.ts +1 -0
  131. package/src/tools/search.ts +1 -0
  132. package/src/tools/ssh.ts +8 -0
  133. package/src/tools/todo-write.ts +1 -0
  134. package/src/tools/write.ts +12 -1
  135. package/src/tools/yield.ts +1 -0
  136. package/src/web/search/index.ts +2 -0
package/src/tools/find.ts CHANGED
@@ -59,11 +59,15 @@ const MAX_GLOB_TIMEOUT_MS = 60_000;
59
59
  * Commas inside brace expansion (`{a,b}`) are legitimate glob syntax and
60
60
  * must pass through.
61
61
  */
62
- function validateFindPathInputs(paths: readonly string[]): void {
62
+ export function validateFindPathInputs(paths: readonly string[]): void {
63
63
  for (const entry of paths) {
64
64
  let braceDepth = 0;
65
65
  for (let i = 0; i < entry.length; i++) {
66
66
  const ch = entry.charCodeAt(i);
67
+ if (ch === 0x5c /* \ */ && i + 1 < entry.length) {
68
+ i++;
69
+ continue;
70
+ }
67
71
  if (ch === 0x7b /* { */) braceDepth++;
68
72
  else if (ch === 0x7d /* } */) {
69
73
  if (braceDepth > 0) braceDepth--;
@@ -115,6 +119,7 @@ export interface FindToolOptions {
115
119
 
116
120
  export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
117
121
  readonly name = "find";
122
+ readonly approval = "read" as const;
118
123
  readonly summary = "Find files and directories matching a glob pattern";
119
124
  readonly loadMode = "discoverable";
120
125
  readonly label = "Find";
@@ -304,6 +309,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
304
309
 
305
310
  let matches: natives.GlobMatch[];
306
311
  const onUpdateMatches: string[] = [];
312
+ const onUpdateMtimes: number[] = [];
307
313
  const updateIntervalMs = 200;
308
314
  let lastUpdate = 0;
309
315
  const emitUpdate = () => {
@@ -323,9 +329,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
323
329
  });
324
330
  };
325
331
  const onMatch = (err: Error | null, match: natives.GlobMatch | null) => {
326
- if (err || signal?.aborted || !match?.path) return;
332
+ if (err || combinedSignal.aborted || !match?.path) return;
327
333
  const relativePath = formatMatchPath(match.path, match.fileType);
328
334
  onUpdateMatches.push(relativePath);
335
+ onUpdateMtimes.push(match.mtime ?? 0);
329
336
  emitUpdate();
330
337
  };
331
338
 
@@ -335,7 +342,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
335
342
  {
336
343
  pattern: globPattern,
337
344
  path: searchPath,
338
- fileType: natives.FileType.File,
339
345
  hidden: includeHidden,
340
346
  maxResults: effectiveLimit,
341
347
  sortByMtime: true,
@@ -371,15 +377,18 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
371
377
  // instead of throwing — empty results after a multi-second wait force the
372
378
  // caller to retry blind, which is the worst possible outcome.
373
379
  const seen = new Set<string>();
374
- const partial: string[] = [];
375
- for (const entry of onUpdateMatches) {
380
+ const partial: Array<{ p: string; m: number }> = [];
381
+ for (let i = 0; i < onUpdateMatches.length; i++) {
382
+ const entry = onUpdateMatches[i];
376
383
  if (seen.has(entry)) continue;
377
384
  seen.add(entry);
378
- partial.push(entry);
385
+ partial.push({ p: entry, m: onUpdateMtimes[i] ?? 0 });
379
386
  }
387
+ partial.sort((a, b) => b.m - a.m);
388
+ const sortedPaths = partial.map(e => e.p);
380
389
  const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
381
- const notice = `find timed out after ${seconds}s; returning ${partial.length} partial matches — increase timeout or narrow pattern`;
382
- return buildResult(partial, { notice, forceTruncated: true });
390
+ const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — increase timeout or narrow pattern`;
391
+ return buildResult(sortedPaths, { notice, forceTruncated: true });
383
392
  }
384
393
 
385
394
  const relativized: string[] = [];
package/src/tools/gh.ts CHANGED
@@ -2,7 +2,13 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { scheduler } from "node:timers/promises";
5
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
+ import type {
6
+ AgentTool,
7
+ AgentToolContext,
8
+ AgentToolResult,
9
+ AgentToolUpdateCallback,
10
+ ToolApprovalDecision,
11
+ } from "@oh-my-pi/pi-agent-core";
6
12
 
7
13
  import { getWorktreeDir, hashPath, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
8
14
  import * as z from "zod/v4";
@@ -196,6 +202,15 @@ const RUN_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/actions\/runs\/
196
202
  const RUN_SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
197
203
  const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required", "startup_failure"]);
198
204
  const JOB_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required"]);
205
+ const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
206
+ "repo_view",
207
+ "search_issues",
208
+ "search_prs",
209
+ "search_code",
210
+ "search_commits",
211
+ "search_repos",
212
+ "run_watch",
213
+ ]);
199
214
 
200
215
  const githubSchema = z
201
216
  .object({
@@ -2344,6 +2359,11 @@ function buildTextResult(
2344
2359
 
2345
2360
  export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails> {
2346
2361
  readonly name = "github";
2362
+ readonly approval = (args: unknown): ToolApprovalDecision => {
2363
+ const rawOp = (args as Partial<GithubInput>).op;
2364
+ const op = typeof rawOp === "string" ? rawOp : "";
2365
+ return GITHUB_READONLY_OPS.has(op) ? "read" : "exec";
2366
+ };
2347
2367
  readonly summary = "Interact with GitHub issues, pull requests, and repositories";
2348
2368
  readonly loadMode = "discoverable";
2349
2369
  readonly label = "GitHub";
@@ -13,6 +13,7 @@ export type HindsightRecallParams = z.infer<typeof hindsightRecallSchema>;
13
13
 
14
14
  export class HindsightRecallTool implements AgentTool<typeof hindsightRecallSchema> {
15
15
  readonly name = "recall";
16
+ readonly approval = "read" as const;
16
17
  readonly label = "Recall";
17
18
  readonly description = recallDescription;
18
19
  readonly parameters = hindsightRecallSchema;
@@ -14,6 +14,7 @@ export type HindsightReflectParams = z.infer<typeof hindsightReflectSchema>;
14
14
 
15
15
  export class HindsightReflectTool implements AgentTool<typeof hindsightReflectSchema> {
16
16
  readonly name = "reflect";
17
+ readonly approval = "read" as const;
17
18
  readonly label = "Reflect";
18
19
  readonly description = reflectDescription;
19
20
  readonly parameters = hindsightReflectSchema;
@@ -18,6 +18,7 @@ const hindsightRetainSchema = z.object({
18
18
  export type HindsightRetainParams = z.infer<typeof hindsightRetainSchema>;
19
19
  export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSchema> {
20
20
  readonly name = "retain";
21
+ readonly approval = "read" as const;
21
22
  readonly label = "Retain";
22
23
  readonly description = retainDescription;
23
24
  readonly parameters = hindsightRetainSchema;
@@ -901,6 +901,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
901
901
  name: "generate_image",
902
902
  label: "GenerateImage",
903
903
  strict: false,
904
+ approval: "write",
904
905
  description: prompt.render(imageGenDescription),
905
906
  parameters: imageGenSchema,
906
907
  async execute(_toolCallId, params, _onUpdate, ctx, signal) {
@@ -33,6 +33,7 @@ export interface InspectImageToolDetails {
33
33
 
34
34
  export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
35
35
  readonly name = "inspect_image";
36
+ readonly approval = "read" as const;
36
37
  readonly label = "InspectImage";
37
38
  readonly loadMode = "discoverable";
38
39
  readonly summary = "Describe or analyze an image file";
package/src/tools/irc.ts CHANGED
@@ -54,6 +54,7 @@ export interface IrcDetails {
54
54
 
55
55
  export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
56
56
  readonly name = "irc";
57
+ readonly approval = "read" as const;
57
58
  readonly label = "IRC";
58
59
  readonly summary = "Send and receive messages between agents over IRC-like channels";
59
60
  readonly description: string;
package/src/tools/job.ts CHANGED
@@ -67,6 +67,7 @@ export interface JobToolDetails {
67
67
 
68
68
  export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
69
69
  readonly name = "job";
70
+ readonly approval = "read" as const;
70
71
  readonly label = "Job";
71
72
  readonly summary = "Manage long-running background jobs (async bash/python)";
72
73
  readonly description: string;
@@ -29,6 +29,13 @@ const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
29
29
  rule: true,
30
30
  skill: true,
31
31
  };
32
+ // Schemes whose resource URIs are server-defined and may legitimately end
33
+ // with selector-shaped tails (e.g. `:raw`, `:conflicts`, `:1-50`, `/:raw`).
34
+ // `McpProtocolHandler` resolves by exact URI match (`r.uri === uri`), so
35
+ // peeling syntactically can make valid resources unreachable. Keep these
36
+ // schemes opaque; selector support for them needs a resolver-aware path that
37
+ // tries the exact URI before interpreting any suffix as a read selector.
38
+ const OPAQUE_RESOURCE_SCHEMES: ReadonlySet<string> = new Set(["mcp"]);
32
39
  const INTERNAL_URL_SCHEME_RE = /^([a-z][a-z0-9+.-]*):\/\//i;
33
40
  const NARROW_NO_BREAK_SPACE = "\u202F";
34
41
  const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
@@ -173,10 +180,16 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
173
180
  *
174
181
  * Falls back to the input unchanged when nothing matches.
175
182
  */
183
+
176
184
  export function splitInternalUrlSel(rawPath: string): { path: string; sel?: string } {
177
185
  const schemeMatch = rawPath.match(INTERNAL_URL_SCHEME_RE);
178
186
  if (!schemeMatch) return { path: rawPath };
179
- if (!INTERNAL_SCHEMES_WITH_SELECTORS[schemeMatch[1].toLowerCase()]) return { path: rawPath };
187
+ const scheme = schemeMatch[1].toLowerCase();
188
+ // Opaque schemes (mcp://, etc.) carry server-defined resource URIs that may
189
+ // legitimately end in selector-shaped tails. Forward verbatim — see
190
+ // OPAQUE_RESOURCE_SCHEMES.
191
+ if (OPAQUE_RESOURCE_SCHEMES.has(scheme)) return { path: rawPath };
192
+ if (!INTERNAL_SCHEMES_WITH_SELECTORS[scheme]) return { path: rawPath };
180
193
 
181
194
  const schemeEnd = schemeMatch[0].length;
182
195
  let path = rawPath;
package/src/tools/read.ts CHANGED
@@ -718,6 +718,7 @@ interface ResolvedSqliteReadPath {
718
718
  */
719
719
  export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
720
720
  readonly name = "read";
721
+ readonly approval = "read" as const;
721
722
  readonly label = "Read";
722
723
  readonly loadMode = "essential";
723
724
  readonly description: string;
@@ -27,6 +27,7 @@ type RecipeRenderResult = {
27
27
  export class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetails, Theme> {
28
28
  readonly name = "recipe";
29
29
  readonly label = "Run";
30
+ readonly approval = "exec" as const;
30
31
  readonly description: string;
31
32
  readonly parameters = recipeSchema;
32
33
  readonly strict = true;
@@ -34,6 +34,7 @@ export interface RenderMermaidToolDetails {
34
34
 
35
35
  export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
36
36
  readonly name = "render_mermaid";
37
+ readonly approval = "read" as const;
37
38
  readonly label = "RenderMermaid";
38
39
  readonly summary = "Render a Mermaid diagram to an image";
39
40
  readonly description: string;
@@ -466,6 +466,7 @@ export function createReportToolIssueTool(session: ToolSession, activeBuiltinNam
466
466
  name: "report_tool_issue",
467
467
  label: "Report Tool Issue",
468
468
  strict: false,
469
+ approval: "write",
469
470
  description: "Report unexpected tool behavior for automated QA tracking.",
470
471
  parameters: buildReportToolIssueParams(activeBuiltinNames),
471
472
  intent: "omit",
@@ -160,6 +160,7 @@ export async function runResolveInvocation(
160
160
 
161
161
  export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolDetails> {
162
162
  readonly name = "resolve";
163
+ readonly approval = "read" as const;
163
164
  readonly label = "Resolve";
164
165
  readonly hidden = true;
165
166
  readonly description: string;
@@ -122,6 +122,7 @@ export function parseReportFindingDetails(value: unknown): ReportFindingDetails
122
122
  export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> = {
123
123
  name: "report_finding",
124
124
  label: "Report Finding",
125
+ approval: "read",
125
126
  description: "Report a code review finding. Use this for each issue found. Call yield when done.",
126
127
  parameters: ReportFindingParams,
127
128
  intent: "omit",
@@ -180,6 +180,7 @@ function renderFallbackResult(text: string, theme: Theme): Component {
180
180
  */
181
181
  export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema, SearchToolBm25Details> {
182
182
  readonly name = "search_tool_bm25";
183
+ readonly approval = "read" as const;
183
184
  readonly label = "SearchTools";
184
185
  readonly loadMode = "essential";
185
186
  get description(): string {
@@ -218,6 +218,7 @@ type SearchParams = z.infer<typeof searchSchema>;
218
218
 
219
219
  export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
220
220
  readonly name = "search";
221
+ readonly approval = "read" as const;
221
222
  readonly label = "Search";
222
223
  readonly loadMode = "discoverable";
223
224
  readonly summary = "Search file contents using ripgrep (fast text search)";
package/src/tools/ssh.ts CHANGED
@@ -16,6 +16,7 @@ import { executeSSH } from "../ssh/ssh-executor";
16
16
  import { renderStatusLine } from "../tui";
17
17
  import { CachedOutputBlock } from "../tui/output-block";
18
18
  import type { ToolSession } from ".";
19
+ import { truncateForPrompt } from "./approval";
19
20
  import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
20
21
  import { ToolError } from "./tool-errors";
21
22
  import { toolResult } from "./tool-result";
@@ -120,6 +121,13 @@ type SshToolParams = z.infer<typeof sshSchema>;
120
121
 
121
122
  export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
122
123
  readonly name = "ssh";
124
+ readonly approval = "exec" as const;
125
+ readonly formatApprovalDetails = (args: unknown): string[] => {
126
+ const params = args as Partial<SshToolParams>;
127
+ const host = typeof params.host === "string" ? params.host : "(missing)";
128
+ const command = typeof params.command === "string" ? params.command : "(missing)";
129
+ return [`Host: ${truncateForPrompt(host)}`, `Command: ${truncateForPrompt(command)}`];
130
+ };
123
131
  readonly summary = "Execute a command on a remote host over SSH";
124
132
  readonly loadMode = "discoverable";
125
133
  readonly label = "SSH";
@@ -493,6 +493,7 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
493
493
 
494
494
  export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
495
495
  readonly name = "todo_write";
496
+ readonly approval = "read" as const;
496
497
  readonly label = "Todo Write";
497
498
  readonly summary = "Write a structured todo list to track progress within a session";
498
499
  readonly description: string;
@@ -16,6 +16,7 @@ import writeDescription from "../prompts/tools/write.md" with { type: "text" };
16
16
  import type { ToolSession } from "../sdk";
17
17
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
18
18
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
19
+ import { truncateForPrompt } from "./approval";
19
20
  import { parseArchivePathCandidates } from "./archive-reader";
20
21
  import { assertEditableFile } from "./auto-generated-guard";
21
22
  import {
@@ -27,7 +28,7 @@ import {
27
28
  } from "./conflict-detect";
28
29
  import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
29
30
  import { type OutputMeta, outputMeta } from "./output-meta";
30
- import { formatPathRelativeToCwd } from "./path-utils";
31
+ import { formatPathRelativeToCwd, isInternalUrlPath } from "./path-utils";
31
32
  import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
32
33
  import {
33
34
  formatDiagnostics,
@@ -184,6 +185,16 @@ function parseSqliteWriteTarget(subPath: string, queryString: string): { table:
184
185
  */
185
186
  export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails> {
186
187
  readonly name = "write";
188
+ readonly approval = (args: unknown) => {
189
+ const rawPath = (args as Partial<WriteParams>).path;
190
+ return typeof rawPath === "string" && isInternalUrlPath(rawPath) ? "read" : "write";
191
+ };
192
+ readonly formatApprovalDetails = (args: unknown): string[] => {
193
+ const params = args as Partial<WriteParams>;
194
+ const targetPath = typeof params.path === "string" ? params.path : "(missing)";
195
+ const content = typeof params.content === "string" ? params.content : "";
196
+ return [`Path: ${truncateForPrompt(targetPath)}`, `Content:\n${truncateForPrompt(content)}`];
197
+ };
187
198
  readonly label = "Write";
188
199
  readonly description: string;
189
200
  readonly parameters = writeSchema;
@@ -99,6 +99,7 @@ const MAX_SCHEMA_RETRIES = 3;
99
99
 
100
100
  export class YieldTool implements AgentTool<TSchema, YieldDetails> {
101
101
  readonly name = "yield";
102
+ readonly approval = "read" as const;
102
103
  readonly label = "Submit Result";
103
104
  readonly description =
104
105
  "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
@@ -224,6 +224,7 @@ export async function runSearchQuery(
224
224
  */
225
225
  export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
226
226
  readonly name = "web_search";
227
+ readonly approval = "read" as const;
227
228
  readonly label = "Web Search";
228
229
  readonly description: string;
229
230
  readonly parameters = webSearchSchema;
@@ -258,6 +259,7 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
258
259
  description: prompt.render(webSearchDescription),
259
260
  parameters: webSearchSchema,
260
261
 
262
+ approval: "read",
261
263
  async execute(
262
264
  toolCallId: string,
263
265
  params: SearchToolParams,