@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0-beta.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 (168) hide show
  1. package/THIRD_PARTY_NOTICES.md +206 -0
  2. package/dist/automation/scheduler.d.ts +13 -7
  3. package/dist/automation/scheduler.js +116 -37
  4. package/dist/capability-control/service.d.ts +2 -0
  5. package/dist/capability-control/service.js +4 -2
  6. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  7. package/dist/cc-orchestrator/agent-adapter.js +7 -1
  8. package/dist/cc-orchestrator/codex-session-history.js +1 -1
  9. package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
  10. package/dist/cc-orchestrator/cwd-normalize.js +19 -0
  11. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  12. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  13. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
  14. package/dist/cc-orchestrator/external-agent-driver.js +102 -51
  15. package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
  16. package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
  17. package/dist/cc-orchestrator/session-history.js +2 -2
  18. package/dist/cli/agent-server-stdio.js +9 -2
  19. package/dist/context/compaction.d.ts +8 -1
  20. package/dist/context/compaction.js +49 -4
  21. package/dist/context/manager.d.ts +16 -2
  22. package/dist/context/manager.js +103 -19
  23. package/dist/credentials/access.d.ts +56 -0
  24. package/dist/credentials/access.js +183 -0
  25. package/dist/credentials/index.d.ts +1 -0
  26. package/dist/credentials/index.js +1 -0
  27. package/dist/credentials/inject-credential-tool.d.ts +3 -1
  28. package/dist/credentials/inject-credential-tool.js +30 -11
  29. package/dist/credentials/types.d.ts +2 -2
  30. package/dist/credentials/use-credential-tool.d.ts +9 -1
  31. package/dist/credentials/use-credential-tool.js +58 -45
  32. package/dist/engine/engine.d.ts +17 -1
  33. package/dist/engine/engine.js +247 -89
  34. package/dist/engine/image-policy.d.ts +6 -0
  35. package/dist/engine/image-policy.js +17 -6
  36. package/dist/engine/input-attachments.d.ts +13 -0
  37. package/dist/engine/input-attachments.js +255 -0
  38. package/dist/engine/model-facade.d.ts +5 -2
  39. package/dist/engine/model-facade.js +4 -4
  40. package/dist/engine/parse-task.d.ts +10 -0
  41. package/dist/engine/parse-task.js +5 -0
  42. package/dist/engine/streaming-tool-queue.d.ts +11 -7
  43. package/dist/engine/streaming-tool-queue.js +11 -7
  44. package/dist/engine/turn-loop.d.ts +7 -1
  45. package/dist/engine/turn-loop.js +117 -27
  46. package/dist/engine/types.d.ts +8 -0
  47. package/dist/git/worktree/crud.d.ts +69 -0
  48. package/dist/git/worktree/crud.js +206 -0
  49. package/dist/git/worktree/diff.d.ts +14 -0
  50. package/dist/git/worktree/diff.js +82 -0
  51. package/dist/git/worktree/git-exec.d.ts +7 -0
  52. package/dist/git/worktree/git-exec.js +51 -0
  53. package/dist/git/worktree/index.d.ts +5 -0
  54. package/dist/git/worktree/index.js +5 -0
  55. package/dist/git/worktree/query.d.ts +42 -0
  56. package/dist/git/worktree/query.js +121 -0
  57. package/dist/git/worktree/slug.d.ts +11 -0
  58. package/dist/git/worktree/slug.js +58 -0
  59. package/dist/git/worktree.d.ts +1 -84
  60. package/dist/git/worktree.js +5 -230
  61. package/dist/index.d.ts +31 -28
  62. package/dist/index.js +27 -25
  63. package/dist/logging/logger.js +6 -6
  64. package/dist/logging/sanitize-messages.d.ts +10 -2
  65. package/dist/logging/sanitize-messages.js +21 -6
  66. package/dist/plugins/installer/checkUpdate.d.ts +4 -1
  67. package/dist/plugins/installer/checkUpdate.js +4 -2
  68. package/dist/plugins/installer/install.js +2 -0
  69. package/dist/plugins/installer/installFromSource.js +9 -1
  70. package/dist/plugins/installer/parseSource.d.ts +4 -1
  71. package/dist/plugins/installer/parseSource.js +28 -10
  72. package/dist/plugins/installer/sourcePath.d.ts +9 -0
  73. package/dist/plugins/installer/sourcePath.js +50 -0
  74. package/dist/plugins/installer/update.d.ts +4 -1
  75. package/dist/plugins/installer/update.js +5 -3
  76. package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
  77. package/dist/plugins/parseMarketplaceInput.js +4 -3
  78. package/dist/plugins/pluginInstaller.js +24 -22
  79. package/dist/preset/index.d.ts +1 -0
  80. package/dist/preset/index.js +16 -9
  81. package/dist/prompt/sections/base.md +1 -1
  82. package/dist/protocol/chat-session-manager.d.ts +2 -0
  83. package/dist/protocol/chat-session-manager.js +38 -2
  84. package/dist/protocol/chat-session.d.ts +3 -0
  85. package/dist/protocol/chat-session.js +1 -0
  86. package/dist/protocol/client.d.ts +9 -4
  87. package/dist/protocol/client.js +18 -1
  88. package/dist/protocol/server.d.ts +30 -0
  89. package/dist/protocol/server.js +246 -46
  90. package/dist/protocol/types.d.ts +47 -0
  91. package/dist/protocol/types.js +6 -0
  92. package/dist/run/FileRunStore.js +10 -1
  93. package/dist/run/Heartbeat.js +12 -0
  94. package/dist/run/RunApprovalBackend.d.ts +3 -0
  95. package/dist/run/RunApprovalBackend.js +41 -6
  96. package/dist/run/RunLock.js +2 -0
  97. package/dist/run/RunManager.d.ts +2 -0
  98. package/dist/run/RunManager.js +64 -24
  99. package/dist/run/ids.d.ts +2 -0
  100. package/dist/run/ids.js +23 -0
  101. package/dist/runtime/background-shell.d.ts +1 -0
  102. package/dist/runtime/background-shell.js +23 -13
  103. package/dist/runtime/spawn-common.js +10 -0
  104. package/dist/services/auto-dream.d.ts +15 -4
  105. package/dist/services/auto-dream.js +20 -20
  106. package/dist/services/dream-consolidation.d.ts +2 -3
  107. package/dist/services/dream-consolidation.js +65 -15
  108. package/dist/services/extract-memories.d.ts +14 -5
  109. package/dist/services/extract-memories.js +20 -3
  110. package/dist/services/global-dream-promotion.d.ts +23 -0
  111. package/dist/services/global-dream-promotion.js +112 -0
  112. package/dist/services/memory-orchestrator.js +347 -34
  113. package/dist/session/memory.d.ts +61 -18
  114. package/dist/session/memory.js +342 -79
  115. package/dist/session/session-manager.d.ts +35 -1
  116. package/dist/session/session-manager.js +190 -3
  117. package/dist/session/transcript.d.ts +1 -1
  118. package/dist/session/transcript.js +17 -5
  119. package/dist/settings/manager.d.ts +1 -0
  120. package/dist/settings/manager.js +87 -37
  121. package/dist/settings/schema-export.d.ts +2 -3
  122. package/dist/settings/schema-export.js +2 -3
  123. package/dist/settings/schema.d.ts +21 -0
  124. package/dist/settings/schema.js +12 -0
  125. package/dist/skills/scanner.d.ts +3 -2
  126. package/dist/skills/scanner.js +12 -9
  127. package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
  128. package/dist/tool-system/builtin/background-jobs.js +12 -1
  129. package/dist/tool-system/builtin/background-work.d.ts +17 -18
  130. package/dist/tool-system/builtin/background-work.js +51 -5
  131. package/dist/tool-system/builtin/config.d.ts +2 -1
  132. package/dist/tool-system/builtin/config.js +16 -11
  133. package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
  134. package/dist/tool-system/builtin/drive-claude-code.js +301 -47
  135. package/dist/tool-system/builtin/edit.js +5 -2
  136. package/dist/tool-system/builtin/generate-video.d.ts +1 -0
  137. package/dist/tool-system/builtin/generate-video.js +13 -4
  138. package/dist/tool-system/builtin/index.d.ts +8 -3
  139. package/dist/tool-system/builtin/index.js +37 -21
  140. package/dist/tool-system/builtin/lsp.d.ts +2 -1
  141. package/dist/tool-system/builtin/lsp.js +6 -3
  142. package/dist/tool-system/builtin/memory.js +40 -8
  143. package/dist/tool-system/builtin/notebook-edit.js +5 -2
  144. package/dist/tool-system/builtin/powershell.d.ts +5 -2
  145. package/dist/tool-system/builtin/powershell.js +11 -7
  146. package/dist/tool-system/builtin/read.js +118 -6
  147. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  148. package/dist/tool-system/builtin/view-image.js +109 -19
  149. package/dist/tool-system/builtin/worktree.d.ts +4 -4
  150. package/dist/tool-system/builtin/worktree.js +297 -74
  151. package/dist/tool-system/builtin/write.js +5 -3
  152. package/dist/tool-system/context.d.ts +23 -1
  153. package/dist/tool-system/executor.d.ts +1 -5
  154. package/dist/tool-system/executor.js +94 -115
  155. package/dist/tool-system/mcp-manager.d.ts +18 -5
  156. package/dist/tool-system/mcp-manager.js +140 -74
  157. package/dist/tool-system/path-policy.d.ts +2 -0
  158. package/dist/tool-system/path-policy.js +41 -12
  159. package/dist/tool-system/permission.d.ts +28 -7
  160. package/dist/tool-system/permission.js +130 -49
  161. package/dist/tool-system/registry.js +11 -4
  162. package/dist/tool-system/tool-result-redaction.d.ts +7 -0
  163. package/dist/tool-system/tool-result-redaction.js +48 -0
  164. package/dist/tool-system/workspace-bridge.d.ts +11 -0
  165. package/dist/tool-system/workspace-bridge.js +1 -0
  166. package/dist/types.d.ts +55 -4
  167. package/dist/utils/toolDisplay.js +1 -1
  168. package/package.json +4 -3
@@ -7,11 +7,12 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
7
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
8
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
9
  import { logger } from "../logging/logger.js";
10
- import { CredentialStore } from "../credentials/index.js";
10
+ import { getCredentialAccess } from "../credentials/access.js";
11
11
  import { ENV_ALLOWLIST } from "../runtime/spawn-common.js";
12
- import { writeFile, mkdir } from "node:fs/promises";
12
+ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
  import { diagnoseMcpStdioMissingCommand, previewPath } from "./mcp-stdio-diagnostics.js";
15
+ import { codeShellHome } from "../session/session-manager.js";
15
16
  /**
16
17
  * Read a required secret from `process.env` by NAME (Codex-style env-secret
17
18
  * handling — the value is never persisted in MCP config). A referenced env var
@@ -89,6 +90,20 @@ export function buildHttpHeaders(serverName, config, resolveCredential) {
89
90
  }
90
91
  return headers;
91
92
  }
93
+ export async function buildHttpHeadersWithCredentialAccess(serverName, config, access = getCredentialAccess()) {
94
+ if (!config.credentialRef)
95
+ return buildHttpHeaders(serverName, config);
96
+ if (!access.resolveValue) {
97
+ throw new Error(`MCP server "${serverName}": credential "${config.credentialRef}" not found or empty`);
98
+ }
99
+ const secret = await access.resolveValue({
100
+ cwd: undefined,
101
+ id: config.credentialRef,
102
+ scope: "full",
103
+ purpose: "mcp",
104
+ });
105
+ return buildHttpHeaders(serverName, config, (id) => id === config.credentialRef ? secret : undefined);
106
+ }
92
107
  /**
93
108
  * Infer the transport when the config doesn't name one: a url-only entry is
94
109
  * HTTP, everything else stdio. This is the CC `.mcp.json` convention —
@@ -118,7 +133,29 @@ export function inferTransportType(config) {
118
133
  * spill itself is bounded by the byte budget below; this cap is just
119
134
  * to keep ls(~/.code-shell/mcp_images) tractable.
120
135
  */
136
+ const MAX_MCP_IMAGE_SPILLS_PER_TOOL = 50;
121
137
  const MAX_MCP_IMAGE_BYTES = 8 * 1024 * 1024;
138
+ function spillTimestamp(filename, prefix) {
139
+ const suffix = filename.slice(prefix.length);
140
+ const dot = suffix.lastIndexOf(".");
141
+ const raw = dot === -1 ? suffix : suffix.slice(0, dot);
142
+ const timestamp = Number(raw);
143
+ return Number.isFinite(timestamp) ? timestamp : 0;
144
+ }
145
+ async function pruneMcpImageSpills(baseDir, prefix) {
146
+ const entries = await readdir(baseDir, { withFileTypes: true });
147
+ const spills = entries
148
+ .filter((entry) => entry.isFile() && entry.name.startsWith(prefix))
149
+ .map((entry) => ({
150
+ name: entry.name,
151
+ timestamp: spillTimestamp(entry.name, prefix),
152
+ }))
153
+ .sort((a, b) => a.timestamp - b.timestamp || a.name.localeCompare(b.name));
154
+ const excess = spills.length - MAX_MCP_IMAGE_SPILLS_PER_TOOL;
155
+ if (excess <= 0)
156
+ return;
157
+ await Promise.all(spills.slice(0, excess).map((entry) => unlink(join(baseDir, entry.name)).catch(() => { })));
158
+ }
122
159
  /**
123
160
  * Persist an MCP-returned image to disk and return the textual
124
161
  * reference the LLM should see. Images larger than
@@ -130,9 +167,7 @@ const MAX_MCP_IMAGE_BYTES = 8 * 1024 * 1024;
130
167
  * [mcp-image] server=playwright tool=screenshot saved=/abs/path.png (123 KB)
131
168
  */
132
169
  export async function spillMcpImage(serverName, toolName, base64, mimeType, opts) {
133
- const home = process.env.CODE_SHELL_HOME ?? process.env.HOME ?? "";
134
- const baseDir = opts?.baseDir ??
135
- (home ? join(home, ".code-shell", "mcp_images") : join("/tmp", "code-shell-mcp-images"));
170
+ const baseDir = opts?.baseDir ?? join(codeShellHome(), "mcp_images");
136
171
  const now = opts?.now ?? Date.now;
137
172
  const decodedBytes = Math.floor((base64.length * 3) / 4);
138
173
  if (decodedBytes > MAX_MCP_IMAGE_BYTES) {
@@ -147,11 +182,13 @@ export async function spillMcpImage(serverName, toolName, base64, mimeType, opts
147
182
  : "png";
148
183
  const safeServer = serverName.replace(/[^\w.-]+/g, "_");
149
184
  const safeTool = toolName.replace(/[^\w.-]+/g, "_");
150
- const filename = `${safeServer}-${safeTool}-${now()}.${ext}`;
185
+ const prefix = `${safeServer}-${safeTool}-`;
186
+ const filename = `${prefix}${now()}.${ext}`;
151
187
  const filePath = join(baseDir, filename);
152
188
  try {
153
189
  await mkdir(baseDir, { recursive: true });
154
190
  await writeFile(filePath, Buffer.from(base64, "base64"));
191
+ await pruneMcpImageSpills(baseDir, prefix);
155
192
  }
156
193
  catch (err) {
157
194
  logger.warn("mcp.image_spill_failed", {
@@ -205,7 +242,10 @@ export function buildRegisteredTool(serverName, tool) {
205
242
  return {
206
243
  name: toOpenAIToolName(`mcp_${serverName}_${tool.name}`),
207
244
  description: `[${serverName}] ${tool.description ?? tool.name}`,
208
- inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
245
+ inputSchema: tool.inputSchema ?? {
246
+ type: "object",
247
+ properties: {},
248
+ },
209
249
  source: "mcp",
210
250
  serverName,
211
251
  permissionDefault: "ask",
@@ -247,13 +287,16 @@ export class MCPManager {
247
287
  * Connect to all configured MCP servers and register their tools.
248
288
  */
249
289
  async connectAll(servers, owner) {
290
+ const enabledNames = this.enabledServerNames(servers);
250
291
  // Register this owner's desired set up front (see reconcile's shared-pool
251
292
  // note) so a later reconcile from ANOTHER session can't disconnect servers
252
293
  // this session connected at run start.
253
294
  if (owner !== undefined) {
254
- this.desiredByOwner.set(owner, new Set(Object.entries(servers)
255
- .filter(([, c]) => c.enabled !== false)
256
- .map(([n]) => n)));
295
+ this.desiredByOwner.set(owner, enabledNames);
296
+ this.desiredServerNames = this.unionDesired() ?? new Set();
297
+ }
298
+ else if (this.desiredByOwner.size === 0) {
299
+ this.desiredServerNames = enabledNames;
257
300
  }
258
301
  // Codex-style toggle: skip servers explicitly disabled in settings.
259
302
  // Only the literal `false` disables — absent / true / any other value
@@ -279,26 +322,42 @@ export class MCPManager {
279
322
  }
280
323
  }
281
324
  async reconcile(servers, owner) {
282
- const enabledNames = new Set(Object.entries(servers)
283
- .filter(([, config]) => config.enabled !== false)
284
- .map(([name]) => name));
285
- this.desiredServerNames = enabledNames;
325
+ const enabledNames = this.enabledServerNames(servers);
286
326
  // Shared-pool semantics: this ONE pool serves every session in the worker,
287
327
  // and sessions in different projects legitimately want DIFFERENT server
288
328
  // sets (per-project capabilityOverrides). Disconnect only servers that NO
289
329
  // registered owner wants — otherwise the per-session hot-reload patches
290
330
  // would thrash each other's connections (last reconcile wins, killing a
291
- // server another project's session is using). Owners are engines; a
292
- // closed session's desires linger (no engine-teardown hook), which merely
293
- // over-RETAINS idle connections — per-session tool visibility keeps
294
- // correctness regardless.
331
+ // server another project's session is using). Owners are engines; closed
332
+ // sessions call unregisterOwner() so their desired sets stop retaining idle
333
+ // connections.
295
334
  if (owner !== undefined)
296
335
  this.desiredByOwner.set(owner, enabledNames);
297
336
  const union = this.unionDesired() ?? enabledNames;
337
+ this.desiredServerNames = union;
298
338
  const stale = this.listServers().filter((name) => !union.has(name));
299
339
  await Promise.all(stale.map((name) => this.disconnect(name)));
300
340
  await this.connectAll(servers, owner);
301
341
  }
342
+ /**
343
+ * Remove one owner from the shared desired-server pool and disconnect servers
344
+ * that no remaining owner wants. This is called when a session-owned Engine
345
+ * is closed; without it project-scoped MCP servers can stay connected until
346
+ * worker shutdown.
347
+ */
348
+ async unregisterOwner(owner) {
349
+ if (!this.desiredByOwner.delete(owner))
350
+ return;
351
+ const desired = this.unionDesired() ?? new Set();
352
+ this.desiredServerNames = desired;
353
+ const stale = this.listServers().filter((name) => !desired.has(name));
354
+ await Promise.all(stale.map((name) => this.disconnect(name)));
355
+ }
356
+ enabledServerNames(servers) {
357
+ return new Set(Object.entries(servers)
358
+ .filter(([, config]) => config.enabled !== false)
359
+ .map(([name]) => name));
360
+ }
302
361
  /** Union of every registered owner's desired set; null when none registered. */
303
362
  unionDesired() {
304
363
  if (this.desiredByOwner.size === 0)
@@ -361,12 +420,10 @@ export class MCPManager {
361
420
  if (!config.url) {
362
421
  throw new Error(`MCP server "${name}": url is required for ${transportType} transport`);
363
422
  }
364
- // credentialRef resolves against the user-scope CredentialStore. The
365
- // shared MCPManager pool has no per-session cwd (see desiredByOwner note),
366
- // and MCP-referenced credentials (e.g. a Figma token) are user-global by
367
- // nature, so user scope is the right resolution surface here.
368
- const credStore = new CredentialStore(undefined);
369
- const headers = buildHttpHeaders(name, config, (id) => credStore.resolve(id)?.secret);
423
+ // credentialRef resolves against user-scope credential access. The shared
424
+ // MCPManager pool has no per-session cwd, and MCP-referenced credentials
425
+ // (e.g. a Figma token) remain user-global by design.
426
+ const headers = await buildHttpHeadersWithCredentialAccess(name, config);
370
427
  transport = new StreamableHTTPClientTransport(new URL(config.url), {
371
428
  requestInit: Object.keys(headers).length ? { headers } : undefined,
372
429
  });
@@ -429,59 +486,65 @@ export class MCPManager {
429
486
  * Discover tools from an MCP server and register them.
430
487
  */
431
488
  async discoverTools(serverName, client) {
432
- const result = await client.listTools();
433
- for (const tool of result.tools) {
434
- const registered = buildRegisteredTool(serverName, tool);
435
- // Register with an executor that calls the MCP server
436
- this.toolRegistry.registerTool(registered, async (args) => {
437
- const callResult = await client.callTool({
438
- name: tool.name,
439
- arguments: stripInternalToolArgs(args),
440
- });
441
- // Extract text + image content from the result. Image blobs
442
- // are spilled to ~/.code-shell/mcp_images/ so they don't bloat
443
- // the LLM message tree same pattern as the GenerateImage
444
- // tool and the model sees a textual reference it can Read
445
- // on a later turn if it needs the pixels. This sidesteps the
446
- // "MCP returned a 5MB screenshot token budget exploded"
447
- // failure mode that bit Codex (issue #11845); we never put
448
- // raw base64 image data in the result text. See
449
- // TODO-week.md #9c + docs/research-cc-vs-codex-image-handling.md §B.
450
- const parts = [];
451
- if (Array.isArray(callResult.content)) {
452
- for (const item of callResult.content) {
453
- if (typeof item === "string") {
454
- parts.push(item);
455
- continue;
456
- }
457
- if (typeof item !== "object" || item === null)
458
- continue;
459
- if ("text" in item) {
460
- parts.push(String(item.text));
461
- continue;
462
- }
463
- if (item.type === "image") {
464
- const block = item;
465
- if (typeof block.data === "string" && block.data.length > 0) {
466
- const note = await spillMcpImage(serverName, tool.name, block.data, block.mimeType ?? "image/png");
467
- parts.push(note);
489
+ try {
490
+ const result = await client.listTools();
491
+ for (const tool of result.tools) {
492
+ const registered = buildRegisteredTool(serverName, tool);
493
+ // Register with an executor that calls the MCP server
494
+ this.toolRegistry.registerTool(registered, async (args, ctx) => {
495
+ const callResult = await client.callTool({
496
+ name: tool.name,
497
+ arguments: stripInternalToolArgs(args),
498
+ }, undefined, ctx?.signal ? { signal: ctx.signal } : undefined);
499
+ // Extract text + image content from the result. Image blobs
500
+ // are spilled to ~/.code-shell/mcp_images/ so they don't bloat
501
+ // the LLM message tree same pattern as the GenerateImage
502
+ // tool and the model sees a textual reference it can Read
503
+ // on a later turn if it needs the pixels. This sidesteps the
504
+ // "MCP returned a 5MB screenshot token budget exploded"
505
+ // failure mode that bit Codex (issue #11845); we never put
506
+ // raw base64 image data in the result text. See
507
+ // TODO-week.md #9c + docs/research-cc-vs-codex-image-handling.md §B.
508
+ const parts = [];
509
+ if (Array.isArray(callResult.content)) {
510
+ for (const item of callResult.content) {
511
+ if (typeof item === "string") {
512
+ parts.push(item);
513
+ continue;
514
+ }
515
+ if (typeof item !== "object" || item === null)
516
+ continue;
517
+ if ("text" in item) {
518
+ parts.push(String(item.text));
519
+ continue;
520
+ }
521
+ if (item.type === "image") {
522
+ const block = item;
523
+ if (typeof block.data === "string" && block.data.length > 0) {
524
+ const note = await spillMcpImage(serverName, tool.name, block.data, block.mimeType ?? "image/png");
525
+ parts.push(note);
526
+ }
468
527
  }
469
528
  }
470
529
  }
471
- }
472
- const body = parts.join("\n") || "(no output)";
473
- // Trust boundary: MCP output is external content. Wrap it so the
474
- // model sees an explicit reminder that the body comes from an
475
- // untrusted server and any instructions inside are content, not
476
- // commands. The marker is intentionally short so it doesn't bloat
477
- // every tool result, but distinct enough that prompt-injected
478
- // strings can't fake their way out.
479
- return wrapMcpOutput(serverName, tool.name, body);
480
- });
481
- const set = this.registeredToolsByServer.get(serverName) ?? new Set();
482
- set.add(registered.name);
483
- this.registeredToolsByServer.set(serverName, set);
484
- logger.info("mcp.tool_registered", { server: serverName, tool: registered.name });
530
+ const body = parts.join("\n") || "(no output)";
531
+ // Trust boundary: MCP output is external content. Wrap it so the
532
+ // model sees an explicit reminder that the body comes from an
533
+ // untrusted server and any instructions inside are content, not
534
+ // commands. The marker is intentionally short so it doesn't bloat
535
+ // every tool result, but distinct enough that prompt-injected
536
+ // strings can't fake their way out.
537
+ return wrapMcpOutput(serverName, tool.name, body);
538
+ });
539
+ const set = this.registeredToolsByServer.get(serverName) ?? new Set();
540
+ set.add(registered.name);
541
+ this.registeredToolsByServer.set(serverName, set);
542
+ logger.info("mcp.tool_registered", { server: serverName, tool: registered.name });
543
+ }
544
+ }
545
+ catch (err) {
546
+ await this.disconnect(serverName);
547
+ throw err;
485
548
  }
486
549
  }
487
550
  /**
@@ -489,6 +552,8 @@ export class MCPManager {
489
552
  */
490
553
  async disconnectAll() {
491
554
  await Promise.all([...this.connections.keys()].map((name) => this.disconnect(name)));
555
+ this.desiredByOwner.clear();
556
+ this.desiredServerNames = null;
492
557
  }
493
558
  async disconnect(name) {
494
559
  const conn = this.connections.get(name);
@@ -562,6 +627,7 @@ export class MCPManager {
562
627
  uri: r.uri,
563
628
  name: r.name ?? r.uri,
564
629
  description: r.description,
630
+ serverName: name,
565
631
  });
566
632
  }
567
633
  }
@@ -52,6 +52,8 @@ export interface ClassifyOptions {
52
52
  export type PathApprovalScope = "once" | "session" | "project";
53
53
  /** Test seam: clear the in-memory session grants. */
54
54
  export declare function _resetSessionPathGrants(): void;
55
+ export declare function openSessionPathApprovals(sessionId: string): void;
56
+ export declare function clearSessionPathApprovals(sessionId: string): void;
55
57
  /**
56
58
  * Classify a file path against the workspace + sensitive-path policy.
57
59
  *
@@ -105,6 +105,10 @@ function expandTilde(p) {
105
105
  }
106
106
  /** sessionId → set of approved directory grants (prefix + operation). */
107
107
  const sessionPathGrants = new Map();
108
+ /** Session ids explicitly closed by ChatSessionManager; late approvals must not recreate grants. */
109
+ const closedPathApprovalSessions = new Set();
110
+ /** Per-session prompt chains for enforcePathPolicyWithApproval (see comment there). */
111
+ const askChains = new Map();
108
112
  /** True if a grant for `grantOp` authorizes an operation of `wantOp`. */
109
113
  function grantCoversOp(grantOp, wantOp) {
110
114
  // write ⊇ {read, write}; read ⊇ {read}.
@@ -198,6 +202,8 @@ function storedOp(g) {
198
202
  function recordPathApproval(scope, resolved, operation, cwd, sessionId) {
199
203
  if (scope === "once")
200
204
  return;
205
+ if (sessionId && closedPathApprovalSessions.has(sessionId))
206
+ return;
201
207
  const prefix = dirPrefix(dirname(resolved));
202
208
  if (scope === "session") {
203
209
  if (!sessionId)
@@ -271,6 +277,16 @@ function recordPathApproval(scope, resolved, operation, cwd, sessionId) {
271
277
  /** Test seam: clear the in-memory session grants. */
272
278
  export function _resetSessionPathGrants() {
273
279
  sessionPathGrants.clear();
280
+ closedPathApprovalSessions.clear();
281
+ askChains.clear();
282
+ }
283
+ export function openSessionPathApprovals(sessionId) {
284
+ closedPathApprovalSessions.delete(sessionId);
285
+ }
286
+ export function clearSessionPathApprovals(sessionId) {
287
+ sessionPathGrants.delete(sessionId);
288
+ closedPathApprovalSessions.add(sessionId);
289
+ askChains.delete(sessionId);
274
290
  }
275
291
  /**
276
292
  * Best-effort resolution. realpath fails when the path doesn't exist yet —
@@ -356,6 +372,12 @@ function isSafeCodeShellDiagnosticRead(resolved) {
356
372
  }
357
373
  return false;
358
374
  }
375
+ function isNoRepoAttachmentRead(resolved, operation) {
376
+ if (operation !== "read")
377
+ return false;
378
+ const root = join(homedir(), ".code-shell", "no-repo", ".code-shell", "attachments");
379
+ return isInsideDir(resolved, root);
380
+ }
359
381
  /**
360
382
  * Classify a file path against the workspace + sensitive-path policy.
361
383
  *
@@ -396,6 +418,13 @@ export function classifyPath(rawPath, opts) {
396
418
  // Sensitive: write is always denied, read always asks. Workspace placement
397
419
  // doesn't soften the rule — an `.env` in the project still asks on read.
398
420
  if (sensitiveLabel) {
421
+ if (isNoRepoAttachmentRead(resolved, opts.operation)) {
422
+ return {
423
+ decision: "allow",
424
+ reason: "no-repo attachment read",
425
+ resolvedPath: resolved,
426
+ };
427
+ }
399
428
  if (opts.operation === "read" && isSafeCodeShellDiagnosticRead(resolved)) {
400
429
  return {
401
430
  decision: "allow",
@@ -461,8 +490,8 @@ export function enforcePathPolicy(filePath, operation, workspaceRoot) {
461
490
  }
462
491
  // ask — MVP refuses with explanatory message until askUser plumbing
463
492
  // lands. The conservative bias matches the plan's leaning answer for Q1.
464
- return `Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
465
- `Set CODESHELL_PATH_POLICY=off to disable enforcement during a rollback.`;
493
+ return (`Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
494
+ `Set CODESHELL_PATH_POLICY=off to disable enforcement during a rollback.`);
466
495
  }
467
496
  export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
468
497
  if (ctx?.cwd === undefined)
@@ -481,8 +510,8 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
481
510
  return `Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}`;
482
511
  }
483
512
  if (operation === "write" && ctx.planMode) {
484
- return `Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}. ` +
485
- `Plan mode does not allow file writes.`;
513
+ return (`Error: blocked by path policy — ${c.reason}. Path: ${c.resolvedPath}. ` +
514
+ `Plan mode does not allow file writes.`);
486
515
  }
487
516
  // Already approved this directory for this OPERATION (this session or
488
517
  // persisted for the project)? Proceed without re-prompting — this is the fix
@@ -491,8 +520,8 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
491
520
  if (isPathPreApproved(c.resolvedPath, operation, ctx.cwd, ctx.sessionId))
492
521
  return null;
493
522
  if (!ctx.askUser) {
494
- return `Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
495
- `No interactive approval UI is available in this run.`;
523
+ return (`Error: path requires approval — ${c.reason}. Path: ${c.resolvedPath}. ` +
524
+ `No interactive approval UI is available in this run.`);
496
525
  }
497
526
  // Serialize concurrent asks (per session) and RE-CHECK grants when our turn
498
527
  // comes. Parallel tools hitting the same not-yet-approved directory all pass
@@ -502,7 +531,8 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
502
531
  const chainKey = ctx.sessionId ?? "__global__";
503
532
  const prevTurn = askChains.get(chainKey) ?? Promise.resolve();
504
533
  let release;
505
- askChains.set(chainKey, new Promise((r) => (release = r)));
534
+ const currentTurn = new Promise((r) => (release = r));
535
+ askChains.set(chainKey, currentTurn);
506
536
  try {
507
537
  await prevTurn;
508
538
  if (isPathPreApproved(c.resolvedPath, operation, ctx.cwd, ctx.sessionId))
@@ -511,6 +541,9 @@ export async function enforcePathPolicyWithApproval(filePath, operation, ctx) {
511
541
  }
512
542
  finally {
513
543
  release();
544
+ if (askChains.get(chainKey) === currentTurn) {
545
+ askChains.delete(chainKey);
546
+ }
514
547
  }
515
548
  }
516
549
  /** The actual interactive ask — split out so the serialized section reads flat.
@@ -521,9 +554,7 @@ async function promptForPathApproval(c, operation, ctx) {
521
554
  // "工作区外路径" header was misleading for sensitive-path asks.
522
555
  const isSensitive = c.reason.startsWith("sensitive");
523
556
  const what = operation === "read" ? "读取" : "写入";
524
- const title = isSensitive
525
- ? `工具想${what}敏感文件`
526
- : `工具想${what}工作区外路径`;
557
+ const title = isSensitive ? `工具想${what}敏感文件` : `工具想${what}工作区外路径`;
527
558
  const header = isSensitive ? "敏感文件权限" : "路径权限";
528
559
  // Scope options carry remembered grants for the directory of this path, so
529
560
  // the same folder isn't re-prompted. Labels are matched by exact string
@@ -566,8 +597,6 @@ async function promptForPathApproval(c, operation, ctx) {
566
597
  }
567
598
  return `Error: path approval denied by user — ${c.reason}. Path: ${c.resolvedPath}`;
568
599
  }
569
- /** Per-session prompt chains for enforcePathPolicyWithApproval (see comment there). */
570
- const askChains = new Map();
571
600
  /**
572
601
  * Internal: reset the "disabled warning" latch. Tests flip the env var
573
602
  * between cases and need each one to be able to re-trigger the warning.
@@ -21,6 +21,17 @@ export declare class AutoApprovalBackend implements ApprovalBackend {
21
21
  requestApproval(req: ApprovalRequest): Promise<ApprovalResult>;
22
22
  private isSafeOperation;
23
23
  }
24
+ /**
25
+ * Closed-session tombstones are a bounded replay guard. A prompt that started
26
+ * before close is still protected by the state-object identity check in
27
+ * isActiveSessionState(): clearSession deletes the captured state, so the late
28
+ * result cannot write into a new bucket even if an old tombstone has aged out.
29
+ * The tombstone only blocks fresh post-close calls carrying a recently closed
30
+ * sessionId from creating an empty bucket. 4096 is 256x the default
31
+ * ChatSessionManager.maxSessions (16), which covers normal late-result bursts
32
+ * while keeping long-lived processes from retaining every historical id.
33
+ */
34
+ export declare const CLOSED_SESSION_TOMBSTONE_LIMIT = 4096;
24
35
  /**
25
36
  * Interactive approval backend — prompts the user via a callback.
26
37
  *
@@ -34,13 +45,11 @@ export declare class AutoApprovalBackend implements ApprovalBackend {
34
45
  * the next time the user opens the project.
35
46
  */
36
47
  export declare class InteractiveApprovalBackend implements ApprovalBackend {
37
- private sessionAllowRules;
38
- private sessionDenyRules;
48
+ private sessionStateById;
49
+ private closedSessionIds;
39
50
  private promptFn;
40
- private cwd;
41
- private savedProjectRules;
42
- private onProjectRules;
43
- private promptTurn;
51
+ private legacyPromptTurn;
52
+ private legacyContext;
44
53
  setPromptFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
45
54
  /**
46
55
  * Has someone installed a real prompt callback? Engine consults this
@@ -59,8 +68,18 @@ export declare class InteractiveApprovalBackend implements ApprovalBackend {
59
68
  * earlier approvals.
60
69
  */
61
70
  setOnProjectRules(fn: (rules: PermissionRule[]) => void): void;
71
+ setSessionContext(sessionId: string, context: {
72
+ cwd: string;
73
+ onProjectRules: (rules: PermissionRule[]) => void;
74
+ }): void;
75
+ openSession(sessionId: string): void;
76
+ clearSession(sessionId: string): void;
77
+ private rememberClosedSession;
78
+ private makeSessionState;
79
+ private getSessionState;
80
+ private isActiveSessionState;
62
81
  requestApproval(req: ApprovalRequest): Promise<ApprovalResult>;
63
- /** Session-rule lookup — operation-scoped (see sessionAllowRules doc). Deny
82
+ /** Session-rule lookup — operation-scoped (see sessionStateById doc). Deny
64
83
  * wins over allow if both somehow match (conservative). Null = no rule. */
65
84
  private checkSessionRules;
66
85
  /** The actual interactive ask + rule recording (runs inside the prompt turn). */
@@ -89,6 +108,8 @@ export declare function pathRuleArgsPattern(absPath: string, scope: "file" | "di
89
108
  */
90
109
  export declare function ruleMatches(rule: PermissionRule, toolName: string, args: Record<string, unknown>): boolean;
91
110
  export declare function getInteractiveApprovalBackend(): InteractiveApprovalBackend;
111
+ export declare function openInteractiveApprovalSession(sessionId: string): void;
112
+ export declare function clearInteractiveApprovalSession(sessionId: string): void;
92
113
  export declare function setInteractiveApprovalFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
93
114
  type BashSafetyLevel = "safe-read" | "safe-write" | "unsafe" | "dangerous";
94
115
  /**