@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12

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 (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -0,0 +1,95 @@
1
+ /**
2
+ * HTTP header precedence and redirect-origin policy for remote MCP transports.
3
+ *
4
+ * Two invariants, applied at every transport fetch:
5
+ *
6
+ * 1. Client-generated headers (protocol headers like `Content-Type`, `Accept`,
7
+ * `Mcp-Session-Id`, and authorization) take precedence over configured
8
+ * headers with the same case-insensitive name. Configured headers can never
9
+ * corrupt the MCP wire protocol via casing tricks.
10
+ * 2. Origin-locked servers (Agent Plugins §7.2.1) never forward configured
11
+ * headers to a different origin: redirects are followed manually and
12
+ * configured headers are attached only when the hop targets the configured
13
+ * origin. Method-changing redirects of non-GET requests are refused.
14
+ */
15
+
16
+ /** Header buckets for one MCP HTTP request. */
17
+ interface MCPHeaderSources {
18
+ /** Client-generated HTTP/MCP/authorization headers; win case-insensitively. */
19
+ generated: Record<string, string>;
20
+ /** Configured headers from the server entry (package or user config). */
21
+ configured?: Record<string, string>;
22
+ }
23
+
24
+ /**
25
+ * Merge configured headers under client-generated ones: a configured entry is
26
+ * dropped when a generated header with the same case-insensitive name exists.
27
+ */
28
+ export function mergeMCPHeaders({ generated, configured }: MCPHeaderSources): Record<string, string> {
29
+ if (!configured) return { ...generated };
30
+ const generatedNames = new Set<string>();
31
+ for (const name in generated) generatedNames.add(name.toLowerCase());
32
+ const merged: Record<string, string> = {};
33
+ for (const name in configured) {
34
+ if (!generatedNames.has(name.toLowerCase())) merged[name] = configured[name];
35
+ }
36
+ return { ...merged, ...generated };
37
+ }
38
+
39
+ /**
40
+ * Set a client-generated header, removing any existing entry with the same
41
+ * case-insensitive name so the generated value is the only one sent.
42
+ */
43
+ export function setGeneratedHeader(headers: Record<string, string>, name: string, value: string): void {
44
+ const lower = name.toLowerCase();
45
+ for (const existing in headers) {
46
+ if (existing.toLowerCase() === lower) delete headers[existing];
47
+ }
48
+ headers[name] = value;
49
+ }
50
+
51
+ const REDIRECT_STATUSES: Record<number, true> = { 301: true, 302: true, 303: true, 307: true, 308: true };
52
+ const MAX_REDIRECT_HOPS = 5;
53
+
54
+ export interface MCPFetchInit {
55
+ method: "GET" | "POST" | "DELETE";
56
+ body?: string;
57
+ signal?: AbortSignal;
58
+ }
59
+
60
+ /**
61
+ * Fetch an MCP endpoint with header precedence and, for origin-locked servers,
62
+ * manual redirect handling that strips configured headers on cross-origin hops.
63
+ *
64
+ * Non-locked servers keep the platform default redirect behavior. Locked
65
+ * non-GET requests only follow 307/308 (method-preserving); a 301/302/303
66
+ * redirect of a JSON-RPC POST is a connection error, never a silent GET.
67
+ */
68
+ export async function mcpFetch(
69
+ url: string,
70
+ init: MCPFetchInit,
71
+ sources: MCPHeaderSources,
72
+ originLocked: boolean,
73
+ ): Promise<Response> {
74
+ if (!originLocked) {
75
+ return fetch(url, { ...init, headers: mergeMCPHeaders(sources) });
76
+ }
77
+
78
+ const configuredOrigin = new URL(url).origin;
79
+ let currentUrl = url;
80
+ for (let hop = 0; hop <= MAX_REDIRECT_HOPS; hop++) {
81
+ const attachConfigured = new URL(currentUrl).origin === configuredOrigin;
82
+ const headers = mergeMCPHeaders(attachConfigured ? sources : { generated: sources.generated });
83
+ const response = await fetch(currentUrl, { ...init, headers, redirect: "manual" });
84
+ if (!REDIRECT_STATUSES[response.status]) return response;
85
+
86
+ const location = response.headers.get("Location");
87
+ if (!location) return response;
88
+ await response.body?.cancel();
89
+ if (init.method !== "GET" && response.status !== 307 && response.status !== 308) {
90
+ throw new Error(`HTTP ${response.status}: server redirected a ${init.method} request; refusing to follow`);
91
+ }
92
+ currentUrl = new URL(location, currentUrl).href;
93
+ }
94
+ throw new Error(`Too many redirects (> ${MAX_REDIRECT_HOPS}) fetching ${url}`);
95
+ }
@@ -19,6 +19,7 @@ import type {
19
19
  import { toJsonRpcError } from "../../mcp/types";
20
20
  import { RequestIdAllocator } from "../request-id";
21
21
  import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
22
+ import { type MCPFetchInit, mcpFetch } from "./header-policy";
22
23
 
23
24
  const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
24
25
  /**
@@ -54,6 +55,16 @@ export class HttpTransport implements MCPTransport {
54
55
 
55
56
  constructor(private config: MCPHttpServerConfig | MCPSseServerConfig) {}
56
57
 
58
+ /** Fetch the configured endpoint with header precedence and origin policy. */
59
+ #fetch(init: MCPFetchInit, generated: Record<string, string>): Promise<Response> {
60
+ return mcpFetch(
61
+ this.config.url,
62
+ init,
63
+ { generated, configured: this.config.headers },
64
+ this.config.headerPolicy === "origin-locked",
65
+ );
66
+ }
67
+
57
68
  get connected(): boolean {
58
69
  return this.#connected;
59
70
  }
@@ -81,13 +92,12 @@ export class HttpTransport implements MCPTransport {
81
92
  if (this.#sseConnection) return;
82
93
 
83
94
  this.#sseConnection = new AbortController();
84
- const headers: Record<string, string> = {
95
+ const generated: Record<string, string> = {
85
96
  Accept: "text/event-stream",
86
- ...this.config.headers,
87
97
  };
88
98
 
89
99
  if (this.#sessionId) {
90
- headers["Mcp-Session-Id"] = this.#sessionId;
100
+ generated["Mcp-Session-Id"] = this.#sessionId;
91
101
  }
92
102
 
93
103
  let response: Response | null;
@@ -95,11 +105,7 @@ export class HttpTransport implements MCPTransport {
95
105
  let startupFinished = false;
96
106
  const connection = this.#sseConnection;
97
107
  const startupTimeoutMs = resolveSSEConnectTimeoutMs(this.config.timeout);
98
- const fetchPromise = fetch(this.config.url, {
99
- method: "GET",
100
- headers,
101
- signal: connection.signal,
102
- });
108
+ const fetchPromise = this.#fetch({ method: "GET", signal: connection.signal }, generated);
103
109
  const timeoutPromise =
104
110
  startupTimeoutMs > 0
105
111
  ? new Promise<null>(resolve => {
@@ -219,26 +225,23 @@ export class HttpTransport implements MCPTransport {
219
225
  params: params ?? {},
220
226
  };
221
227
 
222
- const headers: Record<string, string> = {
228
+ const generated: Record<string, string> = {
223
229
  "Content-Type": "application/json",
224
230
  Accept: "application/json, text/event-stream",
225
- ...this.config.headers,
226
231
  };
227
232
 
228
233
  if (this.#sessionId) {
229
- headers["Mcp-Session-Id"] = this.#sessionId;
234
+ generated["Mcp-Session-Id"] = this.#sessionId;
230
235
  }
231
236
 
232
237
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
233
238
  const operation = createMCPTimeout(timeout, options?.signal);
234
239
 
235
240
  try {
236
- const response = await fetch(this.config.url, {
237
- method: "POST",
238
- headers,
239
- body: JSON.stringify(body),
240
- signal: operation.signal,
241
- });
241
+ const response = await this.#fetch(
242
+ { method: "POST", body: JSON.stringify(body), signal: operation.signal },
243
+ generated,
244
+ );
242
245
 
243
246
  // Check for session ID in response
244
247
  const newSessionId = response.headers.get("Mcp-Session-Id");
@@ -365,23 +368,18 @@ export class HttpTransport implements MCPTransport {
365
368
  const body = error
366
369
  ? { jsonrpc: "2.0" as const, id, error }
367
370
  : { jsonrpc: "2.0" as const, id, result: result ?? {} };
368
- const headers: Record<string, string> = {
371
+ const generated: Record<string, string> = {
369
372
  "Content-Type": "application/json",
370
373
  Accept: "application/json, text/event-stream",
371
- ...this.config.headers,
372
374
  };
373
375
  if (this.#sessionId) {
374
- headers["Mcp-Session-Id"] = this.#sessionId;
376
+ generated["Mcp-Session-Id"] = this.#sessionId;
375
377
  }
378
+ const payload = JSON.stringify(body);
376
379
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
377
380
  const operation = createMCPTimeout(timeout);
378
381
  try {
379
- const resp = await fetch(this.config.url, {
380
- method: "POST",
381
- headers,
382
- body: JSON.stringify(body),
383
- signal: operation.signal,
384
- });
382
+ const resp = await this.#fetch({ method: "POST", body: payload, signal: operation.signal }, generated);
385
383
  // Retry once on auth failure if onAuthError is wired
386
384
  if (this.onAuthError && (resp.status === 401 || resp.status === 403)) {
387
385
  await resp.body?.cancel();
@@ -389,16 +387,13 @@ export class HttpTransport implements MCPTransport {
389
387
  if (newHeaders) {
390
388
  this.config.headers ??= {};
391
389
  Object.assign(this.config.headers, newHeaders);
392
- Object.assign(headers, newHeaders);
393
390
  operation.clear();
394
391
  const retryOperation = createMCPTimeout(timeout);
395
392
  try {
396
- const retry = await fetch(this.config.url, {
397
- method: "POST",
398
- headers,
399
- body: JSON.stringify(body),
400
- signal: retryOperation.signal,
401
- });
393
+ const retry = await this.#fetch(
394
+ { method: "POST", body: payload, signal: retryOperation.signal },
395
+ generated,
396
+ );
402
397
  await retry.body?.cancel();
403
398
  } finally {
404
399
  retryOperation.clear();
@@ -425,26 +420,23 @@ export class HttpTransport implements MCPTransport {
425
420
  params: params ?? {},
426
421
  };
427
422
 
428
- const headers: Record<string, string> = {
423
+ const generated: Record<string, string> = {
429
424
  "Content-Type": "application/json",
430
425
  Accept: "application/json, text/event-stream",
431
- ...this.config.headers,
432
426
  };
433
427
 
434
428
  if (this.#sessionId) {
435
- headers["Mcp-Session-Id"] = this.#sessionId;
429
+ generated["Mcp-Session-Id"] = this.#sessionId;
436
430
  }
437
431
 
438
432
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
439
433
  const operation = createMCPTimeout(timeout);
440
434
 
441
435
  try {
442
- const response = await fetch(this.config.url, {
443
- method: "POST",
444
- headers,
445
- body: JSON.stringify(body),
446
- signal: operation.signal,
447
- });
436
+ const response = await this.#fetch(
437
+ { method: "POST", body: JSON.stringify(body), signal: operation.signal },
438
+ generated,
439
+ );
448
440
 
449
441
  // 202 Accepted is success for notifications
450
442
  if (!response.ok && response.status !== 202) {
@@ -492,16 +484,7 @@ export class HttpTransport implements MCPTransport {
492
484
  const timeout = resolveMCPTimeoutMs(this.config.timeout);
493
485
  const operation = createMCPTimeout(timeout);
494
486
  try {
495
- const headers: Record<string, string> = {
496
- ...this.config.headers,
497
- "Mcp-Session-Id": this.#sessionId,
498
- };
499
-
500
- await fetch(this.config.url, {
501
- method: "DELETE",
502
- headers,
503
- signal: operation.signal,
504
- });
487
+ await this.#fetch({ method: "DELETE", signal: operation.signal }, { "Mcp-Session-Id": this.#sessionId });
505
488
  operation.clear();
506
489
  } catch {
507
490
  operation.clear();
@@ -12,6 +12,7 @@ import type {
12
12
  import { toJsonRpcError } from "../../mcp/types";
13
13
  import { RequestIdAllocator } from "../request-id";
14
14
  import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
15
+ import { type MCPFetchInit, mcpFetch } from "./header-policy";
15
16
 
16
17
  interface MCPTimeoutOperation {
17
18
  signal?: AbortSignal;
@@ -46,6 +47,16 @@ export class LegacySseTransport implements MCPTransport {
46
47
  this.#config = config;
47
48
  }
48
49
 
50
+ /** Fetch an endpoint with header precedence and origin policy. */
51
+ #fetch(url: string, init: MCPFetchInit, generated: Record<string, string>): Promise<Response> {
52
+ return mcpFetch(
53
+ url,
54
+ init,
55
+ { generated, configured: this.#config.headers },
56
+ this.#config.headerPolicy === "origin-locked",
57
+ );
58
+ }
59
+
49
60
  get connected(): boolean {
50
61
  return this.#connected;
51
62
  }
@@ -65,14 +76,11 @@ export class LegacySseTransport implements MCPTransport {
65
76
  this.#sseConnection = connection;
66
77
 
67
78
  try {
68
- const response = await fetch(this.#config.url, {
69
- method: "GET",
70
- headers: {
71
- Accept: "text/event-stream",
72
- ...this.#config.headers,
73
- },
74
- signal: operation.signal,
75
- });
79
+ const response = await this.#fetch(
80
+ this.#config.url,
81
+ { method: "GET", signal: operation.signal },
82
+ { Accept: "text/event-stream" },
83
+ );
76
84
 
77
85
  if (!response.ok) {
78
86
  const text = await response.text();
@@ -286,17 +294,12 @@ export class LegacySseTransport implements MCPTransport {
286
294
  ): Promise<Response> {
287
295
  const endpointUrl = this.#endpointUrl;
288
296
  if (!endpointUrl) throw new Error("Transport not connected");
289
- let headers: Record<string, string> = {
297
+ const generated: Record<string, string> = {
290
298
  "Content-Type": "application/json",
291
299
  Accept: "application/json, text/event-stream",
292
- ...this.#config.headers,
293
300
  };
294
- let response = await fetch(endpointUrl, {
295
- method: "POST",
296
- headers,
297
- body: JSON.stringify(body),
298
- signal,
299
- });
301
+ const payload = JSON.stringify(body);
302
+ let response = await this.#fetch(endpointUrl, { method: "POST", body: payload, signal }, generated);
300
303
  const status = AIError.status(response);
301
304
  if (!this.onAuthError || (status !== 401 && status !== 403)) return response;
302
305
 
@@ -304,17 +307,7 @@ export class LegacySseTransport implements MCPTransport {
304
307
  if (!refreshedHeaders) return response;
305
308
  await response.body?.cancel();
306
309
  this.#config.headers = refreshedHeaders;
307
- headers = {
308
- "Content-Type": "application/json",
309
- Accept: "application/json, text/event-stream",
310
- ...this.#config.headers,
311
- };
312
- response = await fetch(endpointUrl, {
313
- method: "POST",
314
- headers,
315
- body: JSON.stringify(body),
316
- signal,
317
- });
310
+ response = await this.#fetch(endpointUrl, { method: "POST", body: payload, signal }, generated);
318
311
  return response;
319
312
  }
320
313
 
package/src/mcp/types.ts CHANGED
@@ -100,6 +100,13 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
100
100
  command: string;
101
101
  args?: string[];
102
102
  env?: Record<string, string>;
103
+ /**
104
+ * `literal`: env values are opaque plugin package data (Agent Plugins
105
+ * §§4.1/9.2) — no env-name lookup, no `!command` execution, no dropping of
106
+ * empty values. The provider already applied the only permitted expansion
107
+ * (`${PLUGIN_ROOT}`/`${PLUGIN_DATA}`).
108
+ */
109
+ envPolicy?: "literal";
103
110
  cwd?: string;
104
111
  }
105
112
 
@@ -108,6 +115,12 @@ export interface MCPHttpServerConfig extends MCPServerConfigBase {
108
115
  type: "http";
109
116
  url: string;
110
117
  headers?: Record<string, string>;
118
+ /**
119
+ * `origin-locked`: configured headers are literal package data pinned to the
120
+ * configured URL's origin (Agent Plugins §7.2.1) — never expanded, never
121
+ * forwarded cross-origin, and client-generated headers win case-insensitively.
122
+ */
123
+ headerPolicy?: "origin-locked";
111
124
  }
112
125
 
113
126
  /** SSE server configuration (deprecated, use HTTP) */
@@ -115,6 +128,8 @@ export interface MCPSseServerConfig extends MCPServerConfigBase {
115
128
  type: "sse";
116
129
  url: string;
117
130
  headers?: Record<string, string>;
131
+ /** See {@link MCPHttpServerConfig.headerPolicy}. */
132
+ headerPolicy?: "origin-locked";
118
133
  }
119
134
 
120
135
  export type MCPServerConfig = MCPStdioServerConfig | MCPHttpServerConfig | MCPSseServerConfig;
@@ -70,6 +70,7 @@ import { SessionManager } from "../../session/session-manager";
70
70
  import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
71
71
  import { buildAvailableSlashCommands, toAcpAvailableCommands } from "../../slash-commands/available-commands";
72
72
  import { DEFAULT_STT_MODEL_KEY, STT_MODEL_OPTIONS } from "../../stt/models";
73
+ import { refreshAgentDiscovery } from "../../task";
73
74
  import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
74
75
  import { normalizeLocalScheme } from "../../tools/path-utils";
75
76
  import { ToolError } from "../../tools/tool-errors";
@@ -1930,14 +1931,15 @@ export class AcpAgent implements Agent {
1930
1931
  /**
1931
1932
  * Reload plugin/registry state for an ACP session. Mirrors the interactive
1932
1933
  * `/reload-plugins` and `/move` flows: invalidates the plugin-roots cache,
1933
- * resets the capability cache, refreshes the session's slash-command state,
1934
- * then re-advertises commands so the client sees newly installed/disabled
1935
- * plugins.
1934
+ * refreshes task agents, resets the capability cache, refreshes the
1935
+ * session's slash-command state, then re-advertises commands so the client
1936
+ * sees newly installed/disabled plugins.
1936
1937
  */
1937
1938
  async #reloadPluginState(record: ManagedSessionRecord): Promise<void> {
1938
1939
  const cwd = record.session.sessionManager.getCwd();
1939
1940
  const projectPath = await resolveActiveProjectRegistryPath(cwd);
1940
1941
  clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
1942
+ await refreshAgentDiscovery(cwd);
1941
1943
  resetCapabilities();
1942
1944
  await record.session.refreshSkills();
1943
1945
  const fileCommands = await loadSlashCommands({ cwd });
@@ -2019,7 +2021,16 @@ export class AcpAgent implements Agent {
2019
2021
 
2020
2022
  async #findStoredSession(sessionId: string, cwd: string): Promise<StoredSessionInfo | undefined> {
2021
2023
  const sessions = await this.#listStoredSessions(cwd);
2022
- return sessions.find(session => session.id === sessionId);
2024
+ const scoped = sessions.find(session => session.id === sessionId);
2025
+ if (scoped) {
2026
+ return scoped;
2027
+ }
2028
+ // The cwd-derived directory only covers sessions stored under the current
2029
+ // naming scheme. Sessions written under a legacy/hashed project directory
2030
+ // (the 17.2.5+ scheme reverted in #7656) live elsewhere, so fall back to a
2031
+ // global by-id scan: the session id is globally unique, and
2032
+ // #openStoredSession reopens the file with the request cwd. See #7779.
2033
+ return this.#findStoredSessionById(sessionId);
2023
2034
  }
2024
2035
 
2025
2036
  async #findStoredSessionById(sessionId: string): Promise<StoredSessionInfo | undefined> {
@@ -48,6 +48,7 @@ import { Settings } from "../../config/settings";
48
48
  import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-architect.md" with { type: "text" };
49
49
  import agentCreationUserPrompt from "../../prompts/system/agent-creation-user.md" with { type: "text" };
50
50
  import { createAgentSession } from "../../sdk";
51
+ import { refreshAgentDiscovery } from "../../task";
51
52
  import { discoverAgents } from "../../task/discovery";
52
53
  import { resolveAgentPrewalkDefault } from "../../task/prewalk";
53
54
  import type { AgentDefinition, AgentSource } from "../../task/types";
@@ -824,6 +825,7 @@ export class AgentDashboard extends Container {
824
825
  ).trimEnd();
825
826
  const content = `---\n${frontmatter}\n---\n\n${spec.systemPrompt.trim()}\n`;
826
827
  await Bun.write(filePath, content);
828
+ await refreshAgentDiscovery(this.cwd);
827
829
  await this.#reloadData();
828
830
  this.#clearCreateFlow();
829
831
  this.#notice = `Created agent ${spec.identifier} at ${shortenPath(filePath)}`;
@@ -10,7 +10,7 @@ export interface AggregateMetrics extends AgentMetrics {
10
10
  activeDurationAgents: number;
11
11
  }
12
12
 
13
- export interface AgentTreeProjection {
13
+ interface AgentTreeProjection {
14
14
  rows: AgentRef[];
15
15
  depthById: Map<string, number>;
16
16
  parentById: Map<string, string>;
@@ -65,7 +65,7 @@ export function progressMetrics(observed: ObservableSession | undefined): AgentM
65
65
  * usage embedded in completed `task` tool results, so using it for a parent
66
66
  * row would double-count child rows in the aggregate.
67
67
  */
68
- export function readSessionMetrics(session: NonNullable<AgentRef["session"]>): AgentMetrics | undefined {
68
+ function readSessionMetrics(session: NonNullable<AgentRef["session"]>): AgentMetrics | undefined {
69
69
  try {
70
70
  const stats = session.getSessionStats();
71
71
  const messages = session.agent?.state?.messages;
@@ -16,7 +16,7 @@ export interface RosterRender {
16
16
  }
17
17
 
18
18
  /** Legacy progress snapshots may omit counters; snapshot absence remains distinct. */
19
- export function metricNumber(value: number | undefined): number {
19
+ function metricNumber(value: number | undefined): number {
20
20
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
21
21
  }
22
22
 
@@ -67,7 +67,7 @@ export function statusText(status: AgentRef["status"], text: string): string {
67
67
  }
68
68
 
69
69
  /** Model id + thinking level (`sonnet-4-6 ◒ high`), level colored per theme. */
70
- export function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): string {
70
+ function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): string {
71
71
  const model = theme.fg("muted", sanitizeDisplayText(modelId));
72
72
  if (!level || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) return model;
73
73
  const display = theme.thinking[level as keyof typeof theme.thinking] ?? level;
@@ -81,11 +81,7 @@ export function formatRoleBadge(role: string, settings: Settings): string {
81
81
  }
82
82
 
83
83
  /** Format a resolved selector, preserving provider identity when requested. */
84
- export function formatResolvedModelBadge(
85
- resolved: string,
86
- preserveProvider = false,
87
- fallbackLevel?: ThinkingLevel,
88
- ): string {
84
+ function formatResolvedModelBadge(resolved: string, preserveProvider = false, fallbackLevel?: ThinkingLevel): string {
89
85
  const cleanResolved = sanitizeDisplayText(resolved);
90
86
  // Model ids may themselves contain colons (`qwen3:14b`), so only treat the
91
87
  // suffix as a thinking level when it parses as one.
@@ -123,6 +123,8 @@ export interface AgentHubDeps {
123
123
  ui?: TUI;
124
124
  /** Tool lookup for transcript renderers (labels, custom render functions). */
125
125
  getTool?: (name: string) => AgentTool | undefined;
126
+ /** Whether the active registry entry came from a built-in factory. */
127
+ isBuiltInTool?: (name: string) => boolean;
126
128
  /** Extension message renderers for custom messages in the transcript. */
127
129
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
128
130
  /** Cwd used by tool renderers for path shortening; defaults to the project dir. */
@@ -206,6 +208,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
206
208
  // Transcript-viewer launch deps (passed through to AgentTranscriptViewer).
207
209
  #ui: TUI;
208
210
  #getTool: ((name: string) => AgentTool | undefined) | undefined;
211
+ #isBuiltInTool: ((name: string) => boolean) | undefined;
209
212
  #getMessageRenderer: ((customType: string) => MessageRenderer | undefined) | undefined;
210
213
  #cwd: string;
211
214
  #hideThinkingBlock: (() => boolean) | undefined;
@@ -238,6 +241,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
238
241
  requestComponentRender: () => deps.requestRender(),
239
242
  } as unknown as TUI);
240
243
  this.#getTool = deps.getTool;
244
+ this.#isBuiltInTool = deps.isBuiltInTool;
241
245
  this.#getMessageRenderer = deps.getMessageRenderer;
242
246
  this.#cwd = deps.cwd ?? getProjectDir();
243
247
  this.#hideThinkingBlock = deps.hideThinkingBlock;
@@ -364,6 +368,7 @@ export class AgentHubOverlayComponent extends Container implements SelectListMou
364
368
  lifecycle: this.#remote ? undefined : this.#lifecycle,
365
369
  ui: this.#ui,
366
370
  getTool: this.#getTool,
371
+ isBuiltInTool: this.#isBuiltInTool,
367
372
  getMessageRenderer: this.#getMessageRenderer,
368
373
  cwd: this.#cwd,
369
374
  hideThinkingBlock: this.#hideThinkingBlock,
@@ -43,6 +43,8 @@ export interface AgentTranscriptViewerDeps {
43
43
  lifecycle?: () => AgentLifecycleManager;
44
44
  ui: TUI;
45
45
  getTool?: (name: string) => AgentTool | undefined;
46
+ /** Whether the active registry entry came from a built-in factory. */
47
+ isBuiltInTool?: (name: string) => boolean;
46
48
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
47
49
  cwd: string;
48
50
  hideThinkingBlock?: () => boolean;
@@ -161,6 +163,7 @@ export class AgentTranscriptViewer implements Component {
161
163
  this.#builder = new ChatTranscriptBuilder({
162
164
  ui: deps.ui,
163
165
  getTool: deps.getTool,
166
+ isBuiltInTool: deps.isBuiltInTool,
164
167
  getMessageRenderer: deps.getMessageRenderer,
165
168
  cwd: deps.cwd,
166
169
  hideThinkingBlock: deps.hideThinkingBlock,
@@ -61,6 +61,8 @@ import { CollapsedSyntheticMessageComponent, UserMessageComponent } from "./user
61
61
  export interface ChatTranscriptBuilderDeps {
62
62
  ui: TUI;
63
63
  getTool?: (name: string) => AgentTool | undefined;
64
+ /** Whether the active registry entry came from a built-in factory. */
65
+ isBuiltInTool?: (name: string) => boolean;
64
66
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
65
67
  cwd: string;
66
68
  hideThinkingBlock?: () => boolean;
@@ -392,6 +394,7 @@ export class ChatTranscriptBuilder {
392
394
  content.name,
393
395
  content.arguments,
394
396
  {
397
+ useBuiltInRenderer: this.deps.isBuiltInTool?.(content.name) ?? true,
395
398
  // Stable ids and Kitty placeholder cells keep images anchored
396
399
  // while the transcript viewport scrolls and reflows.
397
400
  showImages: settings.get("terminal.showImages"),
@@ -30,7 +30,6 @@ type ConfigurableEditorAction = Extract<
30
30
  | "app.model.cycleBackward"
31
31
  | "app.model.select"
32
32
  | "app.model.selectTemporary"
33
- | "app.tools.expand"
34
33
  | "app.tools.toggleVisibility"
35
34
  | "app.thinking.toggle"
36
35
  | "app.editor.external"
@@ -53,7 +52,6 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
53
52
  "app.model.cycleBackward": ["shift+ctrl+p"],
54
53
  "app.model.select": ["alt+m"],
55
54
  "app.model.selectTemporary": ["alt+p"],
56
- "app.tools.expand": ["ctrl+o"],
57
55
  "app.tools.toggleVisibility": ["ctrl+shift+o"],
58
56
  "app.thinking.toggle": ["ctrl+t"],
59
57
  "app.editor.external": ["ctrl+g"],
@@ -551,7 +549,6 @@ export class CustomEditor extends Editor {
551
549
  onCycleModelForward?: () => void;
552
550
  onCycleModelBackward?: () => void;
553
551
  onSelectModel?: () => void;
554
- onExpandTools?: () => void;
555
552
  onToggleToolActivity?: () => void;
556
553
  onToggleThinking?: () => void;
557
554
  onExternalEditor?: () => void;
@@ -907,12 +904,6 @@ export class CustomEditor extends Editor {
907
904
  return;
908
905
  }
909
906
 
910
- // Intercept configured tool output expansion shortcut
911
- if (this.#matchesAction(canonical, "app.tools.expand") && this.onExpandTools) {
912
- this.onExpandTools();
913
- return;
914
- }
915
-
916
907
  // Intercept configured tool activity visibility toggle
917
908
  if (this.#matchesAction(canonical, "app.tools.toggleVisibility") && this.onToggleToolActivity) {
918
909
  this.onToggleToolActivity();
@@ -1556,6 +1556,7 @@ export class StatusLineComponent implements Component {
1556
1556
  return {
1557
1557
  session: this.session,
1558
1558
  focusedAgentId: this.#focusedAgentId,
1559
+ sessionAccent: this.#resolveSettings().sessionAccent !== false,
1559
1560
  activeRepo: activeRepoCache.activeRepo,
1560
1561
  width,
1561
1562
  options: segmentOptions ?? {},
@@ -592,10 +592,12 @@ const sessionNameSegment: StatusLineSegment = {
592
592
  const name = sessionManager?.getSessionName();
593
593
  if (!name) return { content: "", visible: false };
594
594
 
595
- const ansi =
596
- getSessionAccentAnsi(
597
- getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
598
- ) ?? theme.getFgAnsi("accent");
595
+ const accentEnabled = ctx.sessionAccent !== false;
596
+ const ansi = accentEnabled
597
+ ? (getSessionAccentAnsi(
598
+ getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
599
+ ) ?? theme.getFgAnsi("accent"))
600
+ : theme.getFgAnsi("accent");
599
601
  return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
600
602
  },
601
603
  };
@@ -52,6 +52,8 @@ export interface SegmentContext {
52
52
  session: AgentSession;
53
53
  /** Focused subagent id while the view is proxied at its session, undefined otherwise. */
54
54
  focusedAgentId?: string | undefined;
55
+ /** Effective `statusLine.sessionAccent`; `false` disables hash-derived accent colors, while `true` or omission enables them. */
56
+ sessionAccent?: boolean;
55
57
  activeRepo: ActiveRepoContext | null;
56
58
  width: number;
57
59
  options: StatusLineSegmentOptions;