@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.
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +16 -2
- package/dist/context/manager.js +103 -19
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +17 -1
- package/dist/engine/engine.js +247 -89
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +7 -1
- package/dist/engine/turn-loop.js +117 -27
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +30 -0
- package/dist/protocol/server.js +246 -46
- package/dist/protocol/types.d.ts +47 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +190 -3
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +140 -74
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +55 -4
- package/dist/utils/toolDisplay.js +1 -1
- 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 {
|
|
10
|
+
import { getCredentialAccess } from "../credentials/access.js";
|
|
11
11
|
import { ENV_ALLOWLIST } from "../runtime/spawn-common.js";
|
|
12
|
-
import {
|
|
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
|
|
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
|
|
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 ?? {
|
|
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,
|
|
255
|
-
|
|
256
|
-
|
|
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 =
|
|
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;
|
|
292
|
-
//
|
|
293
|
-
//
|
|
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
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
private
|
|
48
|
+
private sessionStateById;
|
|
49
|
+
private closedSessionIds;
|
|
39
50
|
private promptFn;
|
|
40
|
-
private
|
|
41
|
-
private
|
|
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
|
|
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
|
/**
|