@kiturone/kapibala 0.0.5 → 0.0.6

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/dist/index.cjs CHANGED
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  OpenAIResponsesProvider: () => OpenAIResponsesProvider,
50
50
  PathSandbox: () => PathSandbox,
51
51
  PermissionPolicy: () => PermissionPolicy,
52
+ PreDispatchError: () => PreDispatchError,
52
53
  PromptAssembler: () => PromptAssembler,
53
54
  SESSION_SCHEMA_VERSION: () => SESSION_SCHEMA_VERSION,
54
55
  SKILL_LIMITS: () => SKILL_LIMITS,
@@ -257,12 +258,25 @@ var KapibalaError = class extends Error {
257
258
  code;
258
259
  retryPolicy;
259
260
  safeMessage;
261
+ dispatchOutcome;
260
262
  constructor(message, options) {
261
263
  super(message);
262
264
  this.name = "KapibalaError";
263
265
  this.code = options?.code ?? "INTERNAL_ERROR";
264
266
  this.retryPolicy = options?.retryPolicy ?? "never";
265
267
  this.safeMessage = options?.safeMessage ?? "Operation failed";
268
+ this.dispatchOutcome = options?.dispatchOutcome;
269
+ }
270
+ };
271
+ var PreDispatchError = class extends KapibalaError {
272
+ constructor(message, options) {
273
+ super(message, {
274
+ code: options?.code ?? "PRE_DISPATCH_FAILED",
275
+ retryPolicy: options?.retryPolicy ?? "never",
276
+ safeMessage: options?.safeMessage ?? "Tool execution was aborted before dispatch",
277
+ dispatchOutcome: "not_sent"
278
+ });
279
+ this.name = "PreDispatchError";
266
280
  }
267
281
  };
268
282
  var ModelError = class extends KapibalaError {
@@ -447,6 +461,16 @@ function validateProtocolContent(content) {
447
461
  invalidState();
448
462
  }
449
463
  }
464
+ if (block.type === "tool_result" && "execution" in block && block.execution !== void 0) {
465
+ const ex = block.execution;
466
+ if (!ex || typeof ex !== "object" || ex.version !== 1 || !["not_sent", "succeeded", "failed", "unknown"].includes(ex.dispatchOutcome) || ex.hostError !== void 0 && (!ex.hostError || typeof ex.hostError !== "object" || !["validation", "projection", "hook", "audit"].includes(ex.hostError.phase) || typeof ex.hostError.code !== "string" || !ex.hostError.code))
467
+ invalidState();
468
+ }
469
+ if (block.type === "text" && "promptSource" in block && block.promptSource !== void 0) {
470
+ const ps = block.promptSource;
471
+ if (!ps || typeof ps !== "object" || ps.version !== 1 || typeof ps.server !== "string" || !ps.server || typeof ps.name !== "string" || !ps.name || ps.scope !== void 0 && typeof ps.scope !== "string" || ps.contentDigest !== void 0 && typeof ps.contentDigest !== "string" || ps.configIdentity !== void 0 && typeof ps.configIdentity !== "string")
472
+ invalidState();
473
+ }
450
474
  validatedBlocks.set(block, true);
451
475
  }
452
476
  validatedContents.set(content, true);
@@ -564,7 +588,11 @@ function normalizeHistory(input) {
564
588
  content: "Tool execution was interrupted or crashed in previous session",
565
589
  isError: true,
566
590
  errorCode: "OUTCOME_UNKNOWN",
567
- retryPolicy: "after_user_action"
591
+ retryPolicy: "after_user_action",
592
+ execution: {
593
+ version: 1,
594
+ dispatchOutcome: "unknown"
595
+ }
568
596
  };
569
597
  content.push(repair);
570
598
  result.repairs.push({
@@ -4848,7 +4876,8 @@ var CAPABILITIES = /* @__PURE__ */ new Set([
4848
4876
  "exec",
4849
4877
  "net:outbound",
4850
4878
  "env:read",
4851
- "agent:spawn"
4879
+ "agent:spawn",
4880
+ "external:call"
4852
4881
  ]);
4853
4882
  function validatePermissionRules(rules) {
4854
4883
  for (const [index2, rule] of rules.entries()) {
@@ -4961,7 +4990,8 @@ var SessionApprovalCache = class {
4961
4990
  [...request.capabilities].sort(),
4962
4991
  request.input,
4963
4992
  request.rootDir,
4964
- request.shell ?? null
4993
+ request.shell ?? null,
4994
+ request.executionIdentity ?? null
4965
4995
  ])
4966
4996
  ).digest("hex");
4967
4997
  }
@@ -4979,9 +5009,14 @@ var ToolRegistry = class {
4979
5009
  toolSources = /* @__PURE__ */ new Map();
4980
5010
  // toolName -> source
4981
5011
  conflictPolicy;
5012
+ version = 0;
4982
5013
  constructor(options) {
4983
5014
  this.conflictPolicy = options?.conflictPolicy ?? "error";
4984
5015
  }
5016
+ /** 获取当前注册表版本号;每次有效变更(注册、替换、注销)单调自增。 */
5017
+ get currentVersion() {
5018
+ return this.version;
5019
+ }
4985
5020
  register(tool, opts) {
4986
5021
  const source = opts?.source ?? tool.metadata?.source ?? "builtin";
4987
5022
  const existing = this.tools.get(tool.name);
@@ -4997,27 +5032,93 @@ var ToolRegistry = class {
4997
5032
  }
4998
5033
  this.tools.set(tool.name, tool);
4999
5034
  this.toolSources.set(tool.name, source);
5035
+ this.version++;
5000
5036
  }
5001
- registerSource(source, tools) {
5037
+ /**
5038
+ * 原子替换指定来源的全部工具:
5039
+ * 1. 预检候选批内自身重名:若候选批次内包含同名工具,整批拒绝;
5040
+ * 2. 预检跨来源冲突:若候选工具被其它来源占用且策略不允许覆盖,整批拒绝;
5041
+ * 3. 同步原子提交:预检全部通过后一次性清理该来源旧工具并写入新工具;失败时完全保持旧状态不变。
5042
+ */
5043
+ replaceSource(source, tools) {
5044
+ const batchNames = /* @__PURE__ */ new Set();
5045
+ for (const tool of tools) {
5046
+ if (batchNames.has(tool.name)) {
5047
+ throw new Error(
5048
+ `Invalid candidate tools: duplicate tool name '${tool.name}' within source batch '${source}'`
5049
+ );
5050
+ }
5051
+ batchNames.add(tool.name);
5052
+ }
5002
5053
  for (const tool of tools) {
5003
- this.register(tool, { source });
5054
+ const existing = this.tools.get(tool.name);
5055
+ const existingSource = this.toolSources.get(tool.name);
5056
+ if (existing && existingSource !== source) {
5057
+ if (this.conflictPolicy === "error") {
5058
+ throw new Error(
5059
+ `Tool name conflict: candidate '${tool.name}' from '${source}' conflicts with existing tool registered by '${existingSource}'`
5060
+ );
5061
+ }
5062
+ if (this.conflictPolicy === "prefer-builtin" && existingSource === "builtin") {
5063
+ throw new Error(
5064
+ `Tool name conflict: candidate '${tool.name}' from '${source}' cannot override builtin tool`
5065
+ );
5066
+ }
5067
+ }
5004
5068
  }
5069
+ for (const [name, s] of this.toolSources.entries()) {
5070
+ if (s === source) {
5071
+ this.tools.delete(name);
5072
+ this.toolSources.delete(name);
5073
+ }
5074
+ }
5075
+ for (const tool of tools) {
5076
+ this.tools.set(tool.name, tool);
5077
+ this.toolSources.set(tool.name, source);
5078
+ }
5079
+ this.version++;
5080
+ }
5081
+ /**
5082
+ * 注册指定来源的工具集。
5083
+ *
5084
+ * 语义与约束:
5085
+ * 1. 委托给 `replaceSource` 实现整源原子替换(新集合未包含的该来源旧工具将被注销移除,非增量累加);
5086
+ * 2. 默认禁止跨来源名称冲突(抛出 KapibalaError TOOL_CONFLICT);若 options 配置了 conflictPolicy: 'prefer-last' 则允许覆盖旧来源同名工具;
5087
+ * 3. 预检阶段发现批内同名重复时整批拒绝,绝不破坏注册表原子状态。
5088
+ */
5089
+ registerSource(source, tools) {
5090
+ this.replaceSource(source, tools);
5005
5091
  }
5006
5092
  unregisterSource(source) {
5093
+ let changed = false;
5007
5094
  for (const [name, s] of this.toolSources.entries()) {
5008
5095
  if (s === source) {
5009
5096
  this.tools.delete(name);
5010
5097
  this.toolSources.delete(name);
5098
+ changed = true;
5011
5099
  }
5012
5100
  }
5101
+ if (changed) {
5102
+ this.version++;
5103
+ }
5013
5104
  }
5014
5105
  get(name) {
5015
5106
  return this.tools.get(name);
5016
5107
  }
5108
+ has(name) {
5109
+ return this.tools.has(name);
5110
+ }
5111
+ /** 获取工具对应的来源标记。 */
5112
+ sourceOf(name) {
5113
+ return this.toolSources.get(name);
5114
+ }
5017
5115
  /** 注销指定名称及来源记录;调用方须核对实例身份,不能移除同名替代工具。 */
5018
5116
  unregister(name) {
5019
- this.tools.delete(name);
5020
- this.toolSources.delete(name);
5117
+ if (this.tools.has(name)) {
5118
+ this.tools.delete(name);
5119
+ this.toolSources.delete(name);
5120
+ this.version++;
5121
+ }
5021
5122
  }
5022
5123
  /** 未命中时抛 ToolNotFound,并携带当前全部可用名称辅助模型自纠。 */
5023
5124
  resolve(name) {
@@ -5033,6 +5134,30 @@ var ToolRegistry = class {
5033
5134
  definitions() {
5034
5135
  return this.list().map(toToolDefinition);
5035
5136
  }
5137
+ /**
5138
+ * 捕获当前注册表的不可变只读快照。
5139
+ * 注册表后续的增删改绝不改变该快照的定义列表、工具执行实例与来源版本。
5140
+ */
5141
+ snapshot() {
5142
+ const snapshotTools = new Map(this.tools);
5143
+ const snapshotSources = new Map(this.toolSources);
5144
+ const snapshotVersion = this.version;
5145
+ return {
5146
+ version: snapshotVersion,
5147
+ get: (name) => snapshotTools.get(name),
5148
+ has: (name) => snapshotTools.has(name),
5149
+ resolve: (name) => {
5150
+ const tool = snapshotTools.get(name);
5151
+ if (!tool) {
5152
+ throw new ToolNotFound(name, Array.from(snapshotTools.keys()));
5153
+ }
5154
+ return tool;
5155
+ },
5156
+ list: () => Array.from(snapshotTools.values()),
5157
+ definitions: () => Array.from(snapshotTools.values()).map(toToolDefinition),
5158
+ sourceOf: (name) => snapshotSources.get(name)
5159
+ };
5160
+ }
5036
5161
  };
5037
5162
 
5038
5163
  // src/capabilities/tools/builtin/fs.ts
@@ -5508,14 +5633,18 @@ var ToolExecutor = class {
5508
5633
  call.executionRoot = import_node_path15.default.resolve(this.rootDir);
5509
5634
  const startedAt = Date.now();
5510
5635
  const operationId = createLogId();
5511
- const failure = (message, errorCode, retryPolicy = "never") => ({
5636
+ const failure = (message, errorCode, retryPolicy = "never", execution) => ({
5512
5637
  type: "tool_result",
5513
5638
  toolUseId: call.id,
5514
5639
  content: JSON.stringify({ code: errorCode, retryPolicy, message }),
5515
5640
  isError: true,
5516
5641
  errorCode,
5517
5642
  retryPolicy,
5518
- durationMs: Math.max(0, Date.now() - startedAt)
5643
+ durationMs: Math.max(0, Date.now() - startedAt),
5644
+ execution: execution ?? {
5645
+ version: 1,
5646
+ dispatchOutcome: "not_sent"
5647
+ }
5519
5648
  });
5520
5649
  const audit = async (event, fields = {}) => {
5521
5650
  if (!this.eventLogger || !this.sessionId) return;
@@ -5535,7 +5664,12 @@ var ToolExecutor = class {
5535
5664
  return failure(
5536
5665
  "Audit unavailable; tool was not started",
5537
5666
  "AUDIT_UNAVAILABLE",
5538
- "after_user_action"
5667
+ "after_user_action",
5668
+ {
5669
+ version: 1,
5670
+ dispatchOutcome: "not_sent",
5671
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5672
+ }
5539
5673
  );
5540
5674
  }
5541
5675
  if (forcedDeny) {
@@ -5546,7 +5680,12 @@ var ToolExecutor = class {
5546
5680
  return failure(
5547
5681
  "Audit unavailable; tool was not started",
5548
5682
  "AUDIT_UNAVAILABLE",
5549
- "after_user_action"
5683
+ "after_user_action",
5684
+ {
5685
+ version: 1,
5686
+ dispatchOutcome: "not_sent",
5687
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5688
+ }
5550
5689
  );
5551
5690
  }
5552
5691
  return failure(
@@ -5569,7 +5708,11 @@ var ToolExecutor = class {
5569
5708
  toolUseId: call.id,
5570
5709
  content: decision.result,
5571
5710
  isError: decision.isError ?? false,
5572
- durationMs: Math.max(0, Date.now() - startedAt)
5711
+ durationMs: Math.max(0, Date.now() - startedAt),
5712
+ execution: {
5713
+ version: 1,
5714
+ dispatchOutcome: "not_sent"
5715
+ }
5573
5716
  };
5574
5717
  }
5575
5718
  }
@@ -5579,8 +5722,23 @@ var ToolExecutor = class {
5579
5722
  await audit("tool.finished", { status: "failed" });
5580
5723
  } catch {
5581
5724
  }
5582
- return failure("Tool preparation failed", "HOOK_ERROR");
5725
+ return failure("Tool preparation failed", "HOOK_ERROR", "never", {
5726
+ version: 1,
5727
+ dispatchOutcome: "not_sent",
5728
+ hostError: { phase: "hook", code: "HOOK_ERROR" }
5729
+ });
5583
5730
  }
5731
+ function deepFreeze(obj) {
5732
+ if (obj === null || typeof obj !== "object" || Object.isFrozen(obj)) {
5733
+ return obj;
5734
+ }
5735
+ Object.freeze(obj);
5736
+ for (const key of Object.keys(obj)) {
5737
+ deepFreeze(obj[key]);
5738
+ }
5739
+ return obj;
5740
+ }
5741
+ const finalInput = deepFreeze(structuredClone(currentInput));
5584
5742
  let tool;
5585
5743
  try {
5586
5744
  tool = this.tools.resolve(call.name);
@@ -5593,13 +5751,50 @@ var ToolExecutor = class {
5593
5751
  }
5594
5752
  return failure(
5595
5753
  known?.safeMessage ?? "Requested tool is unavailable",
5596
- known?.code ?? "TOOL_NOT_FOUND"
5754
+ known?.code ?? "TOOL_NOT_FOUND",
5755
+ "never",
5756
+ {
5757
+ version: 1,
5758
+ dispatchOutcome: "not_sent"
5759
+ }
5597
5760
  );
5598
5761
  }
5762
+ try {
5763
+ const validation = tool.validateInput?.(finalInput);
5764
+ if (validation && !validation.valid) {
5765
+ try {
5766
+ await audit("tool.decided", { decision: "deny", decisionSource: "validation" });
5767
+ await audit("tool.finished", { status: "failed", errorCode: "INVALID_INPUT" });
5768
+ } catch {
5769
+ }
5770
+ return failure(
5771
+ validation.message || "Tool input validation failed",
5772
+ "INVALID_INPUT",
5773
+ "never",
5774
+ {
5775
+ version: 1,
5776
+ dispatchOutcome: "not_sent",
5777
+ hostError: { phase: "validation", code: "INVALID_INPUT" }
5778
+ }
5779
+ );
5780
+ }
5781
+ } catch (err) {
5782
+ try {
5783
+ await audit("tool.decided", { decision: "deny", decisionSource: "validation" });
5784
+ await audit("tool.finished", { status: "failed", errorCode: "INVALID_INPUT" });
5785
+ } catch {
5786
+ }
5787
+ const msg = err instanceof Error ? err.message : "Tool input validation error";
5788
+ return failure(msg || "Tool input validation error", "INVALID_INPUT", "never", {
5789
+ version: 1,
5790
+ dispatchOutcome: "not_sent",
5791
+ hostError: { phase: "validation", code: "INVALID_INPUT" }
5792
+ });
5793
+ }
5599
5794
  try {
5600
5795
  return await this.executeResolved(
5601
5796
  call,
5602
- currentInput,
5797
+ finalInput,
5603
5798
  tool,
5604
5799
  startedAt,
5605
5800
  audit,
@@ -5615,14 +5810,18 @@ var ToolExecutor = class {
5615
5810
  return failure(
5616
5811
  "Tool outcome is unknown after an executor failure",
5617
5812
  "OUTCOME_UNKNOWN",
5618
- "after_user_action"
5813
+ "after_user_action",
5814
+ {
5815
+ version: 1,
5816
+ dispatchOutcome: "unknown"
5817
+ }
5619
5818
  );
5620
5819
  }
5621
5820
  }
5622
- async executeResolved(call, currentInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
5821
+ async executeResolved(call, finalInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
5623
5822
  let shell;
5624
5823
  try {
5625
- shell = tool.approvalScope?.(currentInput, this.rootDir);
5824
+ shell = tool.approvalScope?.(finalInput, this.rootDir);
5626
5825
  } catch {
5627
5826
  try {
5628
5827
  await audit("tool.decided", { decision: "deny", decisionSource: "invalid_target" });
@@ -5631,19 +5830,32 @@ var ToolExecutor = class {
5631
5830
  }
5632
5831
  return failure("Invalid tool execution target", "INVALID_TARGET");
5633
5832
  }
5833
+ let executionIdentity;
5834
+ try {
5835
+ executionIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
5836
+ } catch {
5837
+ try {
5838
+ await audit("tool.decided", { decision: "deny", decisionSource: "invalid_identity" });
5839
+ await audit("tool.finished", { status: "failed", errorCode: "INVALID_TARGET" });
5840
+ } catch {
5841
+ }
5842
+ return failure("Invalid tool execution identity", "INVALID_TARGET");
5843
+ }
5634
5844
  let auditFields = {};
5635
5845
  for (let attempt = 0; attempt < 2; attempt++) {
5636
5846
  const request = {
5637
5847
  toolName: call.name,
5638
5848
  capabilities: tool.metadata?.permissions ?? [],
5639
- input: currentInput,
5849
+ input: finalInput,
5640
5850
  rootDir: this.rootDir,
5641
5851
  ...shell ? { shell } : {},
5852
+ ...executionIdentity ? { executionIdentity } : {},
5642
5853
  // 解释器内建查询与内容已绑定摘要的工作区直引脚本可确认不受外部脚本变更影响,
5643
5854
  // 允许会话级批准;其余 shell 命令仅允许单次批准。
5855
+ // 非 shell 工具(含深度绑定 executionIdentity 的外部工具)允许会话级批准。
5644
5856
  sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
5645
5857
  };
5646
- const repeatKey = (0, import_node_crypto14.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex");
5858
+ const repeatKey = (0, import_node_crypto14.createHash)("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex");
5647
5859
  if (this.neverFailures.has(repeatKey)) {
5648
5860
  try {
5649
5861
  await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
@@ -5652,7 +5864,12 @@ var ToolExecutor = class {
5652
5864
  return failure(
5653
5865
  "Audit unavailable; tool was not started",
5654
5866
  "AUDIT_UNAVAILABLE",
5655
- "after_user_action"
5867
+ "after_user_action",
5868
+ {
5869
+ version: 1,
5870
+ dispatchOutcome: "not_sent",
5871
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5872
+ }
5656
5873
  );
5657
5874
  }
5658
5875
  return failure(
@@ -5660,7 +5877,7 @@ var ToolExecutor = class {
5660
5877
  "REPEAT_BLOCKED"
5661
5878
  );
5662
5879
  }
5663
- const hardDenied = this.hardDenial?.(tool, currentInput);
5880
+ const hardDenied = this.hardDenial?.(tool, finalInput);
5664
5881
  const cached = this.approvalCache.get(request);
5665
5882
  const decision = this.policy.decide({
5666
5883
  hardDenied: hardDenied !== void 0,
@@ -5696,6 +5913,11 @@ var ToolExecutor = class {
5696
5913
  executableDigest: shell.executableDigest,
5697
5914
  executionFingerprint: shell.executionFingerprint,
5698
5915
  ...commandAuditSummary(shell.command)
5916
+ } : executionIdentity ? {
5917
+ serverIdentity: executionIdentity.serverIdentity,
5918
+ connectionGeneration: executionIdentity.connectionGeneration,
5919
+ catalogEpoch: executionIdentity.catalogEpoch,
5920
+ toolDigest: executionIdentity.toolDigest
5699
5921
  } : {};
5700
5922
  try {
5701
5923
  await audit("tool.decided", {
@@ -5709,7 +5931,12 @@ var ToolExecutor = class {
5709
5931
  return failure(
5710
5932
  "Audit unavailable; tool was not started",
5711
5933
  "AUDIT_UNAVAILABLE",
5712
- "after_user_action"
5934
+ "after_user_action",
5935
+ {
5936
+ version: 1,
5937
+ dispatchOutcome: "not_sent",
5938
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5939
+ }
5713
5940
  );
5714
5941
  }
5715
5942
  if (!permitted || this.signal?.aborted) {
@@ -5717,7 +5944,11 @@ var ToolExecutor = class {
5717
5944
  try {
5718
5945
  await audit("tool.finished", { status: "denied", errorCode: code });
5719
5946
  } catch {
5720
- return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action");
5947
+ return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
5948
+ version: 1,
5949
+ dispatchOutcome: "not_sent",
5950
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5951
+ });
5721
5952
  }
5722
5953
  return failure(
5723
5954
  this.signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
@@ -5727,7 +5958,7 @@ var ToolExecutor = class {
5727
5958
  }
5728
5959
  if (shell) {
5729
5960
  try {
5730
- const next = tool.approvalScope?.(currentInput, this.rootDir);
5961
+ const next = tool.approvalScope?.(finalInput, this.rootDir);
5731
5962
  if (next?.executionFingerprint !== shell.executionFingerprint) {
5732
5963
  if (attempt === 0 && next) {
5733
5964
  try {
@@ -5736,7 +5967,12 @@ var ToolExecutor = class {
5736
5967
  return failure(
5737
5968
  "Audit unavailable; tool was not started",
5738
5969
  "AUDIT_UNAVAILABLE",
5739
- "after_user_action"
5970
+ "after_user_action",
5971
+ {
5972
+ version: 1,
5973
+ dispatchOutcome: "not_sent",
5974
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5975
+ }
5740
5976
  );
5741
5977
  }
5742
5978
  shell = next;
@@ -5757,6 +5993,37 @@ var ToolExecutor = class {
5757
5993
  );
5758
5994
  }
5759
5995
  }
5996
+ if (executionIdentity) {
5997
+ try {
5998
+ const nextIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
5999
+ if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
6000
+ await audit("tool.finished", {
6001
+ status: "identity_changed",
6002
+ errorCode: "MCP_TOOL_CHANGED"
6003
+ });
6004
+ return failure(
6005
+ "Tool execution identity changed after approval",
6006
+ "MCP_TOOL_CHANGED",
6007
+ "after_user_action",
6008
+ {
6009
+ version: 1,
6010
+ dispatchOutcome: "not_sent",
6011
+ hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
6012
+ }
6013
+ );
6014
+ }
6015
+ } catch {
6016
+ return failure(
6017
+ "Tool execution identity check failed after approval",
6018
+ "MCP_TOOL_CHANGED",
6019
+ "after_user_action",
6020
+ {
6021
+ version: 1,
6022
+ dispatchOutcome: "not_sent"
6023
+ }
6024
+ );
6025
+ }
6026
+ }
5760
6027
  break;
5761
6028
  }
5762
6029
  try {
@@ -5765,13 +6032,19 @@ var ToolExecutor = class {
5765
6032
  return failure(
5766
6033
  "Audit unavailable; tool was not started",
5767
6034
  "AUDIT_UNAVAILABLE",
5768
- "after_user_action"
6035
+ "after_user_action",
6036
+ {
6037
+ version: 1,
6038
+ dispatchOutcome: "not_sent",
6039
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
6040
+ }
5769
6041
  );
5770
6042
  }
5771
6043
  let outputStr;
5772
6044
  let isError = false;
5773
6045
  let errorCode;
5774
6046
  let retryPolicy;
6047
+ let execution;
5775
6048
  try {
5776
6049
  const executionController = new AbortController();
5777
6050
  const abortFromSession = () => executionController.abort();
@@ -5790,16 +6063,52 @@ var ToolExecutor = class {
5790
6063
  } : {}
5791
6064
  };
5792
6065
  try {
5793
- const rawResult = tool.metadata?.managesTimeout ? await tool.execute(currentInput, toolCtx) : await this.withTimeout(
5794
- tool.execute(currentInput, toolCtx),
6066
+ const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await this.withTimeout(
6067
+ tool.execute(finalInput, toolCtx),
5795
6068
  call.name,
5796
6069
  executionController
5797
6070
  );
5798
- outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
5799
- if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
5800
- isError = true;
5801
- errorCode = "COMMAND_EXIT_NONZERO";
5802
- retryPolicy = "never";
6071
+ if (typeof tool.toExecutionResult === "function") {
6072
+ try {
6073
+ const converted = tool.toExecutionResult(rawResult);
6074
+ outputStr = converted.content;
6075
+ isError = converted.isError ?? false;
6076
+ errorCode = converted.errorCode;
6077
+ retryPolicy = converted.retryPolicy;
6078
+ execution = converted.execution ?? {
6079
+ version: 1,
6080
+ dispatchOutcome: isError ? "failed" : "succeeded"
6081
+ };
6082
+ } catch (convErr) {
6083
+ isError = true;
6084
+ errorCode = "MCP_RESULT_INVALID";
6085
+ retryPolicy = "after_user_action";
6086
+ outputStr = convErr instanceof Error ? convErr.message : "Tool result projection failed";
6087
+ execution = {
6088
+ version: 1,
6089
+ dispatchOutcome: "succeeded",
6090
+ hostError: {
6091
+ phase: "projection",
6092
+ code: "MCP_RESULT_INVALID"
6093
+ }
6094
+ };
6095
+ }
6096
+ } else {
6097
+ outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
6098
+ if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
6099
+ isError = true;
6100
+ errorCode = "COMMAND_EXIT_NONZERO";
6101
+ retryPolicy = "never";
6102
+ execution = {
6103
+ version: 1,
6104
+ dispatchOutcome: "failed"
6105
+ };
6106
+ } else {
6107
+ execution = {
6108
+ version: 1,
6109
+ dispatchOutcome: "succeeded"
6110
+ };
6111
+ }
5803
6112
  }
5804
6113
  } finally {
5805
6114
  this.signal?.removeEventListener("abort", abortFromSession);
@@ -5810,13 +6119,19 @@ var ToolExecutor = class {
5810
6119
  outputStr = known?.safeMessage ?? "Tool execution failed";
5811
6120
  errorCode = known?.code ?? "TOOL_FAILED";
5812
6121
  retryPolicy = known?.retryPolicy ?? "never";
6122
+ const outcome = known?.dispatchOutcome ?? (errorCode === "OUTCOME_UNKNOWN" || known && known.retryPolicy === "after_user_action" ? "unknown" : "failed");
6123
+ execution = {
6124
+ version: 1,
6125
+ dispatchOutcome: outcome
6126
+ };
5813
6127
  }
5814
6128
  const resultBlock = {
5815
6129
  type: "tool_result",
5816
6130
  toolUseId: call.id,
5817
6131
  content: outputStr,
5818
6132
  isError,
5819
- ...errorCode ? { errorCode, retryPolicy } : {}
6133
+ ...errorCode ? { errorCode, retryPolicy } : {},
6134
+ execution
5820
6135
  };
5821
6136
  if (resultBlock.isError && resultBlock.errorCode) {
5822
6137
  resultBlock.content = JSON.stringify({
@@ -5829,7 +6144,7 @@ var ToolExecutor = class {
5829
6144
  for (const hook of this.hooks.get("tool:after")) {
5830
6145
  await hook(
5831
6146
  hookCtx,
5832
- { id: call.id, name: call.name, input: currentInput },
6147
+ { id: call.id, name: call.name, input: finalInput },
5833
6148
  { output: outputStr, isError }
5834
6149
  );
5835
6150
  }
@@ -5842,11 +6157,19 @@ var ToolExecutor = class {
5842
6157
  resultBlock.isError = true;
5843
6158
  resultBlock.errorCode = "HOOK_ERROR";
5844
6159
  resultBlock.retryPolicy = "after_user_action";
6160
+ resultBlock.execution = {
6161
+ version: 1,
6162
+ dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
6163
+ hostError: {
6164
+ phase: "hook",
6165
+ code: "HOOK_ERROR"
6166
+ }
6167
+ };
5845
6168
  }
5846
6169
  resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
5847
6170
  if (resultBlock.isError && resultBlock.retryPolicy === "never") {
5848
6171
  this.neverFailures.add(
5849
- (0, import_node_crypto14.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex")
6172
+ (0, import_node_crypto14.createHash)("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
5850
6173
  );
5851
6174
  }
5852
6175
  try {
@@ -5856,13 +6179,25 @@ var ToolExecutor = class {
5856
6179
  durationMs: resultBlock.durationMs
5857
6180
  });
5858
6181
  } catch {
5859
- return failure(
5860
- "Tool outcome is unknown because the audit result could not be written",
5861
- "OUTCOME_UNKNOWN",
5862
- "after_user_action"
5863
- );
6182
+ resultBlock.isError = true;
6183
+ resultBlock.errorCode = "OUTCOME_UNKNOWN";
6184
+ resultBlock.retryPolicy = "after_user_action";
6185
+ resultBlock.content = JSON.stringify({
6186
+ code: "OUTCOME_UNKNOWN",
6187
+ retryPolicy: "after_user_action",
6188
+ message: "Tool outcome is unknown because the audit result could not be written"
6189
+ });
6190
+ resultBlock.execution = {
6191
+ version: 1,
6192
+ dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
6193
+ hostError: {
6194
+ phase: "audit",
6195
+ code: "AUDIT_UNAVAILABLE"
6196
+ }
6197
+ };
6198
+ return resultBlock;
5864
6199
  }
5865
- await this.onToolResult?.(tool, currentInput, resultBlock);
6200
+ await this.onToolResult?.(tool, finalInput, resultBlock);
5866
6201
  return resultBlock;
5867
6202
  }
5868
6203
  /**
@@ -7637,6 +7972,7 @@ var AgentLoop = class {
7637
7972
  let consecutiveErrors = 0;
7638
7973
  let completed = false;
7639
7974
  let overflowRetried = false;
7975
+ const toolSnapshot = "snapshot" in this.tools ? this.tools.snapshot() : this.tools;
7640
7976
  while (step < this.maxSteps) {
7641
7977
  step++;
7642
7978
  const turnStartTime = Date.now();
@@ -7647,7 +7983,7 @@ var AgentLoop = class {
7647
7983
  let request = {
7648
7984
  systemPrompt: this.systemPrompt,
7649
7985
  messages: structuredClone(this.projectHistory?.(history) ?? history),
7650
- tools: visibleTools(this.tools.list(), this.getMode(), this.permissionRules).map(
7986
+ tools: visibleTools(toolSnapshot.list(), this.getMode(), this.permissionRules).map(
7651
7987
  toToolDefinition
7652
7988
  ),
7653
7989
  signal: this.signal,
@@ -7656,7 +7992,30 @@ var AgentLoop = class {
7656
7992
  const beforeModelHooks = this.hooks.get("model:before");
7657
7993
  const hookCtx = { signal: this.signal };
7658
7994
  for (const hook of beforeModelHooks) {
7659
- request = await hook(hookCtx, request);
7995
+ const modified = await hook(hookCtx, request);
7996
+ if (modified) {
7997
+ request = modified;
7998
+ }
7999
+ }
8000
+ if (request.tools) {
8001
+ for (const t of request.tools) {
8002
+ const original = toolSnapshot.get(t.name);
8003
+ if (!original) {
8004
+ throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
8005
+ stage: "request",
8006
+ retryable: false
8007
+ });
8008
+ }
8009
+ if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
8010
+ throw new ModelError(
8011
+ `Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
8012
+ {
8013
+ stage: "request",
8014
+ retryable: false
8015
+ }
8016
+ );
8017
+ }
8018
+ }
7660
8019
  }
7661
8020
  if (this.prepareRequest) request = yield* this.prepareRequest(request, history);
7662
8021
  yield {
@@ -8396,6 +8755,14 @@ var AgentSession = class {
8396
8755
  });
8397
8756
  this.plugins.push(plugin);
8398
8757
  }
8758
+ /**
8759
+ * 获取当前会话已成功挂载的扩展插件只读列表。
8760
+ *
8761
+ * 保证返回当前状态的独立切片,外部修改不污染内部挂载表。
8762
+ */
8763
+ getPlugins() {
8764
+ return [...this.plugins];
8765
+ }
8399
8766
  /**
8400
8767
  * 空闲时原子替换角色的模型绑定;本方法不改变当前角色或工具权限。
8401
8768
  * @param profile 模型协议、请求目标和输出预算。
@@ -8462,6 +8829,10 @@ var AgentSession = class {
8462
8829
  getMode() {
8463
8830
  return this.mode;
8464
8831
  }
8832
+ /** 获取当前会话生效的权限规则列表。 */
8833
+ getPermissionRules() {
8834
+ return this.permissionRules;
8835
+ }
8465
8836
  /** 返回最近一次成功装载的来源路径,不暴露指令正文。 */
8466
8837
  getInstructionSources() {
8467
8838
  return this.instructionSnapshot?.sources.map((source) => source.path) ?? [];
@@ -8590,8 +8961,23 @@ var AgentSession = class {
8590
8961
  options?.signal
8591
8962
  );
8592
8963
  }
8964
+ /**
8965
+ * 受控任务准备入口:在同一运行锁内异步准备文本块/来源,预算和存储成功后才进入模型。
8966
+ *
8967
+ * @param task 用户显式任务正文(或命令描述)。
8968
+ * @param prepare 异步任务准备函数;在锁内执行,若准备失败或取消则释放锁且零 append。
8969
+ * @param options 可选中止信号与显式主任务角色。
8970
+ * @returns 结构化事件的异步流。
8971
+ * @throws {SessionBusyError} 当前会话忙碌时抛出。
8972
+ */
8973
+ runPreparedTask(task, prepare, options) {
8974
+ return cancellableGenerator(
8975
+ (signal) => this.runOperation(task, { signal, role: options?.role }, void 0, prepare),
8976
+ options?.signal
8977
+ );
8978
+ }
8593
8979
  /** 单次任务执行主体;调用方 return 的中止与 finally 清理由外层统一协调。 */
8594
- async *runOperation(userInput, options, skillName) {
8980
+ async *runOperation(userInput, options, skillName, prepare) {
8595
8981
  this.assertIdle("start another run");
8596
8982
  const role = options?.role ?? this.primaryRole;
8597
8983
  const { profile, provider } = this.resolvePrimaryBinding(role);
@@ -8675,12 +9061,22 @@ var AgentSession = class {
8675
9061
  profile.contextWindow,
8676
9062
  profile.maxOutputTokens
8677
9063
  ).inputBudget;
9064
+ const toolSnapshot = this.tools.snapshot();
9065
+ let prepared;
9066
+ if (prepare) {
9067
+ prepared = await prepare({ signal: options?.signal, runId, tools: toolSnapshot });
9068
+ if (options?.signal?.aborted) throw new AbortError();
9069
+ if (prepared.skillName) skillName = prepared.skillName;
9070
+ }
8678
9071
  await this.skillRegistry?.refresh(options?.signal);
8679
9072
  this.skillRuntime?.syncTools();
9073
+ const extraPreparedTokens = prepared?.content ? typeof prepared.content === "string" ? estimateSkillTokens(prepared.content) : prepared.content.filter(isTextBlock).reduce((sum, b) => sum + estimateSkillTokens(b.text), 0) : 0;
8680
9074
  const promptAssembler = this.createPromptAssembler(
8681
9075
  profile,
8682
9076
  this.context.project(this.history),
8683
- 32 + estimateSkillTokens(userInput)
9077
+ 32 + estimateSkillTokens(userInput) + extraPreparedTokens,
9078
+ toolSnapshot.list(),
9079
+ toolSnapshot
8684
9080
  );
8685
9081
  this.skillRuntime?.begin(
8686
9082
  runId,
@@ -8689,7 +9085,7 @@ var AgentSession = class {
8689
9085
  const current = {
8690
9086
  ...previousRequest ?? { systemPrompt: promptAssembler.assemble() },
8691
9087
  messages: [...this.context.project(this.history), ...budgetToolMessages],
8692
- tools: previousRequest?.tools ?? visibleTools(this.tools.list(), this.mode, this.permissionRules).map((tool) => ({
9088
+ tools: previousRequest?.tools ?? visibleTools(toolSnapshot.list(), this.mode, this.permissionRules).map((tool) => ({
8693
9089
  name: tool.name,
8694
9090
  description: tool.description,
8695
9091
  parameters: tool.parameters
@@ -8697,7 +9093,7 @@ var AgentSession = class {
8697
9093
  };
8698
9094
  return Math.max(
8699
9095
  0,
8700
- inputBudget - (this.estimator ?? new UnicodeTokenEstimator()).estimate(current).total - (previousRequest ? 0 : estimateSkillTokens(userInput) + 32)
9096
+ inputBudget - (this.estimator ?? new UnicodeTokenEstimator()).estimate(current).total - (previousRequest ? 0 : estimateSkillTokens(userInput) + extraPreparedTokens + 32)
8701
9097
  );
8702
9098
  },
8703
9099
  () => previousRequest?.messages ?? this.context.project(this.history)
@@ -8709,6 +9105,51 @@ var AgentSession = class {
8709
9105
  await this.authorizeSkill(skillName, runId, options?.signal);
8710
9106
  preparedSkill = await this.skillRuntime.prepare(skillName, "user", options?.signal);
8711
9107
  }
9108
+ if (prepared?.verifyIdentity) {
9109
+ const valid = await prepared.verifyIdentity();
9110
+ if (!valid) {
9111
+ throw new KapibalaError("Task preparation identity changed before commit", {
9112
+ code: "MCP_PROMPT_CHANGED",
9113
+ retryPolicy: "never"
9114
+ });
9115
+ }
9116
+ }
9117
+ const userContent = [{ type: "text", text: userInput }];
9118
+ if (preparedSkill)
9119
+ userContent.push({
9120
+ type: "text",
9121
+ text: preparedSkill.text,
9122
+ skillSource: preparedSkill.source
9123
+ });
9124
+ if (prepared?.content) {
9125
+ if (typeof prepared.content === "string") {
9126
+ userContent.push({
9127
+ type: "text",
9128
+ text: prepared.content,
9129
+ ...prepared.promptSource ? { promptSource: prepared.promptSource } : {}
9130
+ });
9131
+ } else {
9132
+ userContent.push(...prepared.content);
9133
+ }
9134
+ }
9135
+ const userMessage = {
9136
+ id: createMessageId(),
9137
+ interactionId: runId,
9138
+ state: "completed",
9139
+ role: "user",
9140
+ content: userContent,
9141
+ timestamp: Date.now()
9142
+ };
9143
+ const taskBudgetCheck = (this.estimator ?? new UnicodeTokenEstimator()).estimate({
9144
+ systemPrompt: promptAssembler.assemble(),
9145
+ messages: [userMessage]
9146
+ }).total;
9147
+ if (taskBudgetCheck > inputBudget) {
9148
+ throw new ContextOverflowError(400, {
9149
+ message: "Prepared task content exceeds context input budget",
9150
+ stage: "request"
9151
+ });
9152
+ }
8712
9153
  this.totalTurns++;
8713
9154
  if (this.store && supportsSessionState(this.store)) {
8714
9155
  const currentProject = resolveSessionProject(this.cwd);
@@ -8722,24 +9163,39 @@ var AgentSession = class {
8722
9163
  });
8723
9164
  }
8724
9165
  runStarted = true;
8725
- const userMessage = {
8726
- id: createMessageId(),
8727
- interactionId: runId,
8728
- state: "completed",
8729
- role: "user",
8730
- content: [{ type: "text", text: userInput }],
8731
- timestamp: Date.now()
8732
- };
8733
- if (preparedSkill)
8734
- userMessage.content.push({
8735
- type: "text",
8736
- text: preparedSkill.text,
8737
- skillSource: preparedSkill.source
8738
- });
8739
9166
  if (this.store) {
8740
9167
  await this.store.append(userMessage);
8741
9168
  }
8742
9169
  this.history.push(userMessage);
9170
+ if (prepared?.promptSource) {
9171
+ const persistence = this.store ? "disk" : "memory";
9172
+ yield {
9173
+ type: "mcp_prompt_loaded",
9174
+ server: prepared.promptSource.server,
9175
+ name: prepared.promptSource.name,
9176
+ persistence,
9177
+ promptSource: prepared.promptSource,
9178
+ runId
9179
+ };
9180
+ try {
9181
+ await this.eventLogger.recordAudit({
9182
+ level: "info",
9183
+ event: "mcp.prompt_loaded",
9184
+ sessionId: this.sessionId,
9185
+ runId,
9186
+ fields: {
9187
+ server: prepared.promptSource.server,
9188
+ promptName: prepared.promptSource.name,
9189
+ contentDigest: prepared.promptSource.contentDigest,
9190
+ persistence
9191
+ }
9192
+ });
9193
+ } catch (auditErr) {
9194
+ const msg = `MCP prompt audit recording failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`;
9195
+ this.logger?.(msg);
9196
+ this.onDiagnostic?.(msg);
9197
+ }
9198
+ }
8743
9199
  if (preparedSkill && this.skillRuntime) {
8744
9200
  this.skillRuntime.acceptUser(preparedSkill);
8745
9201
  for (const event of await this.commitSkills(runId)) yield event;
@@ -8750,7 +9206,7 @@ var AgentSession = class {
8750
9206
  const executor = new ToolExecutor({
8751
9207
  hardDenial: (tool) => this.skillRuntime?.denial(tool),
8752
9208
  onToolResult: (tool, _input, result) => this.skillRuntime?.acceptResult(tool, result),
8753
- tools: this.tools,
9209
+ tools: toolSnapshot,
8754
9210
  hooks: this.hooks,
8755
9211
  rootDir: this.rootDir,
8756
9212
  signal: options?.signal,
@@ -8872,12 +9328,12 @@ var AgentSession = class {
8872
9328
  const skills = this.skillRuntime;
8873
9329
  const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
8874
9330
  return (async function* () {
8875
- const prepared = yield* preparation;
8876
- skills?.assertProjection(prepared.messages);
8877
- previousRequest = structuredClone({ ...prepared, signal: void 0 });
9331
+ const prepared2 = yield* preparation;
9332
+ skills?.assertProjection(prepared2.messages);
9333
+ previousRequest = structuredClone({ ...prepared2, signal: void 0 });
8878
9334
  lastPromptTokens = void 0;
8879
9335
  lastEstimatedPromptTokens = context.getSnapshot()?.estimate.total;
8880
- return prepared;
9336
+ return prepared2;
8881
9337
  })();
8882
9338
  },
8883
9339
  onUsage: async (request, usage, attemptId) => {
@@ -8896,7 +9352,7 @@ var AgentSession = class {
8896
9352
  },
8897
9353
  provider,
8898
9354
  executor,
8899
- tools: this.tools,
9355
+ tools: toolSnapshot,
8900
9356
  hooks: this.hooks,
8901
9357
  systemPrompt: assembledSystemPrompt,
8902
9358
  maxSteps: this.maxSteps,
@@ -9102,11 +9558,12 @@ var AgentSession = class {
9102
9558
  if (this.activeRun) throw new SessionBusyError(operation);
9103
9559
  }
9104
9560
  /** 实际请求、静态统计和手动压缩共用目录空间推导;先计正文、工具和非目录提示,再预算整段目录。 */
9105
- createPromptAssembler(profile, messages, extraTokens = 0) {
9106
- const tools = visibleTools(this.tools.list(), this.mode, this.permissionRules);
9561
+ createPromptAssembler(profile, messages, extraTokens = 0, toolList, toolRegistry) {
9562
+ const registry = toolRegistry ?? this.tools;
9563
+ const tools = visibleTools(toolList ?? registry.list(), this.mode, this.permissionRules);
9107
9564
  const options = {
9108
9565
  skills: this.skillRegistry,
9109
- tools: this.tools,
9566
+ tools: registry,
9110
9567
  rootDir: this.rootDir,
9111
9568
  customInstructions: this.customSystemPrompt,
9112
9569
  instructionSources: this.instructionSnapshot?.sources,
@@ -9407,6 +9864,7 @@ var AgentSession = class {
9407
9864
  OpenAIResponsesProvider,
9408
9865
  PathSandbox,
9409
9866
  PermissionPolicy,
9867
+ PreDispatchError,
9410
9868
  PromptAssembler,
9411
9869
  SESSION_SCHEMA_VERSION,
9412
9870
  SKILL_LIMITS,