@cjhyy/code-shell-core 0.6.0-rc.16 → 0.6.0-rc.18
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/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cli/agent-server-stdio.js +2 -0
- 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.js +5 -5
- package/dist/credentials/use-credential-tool.d.ts +8 -1
- package/dist/credentials/use-credential-tool.js +55 -45
- package/dist/engine/engine.d.ts +3 -0
- package/dist/engine/engine.js +40 -13
- 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 +4 -0
- package/dist/engine/turn-loop.js +106 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/preset/index.js +10 -9
- package/dist/protocol/chat-session-manager.d.ts +1 -0
- package/dist/protocol/chat-session-manager.js +18 -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 +12 -0
- package/dist/protocol/server.js +116 -38
- package/dist/protocol/types.d.ts +37 -0
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/tool-system/builtin/drive-claude-code.d.ts +1 -0
- package/dist/tool-system/builtin/drive-claude-code.js +137 -18
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +15 -13
- 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 +114 -5
- package/dist/tool-system/builtin/view-image.js +9 -0
- 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 +2 -0
- package/dist/tool-system/mcp-manager.js +23 -8
- package/dist/tool-system/path-policy.js +13 -0
- 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/types.d.ts +23 -4
- package/package.json +4 -3
package/dist/preset/index.js
CHANGED
|
@@ -75,9 +75,9 @@ const GENERAL_BUILTIN_TOOLS = [
|
|
|
75
75
|
// what lets the LLM list/read/save/delete memories during a normal session,
|
|
76
76
|
// AND what makes the end-of-session auto-dream consolidation work — its
|
|
77
77
|
// tool-call loop pulls these from the registry, so without them every dream
|
|
78
|
-
// run bailed with "missing memory tools". Save/Delete in the user scope
|
|
79
|
-
//
|
|
80
|
-
// writes go through freely.
|
|
78
|
+
// run bailed with "missing memory tools". Save/Delete in the user scope have
|
|
79
|
+
// no explicit allow rule, so the default-mode classifier fallback asks;
|
|
80
|
+
// dream-scope writes go through freely.
|
|
81
81
|
"MemoryList",
|
|
82
82
|
"MemoryRead",
|
|
83
83
|
"MemorySave",
|
|
@@ -93,7 +93,7 @@ const GENERAL_BUILTIN_TOOLS = [
|
|
|
93
93
|
// AI 增/改模型 catalog(写 ~/.code-shell/model-catalog.user.json)。同 BashOutput/
|
|
94
94
|
// UseCredential 一样的 whitelist 要求:registerBuiltins 按 preset 集过滤 BUILTIN_TOOLS,
|
|
95
95
|
// 名单里没有它 → 即使工具已注册,AI 列表里也没有 → 用户实测「找不到这个工具」。
|
|
96
|
-
//
|
|
96
|
+
// 列入只是让 AI 看得到;执行期没有显式 allow rule,默认 classifier fallback 会审批。
|
|
97
97
|
"EditModelCatalog",
|
|
98
98
|
// Goal-mode control tools. Same whitelist requirement as the rest:
|
|
99
99
|
// registerBuiltins filters BUILTIN_TOOLS by the preset set, so a goal tool
|
|
@@ -134,18 +134,19 @@ const GENERAL_PERMISSION_RULES = [
|
|
|
134
134
|
{ tool: "Skill", decision: "allow" },
|
|
135
135
|
// ListMcpResources only enumerates resource names (and the executor filters
|
|
136
136
|
// the list to this session's enabled servers). ReadMcpResource pulls actual
|
|
137
|
-
// content and is intentionally NOT auto-allowed here —
|
|
138
|
-
//
|
|
137
|
+
// content and is intentionally NOT auto-allowed here — with no explicit rule,
|
|
138
|
+
// default-mode classifier fallback asks the user. The tool's
|
|
139
|
+
// permissionDefault is only UI/metadata, not classifier input.
|
|
139
140
|
{ tool: "ListMcpResources", decision: "allow" },
|
|
140
141
|
// Reading memory is always safe; writes (MemorySave/MemoryDelete) stay gated
|
|
141
|
-
// by
|
|
142
|
+
// by explicit rules below plus default-mode ask fallback.
|
|
142
143
|
{ tool: "MemoryList", decision: "allow" },
|
|
143
144
|
{ tool: "MemoryRead", decision: "allow" },
|
|
144
145
|
// Browser automation (3 semantic tools). Rules are first-match-wins, ordered
|
|
145
146
|
// by specificity — so the action-gating rule for browser_act MUST come first:
|
|
146
147
|
// click/type/select mutate the page/form → "ask"; all other actions (hover/
|
|
147
|
-
// scroll/wait/press_key/list_tabs/switch_tab) fall through to
|
|
148
|
-
//
|
|
148
|
+
// scroll/wait/press_key/list_tabs/switch_tab) fall through to the default
|
|
149
|
+
// allow rule below. observe (read-only) and navigate auto-allow.
|
|
149
150
|
// (Main-side sensitive-action + domain-whitelist enforcement also applies.)
|
|
150
151
|
{
|
|
151
152
|
tool: "browser_act",
|
|
@@ -25,6 +25,7 @@ export declare class ChatSessionManager {
|
|
|
25
25
|
constructor(opts: ChatSessionManagerOptions);
|
|
26
26
|
getOrCreate(sessionId: string, slice: EngineConfigSlice): ChatSession;
|
|
27
27
|
get(sessionId: string): ChatSession | undefined;
|
|
28
|
+
sessionExistsOnDisk(sessionId: string, slice: EngineConfigSlice): boolean;
|
|
28
29
|
/**
|
|
29
30
|
* Iterate every live session once. Public iterator so callers (e.g. the
|
|
30
31
|
* protocol server's config hot-reload / sessions query) don't reach into
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ChatSession } from "./chat-session.js";
|
|
2
2
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
3
3
|
import { clearAgentOutputFiles } from "../tool-system/builtin/agent-output-file.js";
|
|
4
|
+
import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
|
|
4
5
|
import { clearCredentialSessionAllow } from "../credentials/use-credential-tool.js";
|
|
5
6
|
import { clearInjectCredentialSessionAllow } from "../credentials/inject-credential-tool.js";
|
|
6
7
|
import { logger } from "../logging/logger.js";
|
|
7
8
|
import { clearSessionPathApprovals, openSessionPathApprovals } from "../tool-system/path-policy.js";
|
|
9
|
+
import { clearInteractiveApprovalSession, openInteractiveApprovalSession, } from "../tool-system/permission.js";
|
|
8
10
|
export class ChatSessionManager {
|
|
9
11
|
sessions = new Map();
|
|
10
12
|
runtime;
|
|
@@ -20,6 +22,7 @@ export class ChatSessionManager {
|
|
|
20
22
|
}
|
|
21
23
|
getOrCreate(sessionId, slice) {
|
|
22
24
|
openSessionPathApprovals(sessionId);
|
|
25
|
+
openInteractiveApprovalSession(sessionId);
|
|
23
26
|
const existing = this.sessions.get(sessionId);
|
|
24
27
|
if (existing) {
|
|
25
28
|
existing.lastActivityAt = Date.now();
|
|
@@ -49,6 +52,13 @@ export class ChatSessionManager {
|
|
|
49
52
|
get(sessionId) {
|
|
50
53
|
return this.sessions.get(sessionId);
|
|
51
54
|
}
|
|
55
|
+
sessionExistsOnDisk(sessionId, slice) {
|
|
56
|
+
const existing = this.sessions.get(sessionId);
|
|
57
|
+
if (existing)
|
|
58
|
+
return true;
|
|
59
|
+
const probeEngine = this.factory(slice);
|
|
60
|
+
return probeEngine.sessionExistsOnDisk(sessionId);
|
|
61
|
+
}
|
|
52
62
|
/**
|
|
53
63
|
* Iterate every live session once. Public iterator so callers (e.g. the
|
|
54
64
|
* protocol server's config hot-reload / sessions query) don't reach into
|
|
@@ -65,6 +75,7 @@ export class ChatSessionManager {
|
|
|
65
75
|
return;
|
|
66
76
|
s.cancel();
|
|
67
77
|
clearSessionPathApprovals(sessionId);
|
|
78
|
+
clearInteractiveApprovalSession(sessionId);
|
|
68
79
|
clearCredentialSessionAllow(sessionId);
|
|
69
80
|
clearInjectCredentialSessionAllow(sessionId);
|
|
70
81
|
this.unregisterMcpOwner(s);
|
|
@@ -103,8 +114,13 @@ export class ChatSessionManager {
|
|
|
103
114
|
sweepIdle() {
|
|
104
115
|
const cutoff = Date.now() - this.idleTtlMs;
|
|
105
116
|
for (const [id, s] of [...this.sessions]) {
|
|
106
|
-
if (s.lastActivityAt
|
|
107
|
-
|
|
117
|
+
if (s.lastActivityAt >= cutoff)
|
|
118
|
+
continue;
|
|
119
|
+
if (s.isBusy())
|
|
120
|
+
continue;
|
|
121
|
+
if (backgroundJobRegistry.hasRunningForSession(id))
|
|
122
|
+
continue;
|
|
123
|
+
this.close(id);
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
126
|
startIdleSweeper(intervalMs = 60_000) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Engine, EngineResult } from "../engine/engine.js";
|
|
2
2
|
import type { ModelEntry } from "../llm/model-pool.js";
|
|
3
3
|
import type { StreamEvent } from "../types.js";
|
|
4
|
+
import type { InputAttachmentMeta } from "./types.js";
|
|
4
5
|
export interface ChatSessionOptions {
|
|
5
6
|
id: string;
|
|
6
7
|
engine: Engine;
|
|
@@ -20,6 +21,8 @@ export interface TurnOpts {
|
|
|
20
21
|
injected?: boolean;
|
|
21
22
|
/** Stable id for this user-intent; forwarded to Engine.run for idempotency. */
|
|
22
23
|
clientMessageId?: string;
|
|
24
|
+
/** Structured input attachments for this turn. */
|
|
25
|
+
attachments?: InputAttachmentMeta[];
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* One ChatSession per UI chat tab. Owns a single Engine, an AbortController
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
* const result = await client.run("fix the bug");
|
|
12
12
|
*/
|
|
13
13
|
import type { Transport } from "./transport.js";
|
|
14
|
-
import { type RunParams, type RunResult, type AgentStreamEventNotification, type ConfigureParams, type QueryParams, type QueryResult } from "./types.js";
|
|
14
|
+
import { type RunParams, type RunResult, type AgentStreamEventNotification, type ConfigureParams, type QueryParams, type QueryResult, type ApprovalRequestNotification, type ApprovalResolvedNotification } from "./types.js";
|
|
15
15
|
import type { ApprovalRequest, ApprovalResult, PermissionMode, BackgroundAgentCompletedEvent } from "../types.js";
|
|
16
|
+
export type ApprovalRequestMeta = Pick<ApprovalRequestNotification, "sessionId">;
|
|
17
|
+
export type ApprovalResolvedEvent = ApprovalResolvedNotification;
|
|
16
18
|
export interface AgentClientEvents {
|
|
17
19
|
/** Multi-session envelope: carries sessionId + event. */
|
|
18
20
|
stream: (envelope: AgentStreamEventNotification) => void;
|
|
19
|
-
approvalRequest: (requestId: string, request: ApprovalRequest) => void;
|
|
21
|
+
approvalRequest: (requestId: string, request: ApprovalRequest, meta?: ApprovalRequestMeta) => void;
|
|
22
|
+
approvalResolved: (event: ApprovalResolvedEvent) => void;
|
|
20
23
|
status: (status: string, message?: string) => void;
|
|
21
24
|
}
|
|
22
25
|
export interface AgentRunOptions {
|
|
@@ -115,8 +118,10 @@ export declare class AgentClient {
|
|
|
115
118
|
*/
|
|
116
119
|
onStreamEvent(handler: (envelope: AgentStreamEventNotification) => void): void;
|
|
117
120
|
offStreamEvent(handler: (envelope: AgentStreamEventNotification) => void): void;
|
|
118
|
-
onApprovalRequest(handler: (requestId: string, request: ApprovalRequest) => void): void;
|
|
119
|
-
offApprovalRequest(handler: (requestId: string, request: ApprovalRequest) => void): void;
|
|
121
|
+
onApprovalRequest(handler: (requestId: string, request: ApprovalRequest, meta?: ApprovalRequestMeta) => void): void;
|
|
122
|
+
offApprovalRequest(handler: (requestId: string, request: ApprovalRequest, meta?: ApprovalRequestMeta) => void): void;
|
|
123
|
+
onApprovalResolved(handler: (event: ApprovalResolvedEvent) => void): void;
|
|
124
|
+
offApprovalResolved(handler: (event: ApprovalResolvedEvent) => void): void;
|
|
120
125
|
onStatus(handler: (status: string, message?: string) => void): void;
|
|
121
126
|
offStatus(handler: (status: string, message?: string) => void): void;
|
|
122
127
|
/**
|
package/dist/protocol/client.js
CHANGED
|
@@ -211,6 +211,12 @@ export class AgentClient {
|
|
|
211
211
|
offApprovalRequest(handler) {
|
|
212
212
|
this.emitter.off("approvalRequest", handler);
|
|
213
213
|
}
|
|
214
|
+
onApprovalResolved(handler) {
|
|
215
|
+
this.emitter.on("approvalResolved", handler);
|
|
216
|
+
}
|
|
217
|
+
offApprovalResolved(handler) {
|
|
218
|
+
this.emitter.off("approvalResolved", handler);
|
|
219
|
+
}
|
|
214
220
|
onStatus(handler) {
|
|
215
221
|
this.emitter.on("status", handler);
|
|
216
222
|
}
|
|
@@ -279,7 +285,18 @@ export class AgentClient {
|
|
|
279
285
|
const requestId = params.requestId;
|
|
280
286
|
const request = params.request;
|
|
281
287
|
if (requestId && request) {
|
|
282
|
-
|
|
288
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
289
|
+
const meta = sessionId === undefined ? {} : { sessionId };
|
|
290
|
+
this.emitter.emit("approvalRequest", requestId, request, meta);
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case Methods.ApprovalResolved: {
|
|
295
|
+
const requestId = params.requestId;
|
|
296
|
+
if (requestId) {
|
|
297
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
298
|
+
const event = sessionId === undefined ? { requestId } : { sessionId, requestId };
|
|
299
|
+
this.emitter.emit("approvalResolved", event);
|
|
283
300
|
}
|
|
284
301
|
break;
|
|
285
302
|
}
|
|
@@ -67,6 +67,14 @@ export declare class AgentServer {
|
|
|
67
67
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
68
68
|
private readonly readActiveGoalFromDisk;
|
|
69
69
|
private readonly workspaceBridgeEnabled;
|
|
70
|
+
/**
|
|
71
|
+
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
72
|
+
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
73
|
+
* with the same cwd/permission/trust inputs before draining the queue.
|
|
74
|
+
*/
|
|
75
|
+
private readonly lastSliceBySession;
|
|
76
|
+
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
77
|
+
private diskSessionReader;
|
|
70
78
|
/**
|
|
71
79
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
72
80
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -137,6 +145,10 @@ export declare class AgentServer {
|
|
|
137
145
|
* "wakeup in flight" guard flag.
|
|
138
146
|
*/
|
|
139
147
|
private maybeWakeIdleSession;
|
|
148
|
+
private rehydrateSessionForWake;
|
|
149
|
+
private sliceForWakeRehydrate;
|
|
150
|
+
private rememberSessionSlice;
|
|
151
|
+
private wireInteractiveSession;
|
|
140
152
|
private handleRequest;
|
|
141
153
|
private handleRun;
|
|
142
154
|
private handleRunMulti;
|
package/dist/protocol/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js
|
|
|
24
24
|
import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
|
|
25
25
|
import { logger } from "../logging/logger.js";
|
|
26
26
|
import { nanoid } from "nanoid";
|
|
27
|
+
import { SessionManager } from "../session/session-manager.js";
|
|
27
28
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
28
29
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
29
30
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
@@ -49,6 +50,14 @@ export class AgentServer {
|
|
|
49
50
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
50
51
|
readActiveGoalFromDisk;
|
|
51
52
|
workspaceBridgeEnabled;
|
|
53
|
+
/**
|
|
54
|
+
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
55
|
+
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
56
|
+
* with the same cwd/permission/trust inputs before draining the queue.
|
|
57
|
+
*/
|
|
58
|
+
lastSliceBySession = new Map();
|
|
59
|
+
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
60
|
+
diskSessionReader = null;
|
|
52
61
|
/**
|
|
53
62
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
54
63
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -179,7 +188,7 @@ export class AgentServer {
|
|
|
179
188
|
maybeWakeIdleSession(sessionId) {
|
|
180
189
|
if (!this.chatManager)
|
|
181
190
|
return;
|
|
182
|
-
const session = this.chatManager.get(sessionId);
|
|
191
|
+
const session = this.chatManager.get(sessionId) ?? this.rehydrateSessionForWake(sessionId);
|
|
183
192
|
if (!session || session.isBusy())
|
|
184
193
|
return;
|
|
185
194
|
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
@@ -239,6 +248,76 @@ export class AgentServer {
|
|
|
239
248
|
this.maybeWakeIdleSession(sessionId);
|
|
240
249
|
});
|
|
241
250
|
}
|
|
251
|
+
rehydrateSessionForWake(sessionId) {
|
|
252
|
+
if (!this.chatManager)
|
|
253
|
+
return null;
|
|
254
|
+
try {
|
|
255
|
+
const pending = notificationQueue.getSnapshot(sessionId);
|
|
256
|
+
if (pending.length === 0) {
|
|
257
|
+
logger.debug("bg_wakeup.rehydrate_skipped_no_pending", { sessionId });
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const slice = this.sliceForWakeRehydrate(sessionId);
|
|
261
|
+
if (!slice) {
|
|
262
|
+
logger.warn("bg_wakeup.rehydrate_skipped_missing_disk_session", {
|
|
263
|
+
sessionId,
|
|
264
|
+
pendingCount: pending.length,
|
|
265
|
+
reason: "state_json_cwd_missing",
|
|
266
|
+
});
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (!this.chatManager.sessionExistsOnDisk(sessionId, slice)) {
|
|
270
|
+
logger.warn("bg_wakeup.rehydrate_skipped_missing_disk_session", {
|
|
271
|
+
sessionId,
|
|
272
|
+
pendingCount: pending.length,
|
|
273
|
+
});
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const session = this.chatManager.getOrCreate(sessionId, slice);
|
|
277
|
+
this.wireInteractiveSession(session, sessionId);
|
|
278
|
+
logger.debug("bg_wakeup.rehydrated_session", {
|
|
279
|
+
sessionId,
|
|
280
|
+
pendingCount: pending.length,
|
|
281
|
+
source: this.lastSliceBySession.has(sessionId) ? "last_slice" : "state_json",
|
|
282
|
+
});
|
|
283
|
+
return session;
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
logger.warn("bg_wakeup.rehydrate_failed", {
|
|
287
|
+
sessionId,
|
|
288
|
+
error: err instanceof Error ? err.message : String(err),
|
|
289
|
+
});
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
sliceForWakeRehydrate(sessionId) {
|
|
294
|
+
const cached = this.lastSliceBySession.get(sessionId);
|
|
295
|
+
if (cached)
|
|
296
|
+
return { ...cached };
|
|
297
|
+
if (!this.diskSessionReader)
|
|
298
|
+
this.diskSessionReader = new SessionManager();
|
|
299
|
+
const cwd = this.diskSessionReader.readCwd(sessionId);
|
|
300
|
+
if (!cwd)
|
|
301
|
+
return null;
|
|
302
|
+
return {
|
|
303
|
+
permissionMode: "default",
|
|
304
|
+
projectTrusted: false,
|
|
305
|
+
cwd,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
rememberSessionSlice(sessionId, slice) {
|
|
309
|
+
this.lastSliceBySession.set(sessionId, { ...slice });
|
|
310
|
+
}
|
|
311
|
+
wireInteractiveSession(session, sid) {
|
|
312
|
+
if (session.engine.isHeadless())
|
|
313
|
+
return;
|
|
314
|
+
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
315
|
+
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
316
|
+
session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
|
|
317
|
+
if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
|
|
318
|
+
session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
242
321
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
243
322
|
async handleRequest(req) {
|
|
244
323
|
switch (req.method) {
|
|
@@ -311,28 +390,39 @@ export class AgentServer {
|
|
|
311
390
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "task is required"));
|
|
312
391
|
return;
|
|
313
392
|
}
|
|
393
|
+
const sessionConfig = {
|
|
394
|
+
permissionMode: params.permissionMode,
|
|
395
|
+
cwd: params.cwd,
|
|
396
|
+
projectTrusted: params.projectTrusted,
|
|
397
|
+
goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
|
|
398
|
+
? params.goal
|
|
399
|
+
: undefined,
|
|
400
|
+
};
|
|
401
|
+
if (params.requireExisting === true && !cm.get(params.sessionId)) {
|
|
402
|
+
let existsOnDisk = false;
|
|
403
|
+
try {
|
|
404
|
+
existsOnDisk = cm.sessionExistsOnDisk(params.sessionId, sessionConfig);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const code = err.code ?? ErrorCodes.InternalError;
|
|
408
|
+
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (!existsOnDisk) {
|
|
412
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
314
416
|
let session;
|
|
315
417
|
try {
|
|
316
|
-
session = cm.getOrCreate(params.sessionId,
|
|
317
|
-
permissionMode: params.permissionMode,
|
|
318
|
-
cwd: params.cwd,
|
|
319
|
-
projectTrusted: params.projectTrusted,
|
|
320
|
-
});
|
|
418
|
+
session = cm.getOrCreate(params.sessionId, sessionConfig);
|
|
321
419
|
}
|
|
322
420
|
catch (err) {
|
|
323
421
|
const code = err.code ?? ErrorCodes.InternalError;
|
|
324
422
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
325
423
|
return;
|
|
326
424
|
}
|
|
327
|
-
|
|
328
|
-
// running the prompt against a freshly-created blank session. A cron
|
|
329
|
-
// "continue this conversation" job whose session the user deleted must fail
|
|
330
|
-
// loudly here (SessionNotFound) so the scheduler can auto-disable it,
|
|
331
|
-
// instead of silently executing with no transcript/goal/context.
|
|
332
|
-
if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
|
|
333
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
425
|
+
this.rememberSessionSlice(params.sessionId, sessionConfig);
|
|
336
426
|
if (params.model !== undefined) {
|
|
337
427
|
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
338
428
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
@@ -350,32 +440,14 @@ export class AgentServer {
|
|
|
350
440
|
session.engine.setPlanMode(params.planMode);
|
|
351
441
|
}
|
|
352
442
|
const sid = params.sessionId;
|
|
353
|
-
// Wire AskUserQuestion for this interactive session. The
|
|
354
|
-
// builds a fresh per-session Engine via engineFactory,
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
// chat (and in a resumed automation session). Route it to the client with
|
|
358
|
-
// this session's id so the renderer attributes the question to the right
|
|
359
|
-
// tab, resolving via the session's own pendingApprovals. Skip genuinely
|
|
360
|
-
// headless engines (no human to answer).
|
|
361
|
-
if (!session.engine.isHeadless()) {
|
|
362
|
-
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
363
|
-
// Browser automation bridge: each method routes a browser action to the
|
|
364
|
-
// client (Electron main drives the webview via CDP) over the SAME
|
|
365
|
-
// request/response channel as askUser (pendingApprovals + requestId).
|
|
366
|
-
// Browser actions still keep their own bounded timeout; AskUserQuestion
|
|
367
|
-
// waits for a real answer or Stop/cancel.
|
|
368
|
-
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
369
|
-
// Cookie→browser injection (InjectCredential tool): same cross-process
|
|
370
|
-
// channel; main restores the cookie jar into the built-in browser.
|
|
371
|
-
session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
|
|
372
|
-
if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
|
|
373
|
-
session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
|
|
374
|
-
}
|
|
375
|
-
}
|
|
443
|
+
// Wire AskUserQuestion and host bridges for this interactive session. The
|
|
444
|
+
// chatManager path builds a fresh per-session Engine via engineFactory, so
|
|
445
|
+
// these host callbacks must be installed after every create/recreate.
|
|
446
|
+
this.wireInteractiveSession(session, sid);
|
|
376
447
|
try {
|
|
377
448
|
const result = await session.enqueueTurn(params.task, {
|
|
378
449
|
cwd: params.cwd,
|
|
450
|
+
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
379
451
|
goal: typeof params.goal === "string" ||
|
|
380
452
|
(params.goal != null && typeof params.goal === "object")
|
|
381
453
|
? params.goal
|
|
@@ -637,6 +709,12 @@ export class AgentServer {
|
|
|
637
709
|
: params.sessionId
|
|
638
710
|
? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
|
|
639
711
|
: false;
|
|
712
|
+
if (cleared && typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
713
|
+
this.notify(Methods.StreamEvent, {
|
|
714
|
+
sessionId: params.sessionId,
|
|
715
|
+
event: { type: "goal_cleared" },
|
|
716
|
+
});
|
|
717
|
+
}
|
|
640
718
|
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
641
719
|
}
|
|
642
720
|
/**
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -47,10 +47,41 @@ export declare const ErrorCodes: {
|
|
|
47
47
|
*/
|
|
48
48
|
readonly Cancelled: -32005;
|
|
49
49
|
};
|
|
50
|
+
export type InputAttachmentKind = "image" | "file" | "directory";
|
|
51
|
+
export type InputAttachmentOrigin = "paste" | "os-drop" | "file-panel" | "picker" | "mention" | "generated" | "tool";
|
|
52
|
+
export interface InputAttachmentMeta {
|
|
53
|
+
id: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
kind: InputAttachmentKind;
|
|
56
|
+
origin: InputAttachmentOrigin;
|
|
57
|
+
path: string;
|
|
58
|
+
absPath: string;
|
|
59
|
+
relPath?: string;
|
|
60
|
+
mime?: string;
|
|
61
|
+
size: number;
|
|
62
|
+
sha256: string;
|
|
63
|
+
originalName?: string;
|
|
64
|
+
createdAt: number;
|
|
65
|
+
sourcePath?: string;
|
|
66
|
+
width?: number;
|
|
67
|
+
height?: number;
|
|
68
|
+
vision?: {
|
|
69
|
+
include: boolean;
|
|
70
|
+
mediaPath?: string;
|
|
71
|
+
detail?: "low" | "standard" | "high";
|
|
72
|
+
};
|
|
73
|
+
directory?: {
|
|
74
|
+
treePath?: string;
|
|
75
|
+
truncated?: boolean;
|
|
76
|
+
entryCount?: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
50
79
|
/** Start an agent run with a user task. */
|
|
51
80
|
export interface RunParams {
|
|
52
81
|
sessionId: string;
|
|
53
82
|
task: string;
|
|
83
|
+
/** Structured input attachments. Legacy `<codeshell-image>` task blocks remain supported. */
|
|
84
|
+
attachments?: InputAttachmentMeta[];
|
|
54
85
|
/** Stable id for the user's submit intent; duplicate ids are idempotent. */
|
|
55
86
|
clientMessageId?: string;
|
|
56
87
|
/**
|
|
@@ -264,6 +295,12 @@ export interface ApprovalRequestNotification {
|
|
|
264
295
|
requestId: string;
|
|
265
296
|
request: ApprovalRequest;
|
|
266
297
|
}
|
|
298
|
+
/** Server tells clients a pending approval/ask has been resolved elsewhere. */
|
|
299
|
+
export interface ApprovalResolvedNotification {
|
|
300
|
+
/** Originating engine session when known. */
|
|
301
|
+
sessionId?: string;
|
|
302
|
+
requestId: string;
|
|
303
|
+
}
|
|
267
304
|
/** Server status changed. */
|
|
268
305
|
export interface StatusNotification {
|
|
269
306
|
status: "ready" | "running" | "error" | "shutdown";
|
|
@@ -159,6 +159,13 @@ export function resolveGitBash() {
|
|
|
159
159
|
const override = process.env.CODE_SHELL_GIT_BASH_PATH;
|
|
160
160
|
if (override && existsSync(override))
|
|
161
161
|
return (gitBashCache = override);
|
|
162
|
+
// Test-only hermetic switch: skip auto-discovery entirely. Clearing PATH is
|
|
163
|
+
// NOT enough on Windows — CreateProcess always searches System32, so `where`
|
|
164
|
+
// still spawns and a CI runner's preinstalled Git for Windows would resolve a
|
|
165
|
+
// real bash.exe and defeat the fallback tests. This env flag makes the
|
|
166
|
+
// "Git Bash absent" scenario reproducible on a real Windows host.
|
|
167
|
+
if (process.env.CODE_SHELL_NO_SHELL_DISCOVERY)
|
|
168
|
+
return (gitBashCache = null) ?? undefined;
|
|
162
169
|
const candidates = [];
|
|
163
170
|
// (2) derive from `where git`. Git installs git.exe under either \cmd\ or
|
|
164
171
|
// \bin\; bash.exe lives under the sibling \bin\. Walk up to the Git root.
|
|
@@ -194,6 +201,9 @@ export function resolvePowerShell() {
|
|
|
194
201
|
const override = process.env.CODE_SHELL_POWERSHELL_PATH;
|
|
195
202
|
if (override && existsSync(override))
|
|
196
203
|
return (powerShellCache = override);
|
|
204
|
+
// Test-only hermetic switch (see resolveGitBash for why PATH-clearing fails).
|
|
205
|
+
if (process.env.CODE_SHELL_NO_SHELL_DISCOVERY)
|
|
206
|
+
return (powerShellCache = null) ?? undefined;
|
|
197
207
|
const candidates = [];
|
|
198
208
|
for (const exe of ["pwsh", "powershell"]) {
|
|
199
209
|
try {
|