@vibe-lark/larkpal 0.1.91-issue70.0 → 0.1.91-issue71.0

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/README.md CHANGED
@@ -93,60 +93,9 @@ lark-cli auth login
93
93
  | `LARKPAL_ENCRYPTION_KEY` | 用户凭证加密密钥(32字节 hex) | 未设置(明文,仅限开发) |
94
94
  | `LARKPAL_PLUGINS` | 声明式加载的插件列表 | 空 |
95
95
  | `LARKPAL_CHAT_JWT_SECRET` | Chat API JWT 签名密钥 | fallback 到 `LARKPAL_API_SECRET` |
96
- | `LARKPAL_BUSINESS_EVENT_CALLBACK_URL` | 飞书长连接业务事件回调地址,用于固定菜单和业务卡片 action | 空 |
97
96
 
98
97
  会话工作目录默认写入 `LARKPAL_WORKSPACE` 下的隐藏目录:飞书会话为 `.chats/`,通用 Agent 会话为 `.sessions/`,Chat API 历史为 `.chat-history/`。
99
98
 
100
- ### Business Event Callback
101
-
102
- 当配置 `LARKPAL_BUSINESS_EVENT_CALLBACK_URL` 时,LarkPal 会把内置处理器未消费的业务事件 POST 到该地址。典型来源包括飞书固定机器人菜单 `application.bot.menu_v6` 和业务自定义 `card.action.trigger`。回调响应可以让 LarkPal 在触发会话中发送可见反馈;业务系统拥有 workflow 语义和卡片内容,LarkPal 只负责 Feishu transport。
103
-
104
- **文本消息响应**
105
-
106
- ```json
107
- { "kind": "message", "text": "配方草稿已创建,请继续填写。" }
108
- ```
109
-
110
- 兼容格式:
111
-
112
- ```json
113
- { "message": { "text": "配方草稿已创建,请继续填写。" } }
114
- ```
115
-
116
- **互动卡片响应**
117
-
118
- LarkPal 原样发送 `card` payload,不重写业务 action id。
119
-
120
- ```json
121
- {
122
- "kind": "card",
123
- "card": {
124
- "schema": "2.0",
125
- "body": { "elements": [] }
126
- }
127
- }
128
- ```
129
-
130
- 兼容格式:
131
-
132
- ```json
133
- { "card": { "schema": "2.0", "body": { "elements": [] } } }
134
- ```
135
-
136
- **轻量提示响应**
137
-
138
- 对于不支持直接 toast 的事件(如固定菜单),LarkPal 会降级发送一条可见文本消息,避免用户点击后无反馈。
139
-
140
- ```json
141
- { "kind": "toast", "toast": { "type": "warning", "content": "请先上传实验文档。" } }
142
- ```
143
-
144
- 兼容格式:
145
-
146
- ```json
147
- { "toast": { "type": "warning", "content": "请先上传实验文档。" } }
148
- ```
149
-
150
99
  ### larkpal-agent Runtime 专用(LARKPAL_RUNTIME=larkpal-agent 时必填)
151
100
 
152
101
  | 变量名 | 用途 | 示例 |
package/dist/cli.mjs CHANGED
@@ -73,17 +73,17 @@ async function cli() {
73
73
  process.exit(exitCode);
74
74
  }
75
75
  if (command === "meeting-live-smoke") {
76
- const { runMeetingLiveSmokeCli } = await import("./live-smoke-DsR13pI9.mjs").then((n) => n.t);
76
+ const { runMeetingLiveSmokeCli } = await import("./live-smoke-CukAuAsd.mjs").then((n) => n.t);
77
77
  await runMeetingLiveSmokeCli(args.slice(1));
78
78
  return;
79
79
  }
80
80
  if (command === "meeting-live-teammate") {
81
- const { runMeetingLiveTeammateCli } = await import("./live-smoke-DsR13pI9.mjs").then((n) => n.t);
81
+ const { runMeetingLiveTeammateCli } = await import("./live-smoke-CukAuAsd.mjs").then((n) => n.t);
82
82
  await runMeetingLiveTeammateCli(args.slice(1));
83
83
  return;
84
84
  }
85
85
  if (command === "meeting-live-check") {
86
- const { runMeetingLiveCheckCli } = await import("./live-smoke-DsR13pI9.mjs").then((n) => n.t);
86
+ const { runMeetingLiveCheckCli } = await import("./live-smoke-CukAuAsd.mjs").then((n) => n.t);
87
87
  const exitCode = await runMeetingLiveCheckCli(args.slice(1));
88
88
  process.exit(exitCode);
89
89
  }
@@ -1,19 +1,17 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
2
  import { t as larkLogger } from "./lark-logger-D7_pEVQc.mjs";
3
3
  import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, extname, join, resolve } from "node:path";
4
+ import path, { basename, dirname, extname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
- import { homedir, tmpdir } from "node:os";
6
+ import { copyFile, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
7
+ import os, { homedir, tmpdir } from "node:os";
8
8
  import { execFileSync, spawn } from "node:child_process";
9
9
  import { createHash, randomUUID } from "node:crypto";
10
10
  import { createInterface } from "node:readline";
11
11
  import { v4, v5 } from "uuid";
12
- import { readFile as readFile$1, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
12
+ import { readFile as readFile$1, writeFile as writeFile$1 } from "fs/promises";
13
13
  import { existsSync as existsSync$1 } from "fs";
14
14
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
15
- import path$1 from "path";
16
- import os from "os";
17
15
  import { EventEmitter } from "node:events";
18
16
  import { pipeline } from "node:stream/promises";
19
17
  import { Transform } from "node:stream";
@@ -216,6 +214,29 @@ var CredentialVault = class {
216
214
  }
217
215
  };
218
216
  //#endregion
217
+ //#region src/runtime/tool-policy.ts
218
+ const SIGNAL_NO_REPLY_TOOL = "signal_no_reply";
219
+ function appendDisallowedTool(policy, tool) {
220
+ const disallowedTools = policy?.disallowedTools ?? [];
221
+ if (disallowedTools.some((rule) => rule.tool === tool)) return {
222
+ ...policy,
223
+ allowedTools: policy?.allowedTools ?? [],
224
+ allowedSkills: policy?.allowedSkills ?? [],
225
+ disallowedTools
226
+ };
227
+ return {
228
+ ...policy,
229
+ allowedTools: policy?.allowedTools ?? [],
230
+ allowedSkills: policy?.allowedSkills ?? [],
231
+ disallowedTools: [...disallowedTools, { tool }]
232
+ };
233
+ }
234
+ function serializeDisabledMcpTools(policy) {
235
+ const tools = policy?.disallowedTools?.map((rule) => rule.tool.trim()).filter(Boolean);
236
+ if (!tools?.length) return void 0;
237
+ return [...new Set(tools)].join(",");
238
+ }
239
+ //#endregion
219
240
  //#region src/user/mcp-merge.ts
220
241
  /**
221
242
  * per-user MCP Server 配置合并
@@ -235,28 +256,53 @@ var CredentialVault = class {
235
256
  * - 全局工具(lark-cli、文件操作等)所有用户共享
236
257
  */
237
258
  const log$9 = larkLogger("user/mcp-merge");
259
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
238
260
  /** 全局 MCP 配置路径 */
239
- const GLOBAL_MCP_CONFIG_PATH = path$1.join(os.homedir(), ".claude", "mcp-servers.json");
261
+ const GLOBAL_MCP_CONFIG_PATH = path.join(os.homedir(), ".claude", "mcp-servers.json");
240
262
  /**
241
263
  * 获取 larkpal MCP Server 的可执行路径
242
264
  *
243
265
  * 优先使用 dist/mcp-server.mjs(生产构建),回退到 tsx 直接运行源码(开发模式)
244
266
  */
245
267
  function getLarkpalMcpServerCommand$1() {
246
- const distPath = path$1.resolve(import.meta.dirname ?? __dirname, "..", "dist", "mcp-server.mjs");
247
- if (existsSync$1(distPath)) return {
268
+ const distCandidates = [
269
+ path.resolve(currentDir, "mcp-server.mjs"),
270
+ path.resolve(currentDir, "..", "dist", "mcp-server.mjs"),
271
+ path.resolve(currentDir, "..", "..", "dist", "mcp-server.mjs")
272
+ ];
273
+ for (const distPath of distCandidates) if (existsSync(distPath)) return {
248
274
  command: "node",
249
275
  args: [distPath]
250
276
  };
251
- const srcPath = path$1.resolve(import.meta.dirname ?? __dirname, "..", "src", "mcp-server", "index.ts");
252
- if (existsSync$1(srcPath)) return {
277
+ const srcPath = path.resolve(currentDir, "..", "mcp-server", "index.ts");
278
+ if (existsSync(srcPath)) return {
253
279
  command: "npx",
254
280
  args: ["tsx", srcPath]
255
281
  };
256
282
  return {
283
+ command: "node",
284
+ args: [distCandidates[0] ?? path.resolve(currentDir, "mcp-server.mjs")]
285
+ };
286
+ }
287
+ function getExternalMcpProxyCommand() {
288
+ const distCandidates = [
289
+ path.resolve(currentDir, "mcp-http-proxy.mjs"),
290
+ path.resolve(currentDir, "..", "dist", "mcp-http-proxy.mjs"),
291
+ path.resolve(currentDir, "..", "..", "dist", "mcp-http-proxy.mjs")
292
+ ];
293
+ for (const distPath of distCandidates) if (existsSync(distPath)) return {
257
294
  command: "node",
258
295
  args: [distPath]
259
296
  };
297
+ const srcPath = path.resolve(currentDir, "..", "mcp-http-proxy", "index.ts");
298
+ if (existsSync(srcPath)) return {
299
+ command: "npx",
300
+ args: ["tsx", srcPath]
301
+ };
302
+ return {
303
+ command: "node",
304
+ args: [distCandidates[0] ?? path.resolve(currentDir, "mcp-http-proxy.mjs")]
305
+ };
260
306
  }
261
307
  /**
262
308
  * 构建内置 larkpal MCP Server 配置
@@ -264,8 +310,9 @@ function getLarkpalMcpServerCommand$1() {
264
310
  * 重要: CC 启动 MCP Server 子进程时,env 字段可能是「覆盖」语义(不继承父进程环境变量)。
265
311
  * 因此必须将所有关键变量显式传递,特别是 PATH 和 HOME。
266
312
  */
267
- function getBuiltinLarkpalConfig(sessionId, cwd) {
313
+ function getBuiltinLarkpalConfig(sessionId, cwd, policy) {
268
314
  const { command, args } = getLarkpalMcpServerCommand$1();
315
+ const disabledTools = serializeDisabledMcpTools(policy);
269
316
  return {
270
317
  command,
271
318
  args,
@@ -275,20 +322,59 @@ function getBuiltinLarkpalConfig(sessionId, cwd) {
275
322
  NODE_PATH: process.env["NODE_PATH"] || "",
276
323
  LARKPAL_MCP_SESSION_ID: sessionId,
277
324
  ...cwd ? { LARKPAL_MCP_CWD: cwd } : {},
325
+ ...disabledTools ? { LARKPAL_MCP_DISABLED_TOOLS: disabledTools } : {},
278
326
  LARKPAL_GATEWAY_PORT: process.env["LARKPAL_GATEWAY_PORT"] || "3000"
279
327
  }
280
328
  };
281
329
  }
330
+ function getExternalMcpProviderConfig(provider) {
331
+ const { command, args } = getExternalMcpProxyCommand();
332
+ return {
333
+ command,
334
+ args,
335
+ env: {
336
+ PATH: process.env["PATH"] || "/usr/local/bin:/usr/bin:/bin",
337
+ HOME: process.env["HOME"] || "/home/larkpal",
338
+ NODE_PATH: process.env["NODE_PATH"] || "",
339
+ LARKPAL_EXTERNAL_MCP_NAME: provider.name,
340
+ LARKPAL_EXTERNAL_MCP_URL: provider.url,
341
+ ...provider.headers ? { LARKPAL_EXTERNAL_MCP_HEADERS_JSON: JSON.stringify(provider.headers) } : {},
342
+ ...provider.headerEnv ? { LARKPAL_EXTERNAL_MCP_HEADER_ENV_JSON: JSON.stringify(provider.headerEnv) } : {},
343
+ ...provider.headersJsonEnv ? { LARKPAL_EXTERNAL_MCP_HEADERS_JSON_ENV: provider.headersJsonEnv } : {},
344
+ ...provider.toolAllowlist?.length ? { LARKPAL_EXTERNAL_MCP_TOOL_ALLOWLIST: provider.toolAllowlist.join(",") } : {}
345
+ }
346
+ };
347
+ }
348
+ function buildExternalMcpConfig(providers) {
349
+ const result = {};
350
+ for (const provider of providers ?? []) {
351
+ const name = provider.name.trim();
352
+ const url = provider.url.trim();
353
+ if (!name || !url) {
354
+ log$9.warn("外部 MCP Provider 配置缺少 name 或 url,已跳过", {
355
+ name: provider.name,
356
+ url: provider.url
357
+ });
358
+ continue;
359
+ }
360
+ result[name] = getExternalMcpProviderConfig({
361
+ ...provider,
362
+ name,
363
+ url
364
+ });
365
+ }
366
+ return result;
367
+ }
282
368
  /**
283
369
  * 合并全局、内置和用户 MCP 配置,写入临时文件
284
370
  *
285
371
  * @returns 临时文件路径,用于 --mcp-config 参数(总是返回有效路径)
286
372
  */
287
- async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
288
- const builtinConfig = { larkpal: getBuiltinLarkpalConfig(sessionId, cwd) };
373
+ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd, policy, externalProviders) {
374
+ const builtinConfig = { larkpal: getBuiltinLarkpalConfig(sessionId, cwd, policy) };
289
375
  let globalConfig = {};
290
- if (existsSync$1(GLOBAL_MCP_CONFIG_PATH)) try {
291
- const raw = await readFile$1(GLOBAL_MCP_CONFIG_PATH, "utf-8");
376
+ if (existsSync(GLOBAL_MCP_CONFIG_PATH)) try {
377
+ const raw = await readFile(GLOBAL_MCP_CONFIG_PATH, "utf-8");
292
378
  globalConfig = JSON.parse(raw);
293
379
  log$9.info("全局 MCP 配置加载完成", { serverCount: Object.keys(globalConfig).length });
294
380
  } catch (err) {
@@ -299,9 +385,9 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
299
385
  }
300
386
  let userConfig = {};
301
387
  if (userCtx) {
302
- const userMcpPath = path$1.join(userCtx.credentialDir, "mcp-servers.json");
303
- if (existsSync$1(userMcpPath)) try {
304
- const raw = await readFile$1(userMcpPath, "utf-8");
388
+ const userMcpPath = path.join(userCtx.credentialDir, "mcp-servers.json");
389
+ if (existsSync(userMcpPath)) try {
390
+ const raw = await readFile(userMcpPath, "utf-8");
305
391
  userConfig = JSON.parse(raw);
306
392
  log$9.info("用户 MCP 配置加载完成", {
307
393
  userId: userCtx.userId,
@@ -316,20 +402,23 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
316
402
  });
317
403
  }
318
404
  }
405
+ const externalConfig = buildExternalMcpConfig(externalProviders);
319
406
  const merged = {
320
407
  ...builtinConfig,
321
408
  ...globalConfig,
322
- ...userConfig
409
+ ...userConfig,
410
+ ...externalConfig
323
411
  };
324
412
  log$9.info("MCP 配置合并完成", {
325
413
  userId: userCtx?.userId,
326
414
  builtinServers: Object.keys(builtinConfig),
327
415
  globalServers: Object.keys(globalConfig),
328
416
  userServers: Object.keys(userConfig),
417
+ externalServers: Object.keys(externalConfig),
329
418
  mergedServers: Object.keys(merged)
330
419
  });
331
- const tmpPath = path$1.join(os.tmpdir(), `mcp-${sessionId}.json`);
332
- await writeFile$1(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
420
+ const tmpPath = path.join(os.tmpdir(), `mcp-${sessionId}.json`);
421
+ await writeFile(tmpPath, JSON.stringify({ mcpServers: merged }, null, 2), "utf-8");
333
422
  log$9.info("MCP 合并配置已写入临时文件", {
334
423
  path: tmpPath,
335
424
  serverCount: Object.keys(merged).length
@@ -340,10 +429,10 @@ async function mergeAndWriteMcpConfig(userCtx, sessionId, cwd) {
340
429
  * 清理 MCP 临时配置文件
341
430
  */
342
431
  async function cleanupMcpConfig(sessionId) {
343
- const tmpPath = path$1.join(os.tmpdir(), `mcp-${sessionId}.json`);
432
+ const tmpPath = path.join(os.tmpdir(), `mcp-${sessionId}.json`);
344
433
  try {
345
- if (existsSync$1(tmpPath)) {
346
- await unlink$1(tmpPath);
434
+ if (existsSync(tmpPath)) {
435
+ await unlink(tmpPath);
347
436
  log$9.debug("MCP 临时配置文件已清理", { path: tmpPath });
348
437
  }
349
438
  } catch {}
@@ -478,6 +567,8 @@ const log$8 = larkLogger("cc-runtime/stream-parser");
478
567
  const MAX_TOOL_RESULT_CONTENT_CHARS = 8e3;
479
568
  var CCStreamParser = class extends EventEmitter {
480
569
  toolNamesById = /* @__PURE__ */ new Map();
570
+ emittedToolUseIds = /* @__PURE__ */ new Set();
571
+ emittedToolUseFingerprints = /* @__PURE__ */ new Set();
481
572
  declaredTools = null;
482
573
  /**
483
574
  * 解析一行 NDJSON 文本
@@ -556,12 +647,21 @@ var CCStreamParser = class extends EventEmitter {
556
647
  });
557
648
  if (block?.type === "tool_use" && block.name) {
558
649
  if (!this.recordToolUse(block.name, block.id, block.input)) break;
650
+ if (!hasDurableToolInput(block.input)) {
651
+ log$8.debug("工具调用 start 仅含草稿参数,等待完整 assistant 消息", {
652
+ toolName: block.name,
653
+ toolUseId: block.id
654
+ });
655
+ break;
656
+ }
657
+ if (this.hasEmittedToolUseStart(block.name, block.input, block.id)) break;
559
658
  log$8.info("工具调用开始", {
560
659
  toolName: block.name,
561
660
  displayName: getToolDisplayName(block.name),
562
661
  toolUseId: block.id
563
662
  });
564
- this.emit("toolUseStart", block.name, block.input ?? {}, block.id);
663
+ this.markToolUseStartEmitted(block.name, block.input, block.id);
664
+ this.emit("toolUseStart", block.name, block.input, block.id);
565
665
  }
566
666
  break;
567
667
  }
@@ -604,11 +704,13 @@ var CCStreamParser = class extends EventEmitter {
604
704
  if (Array.isArray(content)) {
605
705
  for (const block of content) if (block.type === "tool_use" && block.name) {
606
706
  if (!this.recordToolUse(block.name, block.id, block.input)) continue;
707
+ if (this.hasEmittedToolUseStart(block.name, block.input, block.id)) continue;
607
708
  log$8.info("从 assistant 消息提取工具调用", {
608
709
  toolName: block.name,
609
710
  displayName: getToolDisplayName(block.name),
610
711
  toolUseId: block.id
611
712
  });
713
+ this.markToolUseStartEmitted(block.name, block.input, block.id);
612
714
  this.emit("toolUseStart", block.name, block.input ?? {}, block.id);
613
715
  }
614
716
  }
@@ -651,6 +753,14 @@ var CCStreamParser = class extends EventEmitter {
651
753
  }
652
754
  return true;
653
755
  }
756
+ hasEmittedToolUseStart(toolName, toolInput, toolUseId) {
757
+ if (toolUseId && this.emittedToolUseIds.has(toolUseId)) return true;
758
+ return this.emittedToolUseFingerprints.has(fingerprintToolUse(toolName, toolInput));
759
+ }
760
+ markToolUseStartEmitted(toolName, toolInput, toolUseId) {
761
+ if (toolUseId) this.emittedToolUseIds.add(toolUseId);
762
+ this.emittedToolUseFingerprints.add(fingerprintToolUse(toolName, toolInput));
763
+ }
654
764
  buildToolResultSummary(item, toolUseResult) {
655
765
  const serializedContent = serializeToolResultContent(item.content);
656
766
  const truncatedContent = truncateToolResultContent(serializedContent);
@@ -673,9 +783,10 @@ var CCStreamParser = class extends EventEmitter {
673
783
  handleToolProgress(msg) {
674
784
  log$8.debug("工具执行进度", {
675
785
  toolName: msg.tool_name,
786
+ toolUseId: msg.tool_use_id,
676
787
  elapsed: msg.elapsed_time_seconds
677
788
  });
678
- this.emit("toolProgress", msg.tool_name, msg.elapsed_time_seconds);
789
+ this.emit("toolProgress", msg.tool_name, msg.elapsed_time_seconds, msg.tool_use_id);
679
790
  }
680
791
  /**
681
792
  * 处理 result 消息 — CC 执行完成的最终结果
@@ -698,6 +809,19 @@ var CCStreamParser = class extends EventEmitter {
698
809
  });
699
810
  }
700
811
  };
812
+ function hasDurableToolInput(toolInput) {
813
+ if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) return false;
814
+ return Object.keys(toolInput).length > 0;
815
+ }
816
+ function fingerprintToolUse(toolName, toolInput) {
817
+ return `${toolName}:${stableJsonStringify(toolInput)}`;
818
+ }
819
+ function stableJsonStringify(value) {
820
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? String(value);
821
+ if (Array.isArray(value)) return `[${value.map((item) => stableJsonStringify(item)).join(",")}]`;
822
+ const input = value;
823
+ return `{${Object.keys(input).sort().map((key) => `${JSON.stringify(key)}:${stableJsonStringify(input[key])}`).join(",")}}`;
824
+ }
701
825
  function serializeToolResultContent(content) {
702
826
  if (typeof content === "string") return content;
703
827
  return safeJsonStringify(content);
@@ -1211,7 +1335,7 @@ var SessionProcessManager = class {
1211
1335
  if (config.model) args.push("--model", config.model);
1212
1336
  args.push("--effort", CC_EFFORT);
1213
1337
  try {
1214
- const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId, cwd);
1338
+ const mcpConfigPath = await mergeAndWriteMcpConfig(config.userContext && !config.userContext.isTenantIdentity ? config.userContext : null, sessionId, cwd, config.policy, config.externalMcpProviders);
1215
1339
  args.push("--mcp-config", mcpConfigPath);
1216
1340
  log$6.info("MCP 配置已合并并注入到 CC 进程", {
1217
1341
  sessionId,
@@ -1442,8 +1566,8 @@ var SessionProcessManager = class {
1442
1566
  summary: this.lastMultimodalSummaries.get(sessionId)
1443
1567
  }));
1444
1568
  });
1445
- parser.on("toolProgress", (toolName, elapsedSeconds) => {
1446
- getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds);
1569
+ parser.on("toolProgress", (toolName, elapsedSeconds, toolUseId) => {
1570
+ getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds, toolUseId);
1447
1571
  });
1448
1572
  parser.on("turnEnd", (stopReason) => {
1449
1573
  log$6.info("[perf] turnEnd", {
@@ -2420,7 +2544,7 @@ var CodexAppServerAdapter = class {
2420
2544
  return existing;
2421
2545
  }
2422
2546
  const env = this.buildProcessEnv(config);
2423
- const extraArgs = this.buildCodexArgs(config.sessionId, config.cwd);
2547
+ const extraArgs = this.buildCodexArgs(config);
2424
2548
  const client = this.clientFactory({
2425
2549
  sessionId: config.sessionId,
2426
2550
  cwd: config.cwd,
@@ -2574,7 +2698,7 @@ var CodexAppServerAdapter = class {
2574
2698
  const { toolName, toolInput } = this.describeToolItem(params.item);
2575
2699
  if (!toolName) return;
2576
2700
  this.rememberToolName(sessionId, params.item.id, toolName);
2577
- callbacks.onToolUseStart?.(toolName, toolInput);
2701
+ callbacks.onToolUseStart?.(toolName, toolInput, params.item.id);
2578
2702
  }
2579
2703
  handleItemCompleted(sessionId, params) {
2580
2704
  if (params.item.type === "contextCompaction") {
@@ -2605,7 +2729,7 @@ var CodexAppServerAdapter = class {
2605
2729
  }
2606
2730
  handleMcpProgress(sessionId, params) {
2607
2731
  const toolName = this.lookupToolName(sessionId, params.itemId) ?? "mcp";
2608
- this.activeCallbacks.get(sessionId)?.onToolProgress?.(toolName, 0);
2732
+ this.activeCallbacks.get(sessionId)?.onToolProgress?.(toolName, 0, params.itemId);
2609
2733
  log$3.debug("Codex MCP tool progress", {
2610
2734
  sessionId,
2611
2735
  itemId: params.itemId,
@@ -2971,10 +3095,12 @@ var CodexAppServerAdapter = class {
2971
3095
  }).catch(() => {})));
2972
3096
  }
2973
3097
  buildProcessEnv(config) {
3098
+ const disabledTools = serializeDisabledMcpTools(config.policy);
2974
3099
  return {
2975
3100
  ...process.env,
2976
3101
  LARKPAL_MCP_SESSION_ID: config.sessionId,
2977
3102
  LARKPAL_MCP_CWD: config.cwd,
3103
+ ...disabledTools ? { LARKPAL_MCP_DISABLED_TOOLS: disabledTools } : {},
2978
3104
  LARKPAL_USER_ID: config.userContext?.userId ?? process.env.LARKPAL_USER_ID,
2979
3105
  LARKPAL_USER_OPEN_ID: config.userContext?.openId ?? process.env.LARKPAL_USER_OPEN_ID,
2980
3106
  LARKPAL_USER_NAME: config.userContext?.userName ?? process.env.LARKPAL_USER_NAME,
@@ -2982,14 +3108,16 @@ var CodexAppServerAdapter = class {
2982
3108
  LARKPAL_IS_TENANT: config.userContext?.isTenantIdentity ? "1" : process.env.LARKPAL_IS_TENANT
2983
3109
  };
2984
3110
  }
2985
- buildCodexArgs(sessionId, cwd) {
3111
+ buildCodexArgs(config) {
2986
3112
  const { command, args } = getLarkpalMcpServerCommand();
3113
+ const disabledTools = serializeDisabledMcpTools(config.policy);
2987
3114
  const env = {
2988
3115
  PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
2989
3116
  HOME: process.env.HOME || homedir(),
2990
3117
  NODE_PATH: process.env.NODE_PATH || "",
2991
- LARKPAL_MCP_SESSION_ID: sessionId,
2992
- ...cwd ? { LARKPAL_MCP_CWD: cwd } : {},
3118
+ LARKPAL_MCP_SESSION_ID: config.sessionId,
3119
+ ...config.cwd ? { LARKPAL_MCP_CWD: config.cwd } : {},
3120
+ ...disabledTools ? { LARKPAL_MCP_DISABLED_TOOLS: disabledTools } : {},
2993
3121
  LARKPAL_GATEWAY_PORT: process.env.LARKPAL_GATEWAY_PORT || "3000"
2994
3122
  };
2995
3123
  return [
@@ -3005,7 +3133,7 @@ var CodexAppServerAdapter = class {
3005
3133
  if (config.llmConfig) log$3.warn("Codex app-server v1 暂不支持 per-request llmConfig,已忽略", { sessionId: config.sessionId });
3006
3134
  if (config.maxBudgetUsd != null) log$3.warn("Codex app-server v1 暂不支持 maxBudgetUsd,已忽略", { sessionId: config.sessionId });
3007
3135
  if (config.maxTurns != null) log$3.warn("Codex app-server v1 暂不支持 maxTurns,已忽略", { sessionId: config.sessionId });
3008
- if (config.policy?.allowedTools || config.policy?.disallowedTools) log$3.warn("Codex app-server v1 暂不强制执行 tool allow/deny policy,已忽略", { sessionId: config.sessionId });
3136
+ if (config.policy?.allowedTools?.length) log$3.warn("Codex app-server v1 暂不强制执行 tool allow policy,已忽略", { sessionId: config.sessionId });
3009
3137
  }
3010
3138
  createCompletionPromise(sessionId) {
3011
3139
  return new Promise((resolve) => {
@@ -4127,9 +4255,11 @@ function buildAgentRuntimeConfig(input) {
4127
4255
  });
4128
4256
  const policy = buildExecutionPolicy(input.policy, {
4129
4257
  maxTurns: input.maxTurns,
4130
- maxBudgetUsd: input.maxBudgetUsd
4258
+ maxBudgetUsd: input.maxBudgetUsd,
4259
+ allowSignalNoReplyTool: input.allowSignalNoReplyTool
4131
4260
  });
4132
4261
  const userContext = buildUserContext(input.identity);
4262
+ const externalMcpProviders = input.externalMcpProviders ?? loadExternalMcpProvidersFromEnv();
4133
4263
  const requestContext = {
4134
4264
  requestId,
4135
4265
  sessionId,
@@ -4180,9 +4310,56 @@ function buildAgentRuntimeConfig(input) {
4180
4310
  authHeaders,
4181
4311
  metadata
4182
4312
  },
4313
+ externalMcpProviders,
4183
4314
  metadata
4184
4315
  };
4185
4316
  }
4317
+ function loadExternalMcpProvidersFromEnv(env = process.env) {
4318
+ const raw = env.LARKPAL_EXTERNAL_MCP_PROVIDERS;
4319
+ if (!raw?.trim()) return void 0;
4320
+ const parsed = JSON.parse(raw);
4321
+ if (!Array.isArray(parsed)) throw new Error("LARKPAL_EXTERNAL_MCP_PROVIDERS must be a JSON array");
4322
+ return parsed.map((entry, index) => normalizeExternalMcpProvider(entry, index));
4323
+ }
4324
+ function normalizeExternalMcpProvider(entry, index) {
4325
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}] must be an object`);
4326
+ const record = entry;
4327
+ return {
4328
+ name: readRequiredString(record.name, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].name`),
4329
+ url: readRequiredString(record.url, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].url`),
4330
+ headers: readStringRecord(record.headers, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].headers`),
4331
+ headerEnv: readStringRecord(record.headerEnv, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].headerEnv`),
4332
+ headersJsonEnv: readOptionalString(record.headersJsonEnv),
4333
+ toolAllowlist: readStringArray(record.toolAllowlist, `LARKPAL_EXTERNAL_MCP_PROVIDERS[${index}].toolAllowlist`)
4334
+ };
4335
+ }
4336
+ function readRequiredString(value, field) {
4337
+ const text = readOptionalString(value);
4338
+ if (!text) throw new Error(`${field} is required`);
4339
+ return text;
4340
+ }
4341
+ function readOptionalString(value) {
4342
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
4343
+ }
4344
+ function readStringRecord(value, field) {
4345
+ if (value === void 0) return void 0;
4346
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object`);
4347
+ const result = {};
4348
+ for (const [key, entry] of Object.entries(value)) {
4349
+ if (typeof entry !== "string") throw new Error(`${field}.${key} must be a string`);
4350
+ result[key] = entry;
4351
+ }
4352
+ return result;
4353
+ }
4354
+ function readStringArray(value, field) {
4355
+ if (value === void 0) return void 0;
4356
+ if (!Array.isArray(value)) throw new Error(`${field} must be an array`);
4357
+ const result = value.map((entry, index) => {
4358
+ if (typeof entry !== "string" || !entry.trim()) throw new Error(`${field}[${index}] must be a non-empty string`);
4359
+ return entry.trim();
4360
+ });
4361
+ return result.length ? result : void 0;
4362
+ }
4186
4363
  function getRunInputValue(...values) {
4187
4364
  return values.find((value) => value !== void 0);
4188
4365
  }
@@ -4298,7 +4475,7 @@ function buildAuthHeaders(params) {
4298
4475
  return headers;
4299
4476
  }
4300
4477
  function buildExecutionPolicy(policy, limits) {
4301
- return {
4478
+ const base = {
4302
4479
  allowedTools: policy?.allowedTools ?? [],
4303
4480
  disallowedTools: policy?.disallowedTools,
4304
4481
  allowedSkills: policy?.allowedSkills ?? [],
@@ -4309,6 +4486,7 @@ function buildExecutionPolicy(policy, limits) {
4309
4486
  maxBudgetUsd: limits.maxBudgetUsd ?? policy?.maxBudgetUsd,
4310
4487
  turnPolicy: policy?.turnPolicy
4311
4488
  };
4489
+ return limits.allowSignalNoReplyTool ? base : appendDisallowedTool(base, SIGNAL_NO_REPLY_TOOL);
4312
4490
  }
4313
4491
  function buildUserContext(identity) {
4314
4492
  if (!identity?.userId && !identity?.openId) return void 0;
@@ -5492,6 +5670,7 @@ async function runFastDecision(params) {
5492
5670
  model: params.model,
5493
5671
  transcriptMode: "ephemeral",
5494
5672
  maxTurns: 1,
5673
+ allowSignalNoReplyTool: true,
5495
5674
  policy: {
5496
5675
  disallowedTools: [{ tool: "ask_user" }, { tool: "request_permission" }],
5497
5676
  maxTurns: 1,