@cjhyy/code-shell-core 0.7.0-beta.1 → 0.7.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 (146) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  19. package/dist/cc-orchestrator/agent-adapter.js +4 -0
  20. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  21. package/dist/cc-orchestrator/codex-session-history.js +64 -4
  22. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  23. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
  24. package/dist/cc-orchestrator/external-agent-driver.js +202 -38
  25. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  26. package/dist/cc-orchestrator/session-history.js +96 -13
  27. package/dist/cli/agent-server-tcp.js +13 -2
  28. package/dist/context/manager.d.ts +3 -3
  29. package/dist/context/manager.js +6 -6
  30. package/dist/context/token-counter.js +5 -3
  31. package/dist/context/tool-result-storage.d.ts +6 -0
  32. package/dist/context/tool-result-storage.js +25 -4
  33. package/dist/credentials/access.d.ts +11 -1
  34. package/dist/credentials/access.js +77 -1
  35. package/dist/credentials/index.d.ts +3 -1
  36. package/dist/credentials/index.js +2 -0
  37. package/dist/credentials/oauth.d.ts +25 -0
  38. package/dist/credentials/oauth.js +179 -0
  39. package/dist/credentials/store.d.ts +2 -1
  40. package/dist/credentials/store.js +19 -8
  41. package/dist/credentials/types.d.ts +84 -1
  42. package/dist/credentials/types.js +16 -1
  43. package/dist/engine/engine.d.ts +67 -34
  44. package/dist/engine/engine.js +448 -247
  45. package/dist/engine/goal.d.ts +19 -0
  46. package/dist/engine/goal.js +16 -6
  47. package/dist/engine/input-attachments.js +156 -13
  48. package/dist/engine/run-image-input.d.ts +22 -0
  49. package/dist/engine/run-image-input.js +195 -0
  50. package/dist/engine/session-title.d.ts +2 -1
  51. package/dist/engine/session-title.js +4 -1
  52. package/dist/engine/steer-queue.d.ts +3 -1
  53. package/dist/engine/steer-queue.js +10 -2
  54. package/dist/engine/turn-loop.d.ts +48 -1
  55. package/dist/engine/turn-loop.js +307 -37
  56. package/dist/engine/types.d.ts +6 -2
  57. package/dist/git/worktree/crud.d.ts +3 -0
  58. package/dist/git/worktree/crud.js +32 -3
  59. package/dist/git/worktree/git-exec.d.ts +2 -2
  60. package/dist/git/worktree/git-exec.js +47 -11
  61. package/dist/git/worktree/query.d.ts +8 -7
  62. package/dist/git/worktree/query.js +27 -20
  63. package/dist/hooks/events.d.ts +3 -0
  64. package/dist/hooks/events.js +0 -3
  65. package/dist/hooks/goal-stop-hook.d.ts +44 -2
  66. package/dist/hooks/goal-stop-hook.js +775 -52
  67. package/dist/hooks/registry.js +3 -0
  68. package/dist/hooks/shell-runner.d.ts +12 -1
  69. package/dist/hooks/shell-runner.js +160 -9
  70. package/dist/index.d.ts +7 -6
  71. package/dist/index.js +6 -5
  72. package/dist/llm/client-base.js +12 -10
  73. package/dist/llm/types.d.ts +12 -5
  74. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  75. package/dist/plugins/pluginCommandHook.js +111 -13
  76. package/dist/preset/index.js +14 -4
  77. package/dist/protocol/chat-session-manager.d.ts +13 -2
  78. package/dist/protocol/chat-session-manager.js +90 -18
  79. package/dist/protocol/chat-session.d.ts +12 -0
  80. package/dist/protocol/chat-session.js +30 -5
  81. package/dist/protocol/client.d.ts +5 -2
  82. package/dist/protocol/client.js +22 -1
  83. package/dist/protocol/server.d.ts +25 -11
  84. package/dist/protocol/server.js +291 -73
  85. package/dist/protocol/types.d.ts +36 -2
  86. package/dist/protocol/types.js +2 -0
  87. package/dist/services/dream-consolidation.d.ts +3 -0
  88. package/dist/services/dream-consolidation.js +4 -1
  89. package/dist/services/index.d.ts +1 -1
  90. package/dist/services/index.js +1 -1
  91. package/dist/services/oauth.d.ts +34 -10
  92. package/dist/services/oauth.js +233 -98
  93. package/dist/session/session-manager.d.ts +35 -6
  94. package/dist/session/session-manager.js +396 -27
  95. package/dist/session/transcript.d.ts +30 -1
  96. package/dist/session/transcript.js +119 -4
  97. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  98. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  99. package/dist/tool-system/builtin/agent.js +5 -1
  100. package/dist/tool-system/builtin/arena.js +1 -0
  101. package/dist/tool-system/builtin/background-jobs.d.ts +28 -5
  102. package/dist/tool-system/builtin/background-jobs.js +109 -7
  103. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  104. package/dist/tool-system/builtin/background-work.js +5 -1
  105. package/dist/tool-system/builtin/bash.d.ts +3 -5
  106. package/dist/tool-system/builtin/bash.js +10 -5
  107. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  108. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  109. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  110. package/dist/tool-system/builtin/cron.d.ts +1 -2
  111. package/dist/tool-system/builtin/cron.js +9 -7
  112. package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
  113. package/dist/tool-system/builtin/drive-claude-code.js +307 -20
  114. package/dist/tool-system/builtin/edit.d.ts +2 -1
  115. package/dist/tool-system/builtin/edit.js +12 -4
  116. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  117. package/dist/tool-system/builtin/generate-video.js +138 -21
  118. package/dist/tool-system/builtin/glob.d.ts +2 -1
  119. package/dist/tool-system/builtin/glob.js +28 -3
  120. package/dist/tool-system/builtin/grep.d.ts +1 -0
  121. package/dist/tool-system/builtin/grep.js +82 -17
  122. package/dist/tool-system/builtin/index.d.ts +25 -11
  123. package/dist/tool-system/builtin/index.js +60 -5
  124. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  125. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  126. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  127. package/dist/tool-system/builtin/sleep.js +1 -22
  128. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  129. package/dist/tool-system/builtin/video-providers.js +1 -0
  130. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  131. package/dist/tool-system/builtin/web-fetch.js +44 -3
  132. package/dist/tool-system/builtin/worktree.js +25 -7
  133. package/dist/tool-system/builtin/write.d.ts +2 -1
  134. package/dist/tool-system/builtin/write.js +14 -4
  135. package/dist/tool-system/context.d.ts +35 -5
  136. package/dist/tool-system/executor.js +24 -8
  137. package/dist/tool-system/mcp-manager.d.ts +20 -2
  138. package/dist/tool-system/mcp-manager.js +111 -12
  139. package/dist/tool-system/path-policy.d.ts +19 -0
  140. package/dist/tool-system/path-policy.js +62 -1
  141. package/dist/tool-system/permission.d.ts +43 -3
  142. package/dist/tool-system/permission.js +383 -30
  143. package/dist/tool-system/registry.d.ts +3 -2
  144. package/dist/tool-system/registry.js +52 -34
  145. package/dist/types.d.ts +38 -7
  146. package/package.json +1 -1
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Sleep tool metadata.
3
+ *
4
+ * Kept separate from the executor so registration and presets can consume the
5
+ * definition without importing implementation code.
6
+ */
7
+ export const sleepToolDef = {
8
+ name: "Sleep",
9
+ description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
10
+ "Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
11
+ "the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
12
+ "If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
13
+ "instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
14
+ "({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
15
+ "prompt: 'check whether <that task> finished; if still running, wait again' }). " +
16
+ "That returns control to you at the interval without burning a turn spinning. " +
17
+ "Maximum duration is 300 seconds (5 minutes).",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ seconds: {
22
+ type: "number",
23
+ description: "Number of seconds to sleep (max 300)",
24
+ },
25
+ },
26
+ required: ["seconds"],
27
+ },
28
+ };
@@ -1,28 +1,7 @@
1
1
  /**
2
2
  * SleepTool — pause execution for a specified duration.
3
3
  */
4
- export const sleepToolDef = {
5
- name: "Sleep",
6
- description: "Pause execution for a brief, deterministic wait (e.g. letting a just-started service settle for a few seconds). " +
7
- "Do NOT use Sleep to poll for or wait on background work (background shells, async sub-agents, video generation): " +
8
- "the system wakes you automatically when that work completes — just end your turn instead of looping Sleep. " +
9
- "If you want a safety net in case a background task hangs and never signals completion, do NOT loop Sleep either — " +
10
- "instead end your turn and schedule a one-shot self-wakeup with CronCreate " +
11
- "({ schedule: '5m', once: true, continueInSession: true, permissionLevel: 'read-only', " +
12
- "prompt: 'check whether <that task> finished; if still running, wait again' }). " +
13
- "That returns control to you at the interval without burning a turn spinning. " +
14
- "Maximum duration is 300 seconds (5 minutes).",
15
- inputSchema: {
16
- type: "object",
17
- properties: {
18
- seconds: {
19
- type: "number",
20
- description: "Number of seconds to sleep (max 300)",
21
- },
22
- },
23
- required: ["seconds"],
24
- },
25
- };
4
+ export { sleepToolDef } from "./sleep.definition.js";
26
5
  export async function sleepTool(args) {
27
6
  const seconds = Math.min(Math.max(Number(args.seconds) || 1, 0.1), 300);
28
7
  const signal = args.__signal;
@@ -40,6 +40,7 @@ export type VideoPollResult = {
40
40
  } | {
41
41
  ok: true;
42
42
  status: "succeeded";
43
+ url?: string;
43
44
  } | {
44
45
  ok: true;
45
46
  status: "failed";
@@ -57,6 +58,14 @@ export type VideoDownloadResult = {
57
58
  ok: false;
58
59
  error: string;
59
60
  };
61
+ export interface VideoDownloadRequest {
62
+ jobId: string;
63
+ creds: VideoProviderCreds;
64
+ signal?: AbortSignal;
65
+ /** Called as soon as the provider exposes the remote result URL, before the
66
+ * potentially long byte download begins. */
67
+ onUrl?: (url: string) => void;
68
+ }
60
69
  export interface VideoProvider {
61
70
  readonly kind: string;
62
71
  submit(req: VideoSubmitRequest): Promise<VideoSubmitResult>;
@@ -65,11 +74,7 @@ export interface VideoProvider {
65
74
  creds: VideoProviderCreds;
66
75
  signal?: AbortSignal;
67
76
  }): Promise<VideoPollResult>;
68
- download(req: {
69
- jobId: string;
70
- creds: VideoProviderCreds;
71
- signal?: AbortSignal;
72
- }): Promise<VideoDownloadResult>;
77
+ download(req: VideoDownloadRequest): Promise<VideoDownloadResult>;
73
78
  }
74
79
  /**
75
80
  * Deterministic in-memory adapter for tests and local dev — no network. Models
@@ -95,11 +100,7 @@ export declare class FakeVideoProvider implements VideoProvider {
95
100
  creds?: VideoProviderCreds;
96
101
  signal?: AbortSignal;
97
102
  }): Promise<VideoPollResult>;
98
- download(_req: {
99
- jobId: string;
100
- creds?: VideoProviderCreds;
101
- signal?: AbortSignal;
102
- }): Promise<VideoDownloadResult>;
103
+ download(_req: VideoDownloadRequest): Promise<VideoDownloadResult>;
103
104
  }
104
105
  /**
105
106
  * fal.ai video adapter — submit/poll/download against the fal queue API
@@ -138,11 +139,7 @@ export declare class FalVideoProvider implements VideoProvider {
138
139
  creds: VideoProviderCreds;
139
140
  signal?: AbortSignal;
140
141
  }): Promise<VideoPollResult>;
141
- download(req: {
142
- jobId: string;
143
- creds: VideoProviderCreds;
144
- signal?: AbortSignal;
145
- }): Promise<VideoDownloadResult>;
142
+ download(req: VideoDownloadRequest): Promise<VideoDownloadResult>;
146
143
  }
147
144
  /** Default model per video provider kind (filled in with real adapters). */
148
145
  export declare const DEFAULT_VIDEO_MODEL: Record<string, string>;
@@ -197,6 +197,7 @@ export class FalVideoProvider {
197
197
  const videoUrl = j.video?.url;
198
198
  if (!videoUrl)
199
199
  return { ok: false, error: "fal result: no video.url" };
200
+ req.onUrl?.(videoUrl);
200
201
  // hop 2: video bytes
201
202
  const vr = await this.fetchImpl(videoUrl, { method: "GET", signal: req.signal });
202
203
  if (!vr.ok) {
@@ -13,6 +13,6 @@
13
13
  */
14
14
  import type { ToolDefinition } from "../../types.js";
15
15
  import type { ToolContext } from "../context.js";
16
- import type { BuiltinToolResult } from "./index.js";
16
+ import type { BuiltinToolReturn } from "./index.js";
17
17
  export declare const viewImageToolDef: ToolDefinition;
18
- export declare function viewImageTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<BuiltinToolResult>;
18
+ export declare function viewImageTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<BuiltinToolReturn>;
@@ -25,6 +25,7 @@ export const webFetchToolDef = {
25
25
  };
26
26
  const MAX_OUTPUT = 100_000;
27
27
  const DEFAULT_MAX = 50_000;
28
+ const RAW_BYTE_BUDGET_MULTIPLIER = 4;
28
29
  // Header names that must not be overridable via args.headers (SSRF / auth injection)
29
30
  const BLOCKED_REQUEST_HEADERS = new Set([
30
31
  "host", "authorization", "cookie", "proxy-authorization",
@@ -252,7 +253,46 @@ export async function webFetchTool(args) {
252
253
  }
253
254
  async function readAndTruncateBody(res, maxLength) {
254
255
  const contentType = res.headers.get("content-type") ?? "";
255
- const body = await res.text();
256
+ const maxBytes = maxLength * RAW_BYTE_BUDGET_MULTIPLIER;
257
+ const reader = res.body?.getReader();
258
+ let body;
259
+ let rawTruncated = false;
260
+ if (!reader) {
261
+ body = await res.text();
262
+ }
263
+ else {
264
+ const decoder = new TextDecoder("utf-8");
265
+ const chunks = [];
266
+ let bytesRead = 0;
267
+ try {
268
+ for (;;) {
269
+ const { done, value } = await reader.read();
270
+ if (done)
271
+ break;
272
+ const remaining = maxBytes - bytesRead;
273
+ if (value.byteLength >= remaining) {
274
+ chunks.push(decoder.decode(value.subarray(0, remaining), { stream: true }));
275
+ bytesRead += remaining;
276
+ rawTruncated = true;
277
+ await reader.cancel();
278
+ break;
279
+ }
280
+ chunks.push(decoder.decode(value, { stream: true }));
281
+ bytesRead += value.byteLength;
282
+ }
283
+ const tail = decoder.decode();
284
+ // When the byte budget cuts through a multi-byte code point, flushing
285
+ // the decoder would turn that intentionally incomplete suffix into �.
286
+ // All complete code points were already emitted by stream:true, so the
287
+ // truncated tail is safe to discard.
288
+ if (!rawTruncated)
289
+ chunks.push(tail);
290
+ body = chunks.join("");
291
+ }
292
+ finally {
293
+ reader.releaseLock();
294
+ }
295
+ }
256
296
  let text;
257
297
  if (contentType.includes("text/html") || contentType.includes("application/xhtml")) {
258
298
  text = extractTextFromHTML(body);
@@ -260,8 +300,9 @@ async function readAndTruncateBody(res, maxLength) {
260
300
  else {
261
301
  text = body;
262
302
  }
263
- if (text.length > maxLength) {
264
- text = text.slice(0, maxLength) + `\n\n... content truncated (${text.length} chars total)`;
303
+ if (rawTruncated || text.length > maxLength) {
304
+ const shownChars = Math.min(text.length, maxLength);
305
+ text = text.slice(0, maxLength) + `\n\n... content truncated (showing first ${shownChars} chars)`;
265
306
  }
266
307
  return text || "(page returned empty content)";
267
308
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { existsSync } from "node:fs";
5
5
  import { isAbsolute, resolve } from "node:path";
6
- import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, } from "../../git/worktree.js";
6
+ import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, cleanupAbortedWorktree, } from "../../git/worktree.js";
7
7
  export const switchSessionWorkspaceToolDef = {
8
8
  name: "SwitchSessionWorkspace",
9
9
  description: "Switch this current conversation session into or out of a workspace through the host UI path. " +
@@ -44,7 +44,7 @@ export async function switchSessionWorkspaceTool(args, ctx) {
44
44
  nextTurnNotice(workspace.root, ctx?.cwd ?? workspace.root));
45
45
  }
46
46
  catch (err) {
47
- return `Error switching workspace: ${err.message}`;
47
+ return `Error: switching workspace failed: ${err.message}`;
48
48
  }
49
49
  }
50
50
  export const enterWorktreeToolDef = {
@@ -86,6 +86,7 @@ export async function enterWorktreeTool(args, ctx) {
86
86
  const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
87
87
  try {
88
88
  if (target === "main") {
89
+ ctx?.signal?.throwIfAborted();
89
90
  const workspace = { root: mainRoot, kind: "main" };
90
91
  persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
91
92
  sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
@@ -101,13 +102,26 @@ export async function enterWorktreeTool(args, ctx) {
101
102
  sessionId,
102
103
  currentWorkspace: fromWorkspace,
103
104
  branchPrefix,
105
+ signal: ctx?.signal,
104
106
  });
107
+ if (ctx?.signal?.aborted) {
108
+ if (selected.created) {
109
+ await cleanupAbortedWorktree(mainRoot, selected.session.worktreePath, selected.session.worktreeBranch);
110
+ }
111
+ throw new Error("Worktree creation aborted");
112
+ }
105
113
  const workspace = toSessionWorkspace(selected, fromWorkspace);
106
- persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
107
- sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
108
114
  const setupNote = selected.created
109
115
  ? await runSetupIfConfigured(selected.session.worktreePath, mainRoot, ctx)
110
116
  : "";
117
+ if (ctx?.signal?.aborted) {
118
+ if (selected.created) {
119
+ await cleanupAbortedWorktree(mainRoot, selected.session.worktreePath, selected.session.worktreeBranch);
120
+ }
121
+ throw new Error("Worktree creation aborted");
122
+ }
123
+ persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
124
+ sessionManager.recordWorkspaceHandoff(sessionId, fromWorkspace, workspace);
111
125
  const verb = selected.created ? "Worktree created and switched" : "Switched to worktree";
112
126
  return (`${verb}:\n` +
113
127
  ` Path: ${workspace.worktree.path}\n` +
@@ -118,7 +132,7 @@ export async function enterWorktreeTool(args, ctx) {
118
132
  setupNote);
119
133
  }
120
134
  catch (err) {
121
- return `Error switching worktree: ${err.message}`;
135
+ return `Error: switching worktree failed: ${err.message}`;
122
136
  }
123
137
  }
124
138
  /** Keep setup output from bloating the tool result — head+tail-ish trim. */
@@ -229,7 +243,7 @@ export async function exitWorktreeTool(args, ctx) {
229
243
  nextTurnNotice(mainRoot, currentTurnRoot));
230
244
  }
231
245
  catch (err) {
232
- return `Error exiting worktree: ${err.message}`;
246
+ return `Error: exiting worktree failed: ${err.message}`;
233
247
  }
234
248
  }
235
249
  function sessionServices(ctx) {
@@ -261,6 +275,7 @@ function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnR
261
275
  }
262
276
  async function resolveWorktreeTarget(opts) {
263
277
  const entries = await listWorktrees(opts.mainRoot);
278
+ opts.signal?.throwIfAborted();
264
279
  const pathTarget = pathLike(opts.target) ? resolvePathTarget(opts.target, opts.cwd) : undefined;
265
280
  const branchTarget = normalizeBranchName(opts.target);
266
281
  const match = entries.find((entry) => {
@@ -269,6 +284,8 @@ async function resolveWorktreeTarget(opts) {
269
284
  return entry.branch === branchTarget;
270
285
  });
271
286
  if (match) {
287
+ const originalBranch = await currentBranch(opts.mainRoot);
288
+ opts.signal?.throwIfAborted();
272
289
  return {
273
290
  created: false,
274
291
  session: {
@@ -276,7 +293,7 @@ async function resolveWorktreeTarget(opts) {
276
293
  worktreePath: match.path,
277
294
  worktreeName: match.path.split(/[\\/]/).pop() ?? match.branch,
278
295
  worktreeBranch: match.branch,
279
- originalBranch: await currentBranch(opts.mainRoot),
296
+ originalBranch,
280
297
  sessionId: opts.sessionId,
281
298
  createdAt: Date.now(),
282
299
  },
@@ -289,6 +306,7 @@ async function resolveWorktreeTarget(opts) {
289
306
  validateWorktreeSlug(opts.target);
290
307
  const created = await createWorktree(opts.mainRoot, opts.target, opts.sessionId, {
291
308
  prefix: opts.branchPrefix,
309
+ signal: opts.signal,
292
310
  });
293
311
  return { created: true, session: created, from: created.originalBranch ?? "HEAD" };
294
312
  }
@@ -3,5 +3,6 @@
3
3
  */
4
4
  import type { ToolDefinition } from "../../types.js";
5
5
  import type { ToolContext } from "../context.js";
6
+ import type { ToolFailure } from "./index.js";
6
7
  export declare const writeToolDef: ToolDefinition;
7
- export declare function writeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
8
+ export declare function writeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | ToolFailure>;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Built-in Write file tool.
3
3
  */
4
- import { writeFile, mkdir } from "node:fs/promises";
4
+ import { mkdir } from "node:fs/promises";
5
5
  import { dirname, isAbsolute, resolve } from "node:path";
6
6
  import { fileCache } from "./file-cache.js";
7
+ import { getFinalWritePathSnapshot, revalidateFinalWritePath, writeFileNoFollow, } from "../path-policy.js";
7
8
  export const writeToolDef = {
8
9
  name: "Write",
9
10
  description: "Write content to a file. Creates the file if it doesn't exist, " +
@@ -27,12 +28,21 @@ export async function writeTool(args, ctx) {
27
28
  const cwd = ctx?.cwd ?? process.cwd();
28
29
  const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
29
30
  try {
30
- await mkdir(dirname(filePath), { recursive: true });
31
- await writeFile(filePath, content, "utf-8");
31
+ const approvedPath = getFinalWritePathSnapshot(args, filePath, cwd);
32
+ const beforeMkdir = revalidateFinalWritePath(filePath, cwd, approvedPath);
33
+ if ("error" in beforeMkdir)
34
+ return { ok: false, error: beforeMkdir.error };
35
+ await mkdir(dirname(beforeMkdir.resolvedPath), { recursive: true });
36
+ // mkdir may have crossed an existing symlink in a missing parent chain;
37
+ // resolve again immediately before opening the final file.
38
+ const beforeWrite = revalidateFinalWritePath(filePath, cwd, approvedPath);
39
+ if ("error" in beforeWrite)
40
+ return { ok: false, error: beforeWrite.error };
41
+ await writeFileNoFollow(beforeWrite.resolvedPath, content);
32
42
  fileCache.invalidate(filePath);
33
43
  return `Successfully wrote to ${filePath}`;
34
44
  }
35
45
  catch (err) {
36
- return `Error writing file: ${err.message}`;
46
+ return { ok: false, error: `Error writing file: ${err.message}` };
37
47
  }
38
48
  }
@@ -12,7 +12,7 @@
12
12
  * Tools that don't need any context (Read/Write/Bash/...) just ignore
13
13
  * the second argument; the type is purely additive.
14
14
  */
15
- import type { LLMConfig, StreamCallback } from "../types.js";
15
+ import type { LLMConfig, StreamCallback, TokenUsage } from "../types.js";
16
16
  import type { ModelPool } from "../llm/model-pool.js";
17
17
  import type { ToolRegistry } from "./registry.js";
18
18
  import type { AgentPresetName } from "../preset/index.js";
@@ -20,6 +20,7 @@ import type { SandboxBackend } from "./sandbox/index.js";
20
20
  import type { HookRegistry } from "../hooks/registry.js";
21
21
  import type { SessionManager } from "../session/session-manager.js";
22
22
  import type { SessionWorkspace } from "../types.js";
23
+ import type { ApprovalRouter } from "./permission.js";
23
24
  /**
24
25
  * Narrow view of the owning Engine that tools are allowed to call back into.
25
26
  * Defined here (in the low-level tool-system) rather than importing the
@@ -154,10 +155,7 @@ export interface SubAgentSpawner {
154
155
  * (or `resumeSessionId` when resuming), so the parent can address follow-up
155
156
  * input at the same session via AgentSendInput (transcript replay).
156
157
  */
157
- spawn(req: SubAgentSpawnRequest): Promise<{
158
- text: string;
159
- sessionId: string;
160
- }>;
158
+ spawn(req: SubAgentSpawnRequest): Promise<SubAgentSpawnResult>;
161
159
  /**
162
160
  * Whether a child session id exists on disk. Lets AgentSendInput resume an
163
161
  * agent across a process restart (when the in-memory registry is empty but
@@ -173,6 +171,12 @@ export interface SubAgentSpawner {
173
171
  permissionMode: string;
174
172
  };
175
173
  }
174
+ export interface SubAgentSpawnResult {
175
+ text: string;
176
+ sessionId: string;
177
+ /** Complete billed usage returned by the child Engine run. */
178
+ usage?: TokenUsage;
179
+ }
176
180
  /**
177
181
  * The full context object handed to every tool invocation.
178
182
  *
@@ -184,6 +188,15 @@ export interface ToolVisibilityContext {
184
188
  hasGoal: boolean;
185
189
  settingsScope?: import("../settings/manager.js").SettingsScope;
186
190
  }
191
+ export interface ExternalFileChangesRecord {
192
+ jobId: string;
193
+ description: string;
194
+ cli: "claude" | "codex";
195
+ cwd: string;
196
+ status: "completed" | "failed" | "cancelled";
197
+ changedFiles: string[];
198
+ originClientMessageId?: string;
199
+ }
187
200
  export interface ToolContext {
188
201
  /** Active working directory for this Engine. */
189
202
  cwd: string;
@@ -238,6 +251,14 @@ export interface ToolContext {
238
251
  * headless/test mode; tools must tolerate absence.
239
252
  */
240
253
  streamCallback?: StreamCallback;
254
+ /**
255
+ * Report provider usage produced by a tool or child Engine into the owning
256
+ * session and run-scoped Goal budget. This must not invoke the process-wide
257
+ * billing hook again; the originating provider client already did that.
258
+ */
259
+ recordBilledUsage?: (usage: TokenUsage) => void;
260
+ /** Connection-scoped approval owner router supplied by the protocol host. */
261
+ approvalRouter?: ApprovalRouter;
241
262
  /** Whether the owning Engine is currently in plan mode. Replaces the
242
263
  * removed module-level `isInPlanMode()` singleton. */
243
264
  planMode: boolean;
@@ -261,6 +282,15 @@ export interface ToolContext {
261
282
  * narrow context, or standalone tool tests. (B2 — Gate 1, standard §S3.)
262
283
  */
263
284
  sessionId?: string;
285
+ /** Stable id of the real user turn that owns async work launched here. */
286
+ originClientMessageId?: string;
287
+ /**
288
+ * Persist file attribution from an async external DriveAgent completion in
289
+ * the owning CodeShell transcript. The callback closes over the live parent
290
+ * transcript, so a background job can record its files after the run that
291
+ * launched it has already returned.
292
+ */
293
+ recordExternalFileChanges?: (record: ExternalFileChangesRecord) => void;
264
294
  /**
265
295
  * Skill names the user has hidden from the LLM (full namespaced names
266
296
  * for plugin skills, e.g. "docs:pdf"). The skill builtin tool uses
@@ -9,7 +9,7 @@ import { logger as rootLogger, getCurrentSid } from "../logging/logger.js";
9
9
  import { recordToolCall, recordToolResult } from "../logging/session-recorder.js";
10
10
  import { validateToolArgs } from "./validation.js";
11
11
  import { PLAN_MODE_ALLOWED_TOOLS } from "./plan-mode-allowlist.js";
12
- import { enforcePathPolicyWithApproval } from "./path-policy.js";
12
+ import { attachFinalWritePathSnapshot, enforcePathPolicyWithApproval, } from "./path-policy.js";
13
13
  import { parsePatch } from "./builtin/apply-patch/parser.js";
14
14
  import { BUILTIN_TOOL_GUARDS } from "./builtin/index.js";
15
15
  import { COMPLETE_GOAL_TOOL_NAME } from "./builtin/complete-goal.js";
@@ -244,6 +244,7 @@ export class ToolExecutor {
244
244
  toolName: call.toolName,
245
245
  args: call.args,
246
246
  toolCallId: call.id,
247
+ signal: this.signal ?? this.toolCtx?.signal,
247
248
  });
248
249
  if (hookResult.decision === "deny") {
249
250
  return {
@@ -280,6 +281,16 @@ export class ToolExecutor {
280
281
  // path surface on RegisteredTool.pathPolicy; the executor enforces it
281
282
  // after hooks have had a chance to rewrite args, but before permission
282
283
  // classification or the handler can touch the filesystem.
284
+ if ((call.toolName === "Write" || call.toolName === "Edit") &&
285
+ this.toolCtx?.cwd &&
286
+ typeof call.args.file_path === "string") {
287
+ const raw = call.args.file_path;
288
+ const target = isAbsolutePath(raw) ? raw : resolvePath(this.toolCtx.cwd, raw);
289
+ call = {
290
+ ...call,
291
+ args: attachFinalWritePathSnapshot(call.args, target, this.toolCtx.cwd),
292
+ };
293
+ }
283
294
  const pathPolicyError = await this.enforceDeclaredPathPolicy(toolDef, call.args);
284
295
  if (pathPolicyError) {
285
296
  return {
@@ -325,6 +336,7 @@ export class ToolExecutor {
325
336
  args: call.args,
326
337
  toolCallId: call.id,
327
338
  classifierDecision,
339
+ signal: this.signal ?? this.toolCtx?.signal,
328
340
  });
329
341
  const preClamped = clampHookDecision(classifierDecision, hookResult.decision);
330
342
  let decision = preClamped.decision;
@@ -383,6 +395,7 @@ export class ToolExecutor {
383
395
  toolName: call.toolName,
384
396
  args: call.args,
385
397
  toolCallId: call.id,
398
+ signal: this.signal ?? this.toolCtx?.signal,
386
399
  });
387
400
  // 3. Execute. Use a span so begin/end share one cat and end carries
388
401
  // duration_ms; widen args truncation from 200→2000 so Edit/Write payloads
@@ -431,25 +444,25 @@ export class ToolExecutor {
431
444
  result.id = call.id;
432
445
  // Prepend any non-blocking guard reminder onto a successful result so the
433
446
  // model sees it on its next turn alongside the content it just fetched.
434
- if (guardDecision?.prepend && !result.error && result.result) {
447
+ if (guardDecision?.prepend && !result.isError && result.result) {
435
448
  result.result = `${guardDecision.prepend}\n${result.result}`;
436
449
  }
437
450
  const observerResult = toolResultDisplayText(result);
438
451
  const payload = observerResult ?? result.error ?? "";
439
452
  span.end({
440
- ok: !result.error,
453
+ ok: !result.isError,
441
454
  chars: payload.length,
442
455
  // Snippet only on failure — successful tool output can be huge and is
443
456
  // already on the transcript. Failures are rare and the first 500 chars
444
457
  // usually contain the message we need.
445
- ...(result.error ? { errorSnippet: payload.slice(0, 500) } : {}),
458
+ ...(result.isError ? { errorSnippet: payload.slice(0, 500) } : {}),
446
459
  });
447
460
  recordToolResult(sid, {
448
461
  id: call.id,
449
462
  toolName: call.toolName,
450
- ok: !result.error,
463
+ ok: !result.isError,
451
464
  durationMs: Date.now() - toolStartedAt,
452
- output: result.error ? undefined : observerResult,
465
+ output: result.isError ? undefined : observerResult,
453
466
  error: result.error,
454
467
  });
455
468
  // 4. Post-tool hook
@@ -458,6 +471,7 @@ export class ToolExecutor {
458
471
  toolCallId: call.id,
459
472
  result: observerResult,
460
473
  error: result.error,
474
+ signal: this.signal ?? this.toolCtx?.signal,
461
475
  });
462
476
  // 5. Post-tool-use hook (after execution, can observe/modify result)
463
477
  const postHook = await this.hooks.emit("post_tool_use", {
@@ -465,13 +479,14 @@ export class ToolExecutor {
465
479
  toolCallId: call.id,
466
480
  result: observerResult,
467
481
  error: result.error,
482
+ signal: this.signal ?? this.toolCtx?.signal,
468
483
  });
469
484
  // Append handler-supplied context (linter output, type-check result,
470
485
  // etc.) onto the tool result so the model sees it on the next turn.
471
486
  // Tagged with a separator so the model can tell hook output from
472
487
  // tool output. Skip when the tool errored — additional context on a
473
488
  // failed tool would be confusing.
474
- if (postHook.additionalContext && !result.error) {
489
+ if (postHook.additionalContext && !result.isError) {
475
490
  const tag = "--- additional context from post_tool_use hook ---";
476
491
  result.result = result.result
477
492
  ? `${result.result}\n\n${tag}\n${postHook.additionalContext}`
@@ -484,10 +499,11 @@ export class ToolExecutor {
484
499
  });
485
500
  }
486
501
  // 6. file_changed hook for Write/Edit tools
487
- if ((call.toolName === "Write" || call.toolName === "Edit") && !result.error) {
502
+ if ((call.toolName === "Write" || call.toolName === "Edit") && !result.isError) {
488
503
  await this.hooks.emit("file_changed", {
489
504
  toolName: call.toolName,
490
505
  filePath: call.args.file_path,
506
+ signal: this.signal ?? this.toolCtx?.signal,
491
507
  });
492
508
  }
493
509
  return result;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
7
7
  import type { MCPServerConfig, RegisteredTool } from "../types.js";
8
+ import type { CredentialType } from "../credentials/types.js";
8
9
  import { ToolRegistry } from "./registry.js";
9
10
  import { type CredentialAccess } from "../credentials/access.js";
10
11
  interface MCPResourceInfo {
@@ -21,6 +22,16 @@ interface MCPResourceInfo {
21
22
  * and the env var.
22
23
  */
23
24
  export declare function readRequiredEnv(serverName: string, field: string, envName: string): string;
25
+ export interface ResolvedMcpCredential {
26
+ secret: string;
27
+ type?: CredentialType;
28
+ label?: string;
29
+ }
30
+ export type HttpCredentialResolverResult = string | ResolvedMcpCredential | undefined;
31
+ export interface BuildHttpHeadersOptions {
32
+ now?: () => number;
33
+ oauthRefreshSkewMs?: number;
34
+ }
24
35
  /**
25
36
  * Build the spawned stdio server's environment. Priority (lowest → highest):
26
37
  * a minimal inherited allowlist < forwarded `envVars` (read from process.env by
@@ -34,8 +45,15 @@ export declare function buildStdioEnv(serverName: string, config: MCPServerConfi
34
45
  * base; env-sourced secrets (`bearerTokenEnvVar`, `envHeaders`) layer on top
35
46
  * and win on conflict. Pure + exported for unit testing.
36
47
  */
37
- export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) => string | undefined): Record<string, string>;
38
- export declare function buildHttpHeadersWithCredentialAccess(serverName: string, config: MCPServerConfig, access?: Pick<CredentialAccess, "resolveValue">): Promise<Record<string, string>>;
48
+ export declare function buildHttpHeaders(serverName: string, config: MCPServerConfig, resolveCredential?: (id: string) => HttpCredentialResolverResult, options?: BuildHttpHeadersOptions): Record<string, string>;
49
+ export declare function bearerTokenFromMcpCredential(serverName: string, credentialId: string, credential: ResolvedMcpCredential, options?: BuildHttpHeadersOptions): string;
50
+ export declare function buildHttpHeadersWithCredentialAccess(serverName: string, config: MCPServerConfig, access?: Pick<CredentialAccess, "resolveValue"> & Partial<Pick<CredentialAccess, "resolveMeta">>, options?: BuildHttpHeadersOptions): Promise<Record<string, string>>;
51
+ /**
52
+ * Fetch adapter for long-lived HTTP MCP transports. OAuth access tokens are
53
+ * resolved for every request, refreshed by the host inside the skew window,
54
+ * and force-refreshed once after a replayable 401.
55
+ */
56
+ export declare function createMcpAuthenticatedFetch(serverName: string, config: MCPServerConfig, access?: CredentialAccess, baseFetch?: typeof fetch): typeof fetch;
39
57
  /**
40
58
  * Infer the transport when the config doesn't name one: a url-only entry is
41
59
  * HTTP, everything else stdio. This is the CC `.mcp.json` convention —