@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.js CHANGED
@@ -119,12 +119,25 @@ var KapibalaError = class extends Error {
119
119
  code;
120
120
  retryPolicy;
121
121
  safeMessage;
122
+ dispatchOutcome;
122
123
  constructor(message, options) {
123
124
  super(message);
124
125
  this.name = "KapibalaError";
125
126
  this.code = options?.code ?? "INTERNAL_ERROR";
126
127
  this.retryPolicy = options?.retryPolicy ?? "never";
127
128
  this.safeMessage = options?.safeMessage ?? "Operation failed";
129
+ this.dispatchOutcome = options?.dispatchOutcome;
130
+ }
131
+ };
132
+ var PreDispatchError = class extends KapibalaError {
133
+ constructor(message, options) {
134
+ super(message, {
135
+ code: options?.code ?? "PRE_DISPATCH_FAILED",
136
+ retryPolicy: options?.retryPolicy ?? "never",
137
+ safeMessage: options?.safeMessage ?? "Tool execution was aborted before dispatch",
138
+ dispatchOutcome: "not_sent"
139
+ });
140
+ this.name = "PreDispatchError";
128
141
  }
129
142
  };
130
143
  var ModelError = class extends KapibalaError {
@@ -309,6 +322,16 @@ function validateProtocolContent(content) {
309
322
  invalidState();
310
323
  }
311
324
  }
325
+ if (block.type === "tool_result" && "execution" in block && block.execution !== void 0) {
326
+ const ex = block.execution;
327
+ 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))
328
+ invalidState();
329
+ }
330
+ if (block.type === "text" && "promptSource" in block && block.promptSource !== void 0) {
331
+ const ps = block.promptSource;
332
+ 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")
333
+ invalidState();
334
+ }
312
335
  validatedBlocks.set(block, true);
313
336
  }
314
337
  validatedContents.set(content, true);
@@ -426,7 +449,11 @@ function normalizeHistory(input) {
426
449
  content: "Tool execution was interrupted or crashed in previous session",
427
450
  isError: true,
428
451
  errorCode: "OUTCOME_UNKNOWN",
429
- retryPolicy: "after_user_action"
452
+ retryPolicy: "after_user_action",
453
+ execution: {
454
+ version: 1,
455
+ dispatchOutcome: "unknown"
456
+ }
430
457
  };
431
458
  content.push(repair);
432
459
  result.repairs.push({
@@ -4710,7 +4737,8 @@ var CAPABILITIES = /* @__PURE__ */ new Set([
4710
4737
  "exec",
4711
4738
  "net:outbound",
4712
4739
  "env:read",
4713
- "agent:spawn"
4740
+ "agent:spawn",
4741
+ "external:call"
4714
4742
  ]);
4715
4743
  function validatePermissionRules(rules) {
4716
4744
  for (const [index2, rule] of rules.entries()) {
@@ -4823,7 +4851,8 @@ var SessionApprovalCache = class {
4823
4851
  [...request.capabilities].sort(),
4824
4852
  request.input,
4825
4853
  request.rootDir,
4826
- request.shell ?? null
4854
+ request.shell ?? null,
4855
+ request.executionIdentity ?? null
4827
4856
  ])
4828
4857
  ).digest("hex");
4829
4858
  }
@@ -4841,9 +4870,14 @@ var ToolRegistry = class {
4841
4870
  toolSources = /* @__PURE__ */ new Map();
4842
4871
  // toolName -> source
4843
4872
  conflictPolicy;
4873
+ version = 0;
4844
4874
  constructor(options) {
4845
4875
  this.conflictPolicy = options?.conflictPolicy ?? "error";
4846
4876
  }
4877
+ /** 获取当前注册表版本号;每次有效变更(注册、替换、注销)单调自增。 */
4878
+ get currentVersion() {
4879
+ return this.version;
4880
+ }
4847
4881
  register(tool, opts) {
4848
4882
  const source = opts?.source ?? tool.metadata?.source ?? "builtin";
4849
4883
  const existing = this.tools.get(tool.name);
@@ -4859,27 +4893,93 @@ var ToolRegistry = class {
4859
4893
  }
4860
4894
  this.tools.set(tool.name, tool);
4861
4895
  this.toolSources.set(tool.name, source);
4896
+ this.version++;
4862
4897
  }
4863
- registerSource(source, tools) {
4898
+ /**
4899
+ * 原子替换指定来源的全部工具:
4900
+ * 1. 预检候选批内自身重名:若候选批次内包含同名工具,整批拒绝;
4901
+ * 2. 预检跨来源冲突:若候选工具被其它来源占用且策略不允许覆盖,整批拒绝;
4902
+ * 3. 同步原子提交:预检全部通过后一次性清理该来源旧工具并写入新工具;失败时完全保持旧状态不变。
4903
+ */
4904
+ replaceSource(source, tools) {
4905
+ const batchNames = /* @__PURE__ */ new Set();
4906
+ for (const tool of tools) {
4907
+ if (batchNames.has(tool.name)) {
4908
+ throw new Error(
4909
+ `Invalid candidate tools: duplicate tool name '${tool.name}' within source batch '${source}'`
4910
+ );
4911
+ }
4912
+ batchNames.add(tool.name);
4913
+ }
4864
4914
  for (const tool of tools) {
4865
- this.register(tool, { source });
4915
+ const existing = this.tools.get(tool.name);
4916
+ const existingSource = this.toolSources.get(tool.name);
4917
+ if (existing && existingSource !== source) {
4918
+ if (this.conflictPolicy === "error") {
4919
+ throw new Error(
4920
+ `Tool name conflict: candidate '${tool.name}' from '${source}' conflicts with existing tool registered by '${existingSource}'`
4921
+ );
4922
+ }
4923
+ if (this.conflictPolicy === "prefer-builtin" && existingSource === "builtin") {
4924
+ throw new Error(
4925
+ `Tool name conflict: candidate '${tool.name}' from '${source}' cannot override builtin tool`
4926
+ );
4927
+ }
4928
+ }
4866
4929
  }
4930
+ for (const [name, s] of this.toolSources.entries()) {
4931
+ if (s === source) {
4932
+ this.tools.delete(name);
4933
+ this.toolSources.delete(name);
4934
+ }
4935
+ }
4936
+ for (const tool of tools) {
4937
+ this.tools.set(tool.name, tool);
4938
+ this.toolSources.set(tool.name, source);
4939
+ }
4940
+ this.version++;
4941
+ }
4942
+ /**
4943
+ * 注册指定来源的工具集。
4944
+ *
4945
+ * 语义与约束:
4946
+ * 1. 委托给 `replaceSource` 实现整源原子替换(新集合未包含的该来源旧工具将被注销移除,非增量累加);
4947
+ * 2. 默认禁止跨来源名称冲突(抛出 KapibalaError TOOL_CONFLICT);若 options 配置了 conflictPolicy: 'prefer-last' 则允许覆盖旧来源同名工具;
4948
+ * 3. 预检阶段发现批内同名重复时整批拒绝,绝不破坏注册表原子状态。
4949
+ */
4950
+ registerSource(source, tools) {
4951
+ this.replaceSource(source, tools);
4867
4952
  }
4868
4953
  unregisterSource(source) {
4954
+ let changed = false;
4869
4955
  for (const [name, s] of this.toolSources.entries()) {
4870
4956
  if (s === source) {
4871
4957
  this.tools.delete(name);
4872
4958
  this.toolSources.delete(name);
4959
+ changed = true;
4873
4960
  }
4874
4961
  }
4962
+ if (changed) {
4963
+ this.version++;
4964
+ }
4875
4965
  }
4876
4966
  get(name) {
4877
4967
  return this.tools.get(name);
4878
4968
  }
4969
+ has(name) {
4970
+ return this.tools.has(name);
4971
+ }
4972
+ /** 获取工具对应的来源标记。 */
4973
+ sourceOf(name) {
4974
+ return this.toolSources.get(name);
4975
+ }
4879
4976
  /** 注销指定名称及来源记录;调用方须核对实例身份,不能移除同名替代工具。 */
4880
4977
  unregister(name) {
4881
- this.tools.delete(name);
4882
- this.toolSources.delete(name);
4978
+ if (this.tools.has(name)) {
4979
+ this.tools.delete(name);
4980
+ this.toolSources.delete(name);
4981
+ this.version++;
4982
+ }
4883
4983
  }
4884
4984
  /** 未命中时抛 ToolNotFound,并携带当前全部可用名称辅助模型自纠。 */
4885
4985
  resolve(name) {
@@ -4895,6 +4995,30 @@ var ToolRegistry = class {
4895
4995
  definitions() {
4896
4996
  return this.list().map(toToolDefinition);
4897
4997
  }
4998
+ /**
4999
+ * 捕获当前注册表的不可变只读快照。
5000
+ * 注册表后续的增删改绝不改变该快照的定义列表、工具执行实例与来源版本。
5001
+ */
5002
+ snapshot() {
5003
+ const snapshotTools = new Map(this.tools);
5004
+ const snapshotSources = new Map(this.toolSources);
5005
+ const snapshotVersion = this.version;
5006
+ return {
5007
+ version: snapshotVersion,
5008
+ get: (name) => snapshotTools.get(name),
5009
+ has: (name) => snapshotTools.has(name),
5010
+ resolve: (name) => {
5011
+ const tool = snapshotTools.get(name);
5012
+ if (!tool) {
5013
+ throw new ToolNotFound(name, Array.from(snapshotTools.keys()));
5014
+ }
5015
+ return tool;
5016
+ },
5017
+ list: () => Array.from(snapshotTools.values()),
5018
+ definitions: () => Array.from(snapshotTools.values()).map(toToolDefinition),
5019
+ sourceOf: (name) => snapshotSources.get(name)
5020
+ };
5021
+ }
4898
5022
  };
4899
5023
 
4900
5024
  // src/capabilities/tools/builtin/fs.ts
@@ -5370,14 +5494,18 @@ var ToolExecutor = class {
5370
5494
  call.executionRoot = path15.resolve(this.rootDir);
5371
5495
  const startedAt = Date.now();
5372
5496
  const operationId = createLogId();
5373
- const failure = (message, errorCode, retryPolicy = "never") => ({
5497
+ const failure = (message, errorCode, retryPolicy = "never", execution) => ({
5374
5498
  type: "tool_result",
5375
5499
  toolUseId: call.id,
5376
5500
  content: JSON.stringify({ code: errorCode, retryPolicy, message }),
5377
5501
  isError: true,
5378
5502
  errorCode,
5379
5503
  retryPolicy,
5380
- durationMs: Math.max(0, Date.now() - startedAt)
5504
+ durationMs: Math.max(0, Date.now() - startedAt),
5505
+ execution: execution ?? {
5506
+ version: 1,
5507
+ dispatchOutcome: "not_sent"
5508
+ }
5381
5509
  });
5382
5510
  const audit = async (event, fields = {}) => {
5383
5511
  if (!this.eventLogger || !this.sessionId) return;
@@ -5397,7 +5525,12 @@ var ToolExecutor = class {
5397
5525
  return failure(
5398
5526
  "Audit unavailable; tool was not started",
5399
5527
  "AUDIT_UNAVAILABLE",
5400
- "after_user_action"
5528
+ "after_user_action",
5529
+ {
5530
+ version: 1,
5531
+ dispatchOutcome: "not_sent",
5532
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5533
+ }
5401
5534
  );
5402
5535
  }
5403
5536
  if (forcedDeny) {
@@ -5408,7 +5541,12 @@ var ToolExecutor = class {
5408
5541
  return failure(
5409
5542
  "Audit unavailable; tool was not started",
5410
5543
  "AUDIT_UNAVAILABLE",
5411
- "after_user_action"
5544
+ "after_user_action",
5545
+ {
5546
+ version: 1,
5547
+ dispatchOutcome: "not_sent",
5548
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5549
+ }
5412
5550
  );
5413
5551
  }
5414
5552
  return failure(
@@ -5431,7 +5569,11 @@ var ToolExecutor = class {
5431
5569
  toolUseId: call.id,
5432
5570
  content: decision.result,
5433
5571
  isError: decision.isError ?? false,
5434
- durationMs: Math.max(0, Date.now() - startedAt)
5572
+ durationMs: Math.max(0, Date.now() - startedAt),
5573
+ execution: {
5574
+ version: 1,
5575
+ dispatchOutcome: "not_sent"
5576
+ }
5435
5577
  };
5436
5578
  }
5437
5579
  }
@@ -5441,8 +5583,23 @@ var ToolExecutor = class {
5441
5583
  await audit("tool.finished", { status: "failed" });
5442
5584
  } catch {
5443
5585
  }
5444
- return failure("Tool preparation failed", "HOOK_ERROR");
5586
+ return failure("Tool preparation failed", "HOOK_ERROR", "never", {
5587
+ version: 1,
5588
+ dispatchOutcome: "not_sent",
5589
+ hostError: { phase: "hook", code: "HOOK_ERROR" }
5590
+ });
5445
5591
  }
5592
+ function deepFreeze(obj) {
5593
+ if (obj === null || typeof obj !== "object" || Object.isFrozen(obj)) {
5594
+ return obj;
5595
+ }
5596
+ Object.freeze(obj);
5597
+ for (const key of Object.keys(obj)) {
5598
+ deepFreeze(obj[key]);
5599
+ }
5600
+ return obj;
5601
+ }
5602
+ const finalInput = deepFreeze(structuredClone(currentInput));
5446
5603
  let tool;
5447
5604
  try {
5448
5605
  tool = this.tools.resolve(call.name);
@@ -5455,13 +5612,50 @@ var ToolExecutor = class {
5455
5612
  }
5456
5613
  return failure(
5457
5614
  known?.safeMessage ?? "Requested tool is unavailable",
5458
- known?.code ?? "TOOL_NOT_FOUND"
5615
+ known?.code ?? "TOOL_NOT_FOUND",
5616
+ "never",
5617
+ {
5618
+ version: 1,
5619
+ dispatchOutcome: "not_sent"
5620
+ }
5459
5621
  );
5460
5622
  }
5623
+ try {
5624
+ const validation = tool.validateInput?.(finalInput);
5625
+ if (validation && !validation.valid) {
5626
+ try {
5627
+ await audit("tool.decided", { decision: "deny", decisionSource: "validation" });
5628
+ await audit("tool.finished", { status: "failed", errorCode: "INVALID_INPUT" });
5629
+ } catch {
5630
+ }
5631
+ return failure(
5632
+ validation.message || "Tool input validation failed",
5633
+ "INVALID_INPUT",
5634
+ "never",
5635
+ {
5636
+ version: 1,
5637
+ dispatchOutcome: "not_sent",
5638
+ hostError: { phase: "validation", code: "INVALID_INPUT" }
5639
+ }
5640
+ );
5641
+ }
5642
+ } catch (err) {
5643
+ try {
5644
+ await audit("tool.decided", { decision: "deny", decisionSource: "validation" });
5645
+ await audit("tool.finished", { status: "failed", errorCode: "INVALID_INPUT" });
5646
+ } catch {
5647
+ }
5648
+ const msg = err instanceof Error ? err.message : "Tool input validation error";
5649
+ return failure(msg || "Tool input validation error", "INVALID_INPUT", "never", {
5650
+ version: 1,
5651
+ dispatchOutcome: "not_sent",
5652
+ hostError: { phase: "validation", code: "INVALID_INPUT" }
5653
+ });
5654
+ }
5461
5655
  try {
5462
5656
  return await this.executeResolved(
5463
5657
  call,
5464
- currentInput,
5658
+ finalInput,
5465
5659
  tool,
5466
5660
  startedAt,
5467
5661
  audit,
@@ -5477,14 +5671,18 @@ var ToolExecutor = class {
5477
5671
  return failure(
5478
5672
  "Tool outcome is unknown after an executor failure",
5479
5673
  "OUTCOME_UNKNOWN",
5480
- "after_user_action"
5674
+ "after_user_action",
5675
+ {
5676
+ version: 1,
5677
+ dispatchOutcome: "unknown"
5678
+ }
5481
5679
  );
5482
5680
  }
5483
5681
  }
5484
- async executeResolved(call, currentInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
5682
+ async executeResolved(call, finalInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
5485
5683
  let shell;
5486
5684
  try {
5487
- shell = tool.approvalScope?.(currentInput, this.rootDir);
5685
+ shell = tool.approvalScope?.(finalInput, this.rootDir);
5488
5686
  } catch {
5489
5687
  try {
5490
5688
  await audit("tool.decided", { decision: "deny", decisionSource: "invalid_target" });
@@ -5493,19 +5691,32 @@ var ToolExecutor = class {
5493
5691
  }
5494
5692
  return failure("Invalid tool execution target", "INVALID_TARGET");
5495
5693
  }
5694
+ let executionIdentity;
5695
+ try {
5696
+ executionIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
5697
+ } catch {
5698
+ try {
5699
+ await audit("tool.decided", { decision: "deny", decisionSource: "invalid_identity" });
5700
+ await audit("tool.finished", { status: "failed", errorCode: "INVALID_TARGET" });
5701
+ } catch {
5702
+ }
5703
+ return failure("Invalid tool execution identity", "INVALID_TARGET");
5704
+ }
5496
5705
  let auditFields = {};
5497
5706
  for (let attempt = 0; attempt < 2; attempt++) {
5498
5707
  const request = {
5499
5708
  toolName: call.name,
5500
5709
  capabilities: tool.metadata?.permissions ?? [],
5501
- input: currentInput,
5710
+ input: finalInput,
5502
5711
  rootDir: this.rootDir,
5503
5712
  ...shell ? { shell } : {},
5713
+ ...executionIdentity ? { executionIdentity } : {},
5504
5714
  // 解释器内建查询与内容已绑定摘要的工作区直引脚本可确认不受外部脚本变更影响,
5505
5715
  // 允许会话级批准;其余 shell 命令仅允许单次批准。
5716
+ // 非 shell 工具(含深度绑定 executionIdentity 的外部工具)允许会话级批准。
5506
5717
  sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
5507
5718
  };
5508
- const repeatKey = createHash11("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex");
5719
+ const repeatKey = createHash11("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex");
5509
5720
  if (this.neverFailures.has(repeatKey)) {
5510
5721
  try {
5511
5722
  await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
@@ -5514,7 +5725,12 @@ var ToolExecutor = class {
5514
5725
  return failure(
5515
5726
  "Audit unavailable; tool was not started",
5516
5727
  "AUDIT_UNAVAILABLE",
5517
- "after_user_action"
5728
+ "after_user_action",
5729
+ {
5730
+ version: 1,
5731
+ dispatchOutcome: "not_sent",
5732
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5733
+ }
5518
5734
  );
5519
5735
  }
5520
5736
  return failure(
@@ -5522,7 +5738,7 @@ var ToolExecutor = class {
5522
5738
  "REPEAT_BLOCKED"
5523
5739
  );
5524
5740
  }
5525
- const hardDenied = this.hardDenial?.(tool, currentInput);
5741
+ const hardDenied = this.hardDenial?.(tool, finalInput);
5526
5742
  const cached = this.approvalCache.get(request);
5527
5743
  const decision = this.policy.decide({
5528
5744
  hardDenied: hardDenied !== void 0,
@@ -5558,6 +5774,11 @@ var ToolExecutor = class {
5558
5774
  executableDigest: shell.executableDigest,
5559
5775
  executionFingerprint: shell.executionFingerprint,
5560
5776
  ...commandAuditSummary(shell.command)
5777
+ } : executionIdentity ? {
5778
+ serverIdentity: executionIdentity.serverIdentity,
5779
+ connectionGeneration: executionIdentity.connectionGeneration,
5780
+ catalogEpoch: executionIdentity.catalogEpoch,
5781
+ toolDigest: executionIdentity.toolDigest
5561
5782
  } : {};
5562
5783
  try {
5563
5784
  await audit("tool.decided", {
@@ -5571,7 +5792,12 @@ var ToolExecutor = class {
5571
5792
  return failure(
5572
5793
  "Audit unavailable; tool was not started",
5573
5794
  "AUDIT_UNAVAILABLE",
5574
- "after_user_action"
5795
+ "after_user_action",
5796
+ {
5797
+ version: 1,
5798
+ dispatchOutcome: "not_sent",
5799
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5800
+ }
5575
5801
  );
5576
5802
  }
5577
5803
  if (!permitted || this.signal?.aborted) {
@@ -5579,7 +5805,11 @@ var ToolExecutor = class {
5579
5805
  try {
5580
5806
  await audit("tool.finished", { status: "denied", errorCode: code });
5581
5807
  } catch {
5582
- return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action");
5808
+ return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
5809
+ version: 1,
5810
+ dispatchOutcome: "not_sent",
5811
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5812
+ });
5583
5813
  }
5584
5814
  return failure(
5585
5815
  this.signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
@@ -5589,7 +5819,7 @@ var ToolExecutor = class {
5589
5819
  }
5590
5820
  if (shell) {
5591
5821
  try {
5592
- const next = tool.approvalScope?.(currentInput, this.rootDir);
5822
+ const next = tool.approvalScope?.(finalInput, this.rootDir);
5593
5823
  if (next?.executionFingerprint !== shell.executionFingerprint) {
5594
5824
  if (attempt === 0 && next) {
5595
5825
  try {
@@ -5598,7 +5828,12 @@ var ToolExecutor = class {
5598
5828
  return failure(
5599
5829
  "Audit unavailable; tool was not started",
5600
5830
  "AUDIT_UNAVAILABLE",
5601
- "after_user_action"
5831
+ "after_user_action",
5832
+ {
5833
+ version: 1,
5834
+ dispatchOutcome: "not_sent",
5835
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5836
+ }
5602
5837
  );
5603
5838
  }
5604
5839
  shell = next;
@@ -5619,6 +5854,37 @@ var ToolExecutor = class {
5619
5854
  );
5620
5855
  }
5621
5856
  }
5857
+ if (executionIdentity) {
5858
+ try {
5859
+ const nextIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
5860
+ if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
5861
+ await audit("tool.finished", {
5862
+ status: "identity_changed",
5863
+ errorCode: "MCP_TOOL_CHANGED"
5864
+ });
5865
+ return failure(
5866
+ "Tool execution identity changed after approval",
5867
+ "MCP_TOOL_CHANGED",
5868
+ "after_user_action",
5869
+ {
5870
+ version: 1,
5871
+ dispatchOutcome: "not_sent",
5872
+ hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
5873
+ }
5874
+ );
5875
+ }
5876
+ } catch {
5877
+ return failure(
5878
+ "Tool execution identity check failed after approval",
5879
+ "MCP_TOOL_CHANGED",
5880
+ "after_user_action",
5881
+ {
5882
+ version: 1,
5883
+ dispatchOutcome: "not_sent"
5884
+ }
5885
+ );
5886
+ }
5887
+ }
5622
5888
  break;
5623
5889
  }
5624
5890
  try {
@@ -5627,13 +5893,19 @@ var ToolExecutor = class {
5627
5893
  return failure(
5628
5894
  "Audit unavailable; tool was not started",
5629
5895
  "AUDIT_UNAVAILABLE",
5630
- "after_user_action"
5896
+ "after_user_action",
5897
+ {
5898
+ version: 1,
5899
+ dispatchOutcome: "not_sent",
5900
+ hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
5901
+ }
5631
5902
  );
5632
5903
  }
5633
5904
  let outputStr;
5634
5905
  let isError = false;
5635
5906
  let errorCode;
5636
5907
  let retryPolicy;
5908
+ let execution;
5637
5909
  try {
5638
5910
  const executionController = new AbortController();
5639
5911
  const abortFromSession = () => executionController.abort();
@@ -5652,16 +5924,52 @@ var ToolExecutor = class {
5652
5924
  } : {}
5653
5925
  };
5654
5926
  try {
5655
- const rawResult = tool.metadata?.managesTimeout ? await tool.execute(currentInput, toolCtx) : await this.withTimeout(
5656
- tool.execute(currentInput, toolCtx),
5927
+ const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await this.withTimeout(
5928
+ tool.execute(finalInput, toolCtx),
5657
5929
  call.name,
5658
5930
  executionController
5659
5931
  );
5660
- outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
5661
- if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
5662
- isError = true;
5663
- errorCode = "COMMAND_EXIT_NONZERO";
5664
- retryPolicy = "never";
5932
+ if (typeof tool.toExecutionResult === "function") {
5933
+ try {
5934
+ const converted = tool.toExecutionResult(rawResult);
5935
+ outputStr = converted.content;
5936
+ isError = converted.isError ?? false;
5937
+ errorCode = converted.errorCode;
5938
+ retryPolicy = converted.retryPolicy;
5939
+ execution = converted.execution ?? {
5940
+ version: 1,
5941
+ dispatchOutcome: isError ? "failed" : "succeeded"
5942
+ };
5943
+ } catch (convErr) {
5944
+ isError = true;
5945
+ errorCode = "MCP_RESULT_INVALID";
5946
+ retryPolicy = "after_user_action";
5947
+ outputStr = convErr instanceof Error ? convErr.message : "Tool result projection failed";
5948
+ execution = {
5949
+ version: 1,
5950
+ dispatchOutcome: "succeeded",
5951
+ hostError: {
5952
+ phase: "projection",
5953
+ code: "MCP_RESULT_INVALID"
5954
+ }
5955
+ };
5956
+ }
5957
+ } else {
5958
+ outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
5959
+ if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
5960
+ isError = true;
5961
+ errorCode = "COMMAND_EXIT_NONZERO";
5962
+ retryPolicy = "never";
5963
+ execution = {
5964
+ version: 1,
5965
+ dispatchOutcome: "failed"
5966
+ };
5967
+ } else {
5968
+ execution = {
5969
+ version: 1,
5970
+ dispatchOutcome: "succeeded"
5971
+ };
5972
+ }
5665
5973
  }
5666
5974
  } finally {
5667
5975
  this.signal?.removeEventListener("abort", abortFromSession);
@@ -5672,13 +5980,19 @@ var ToolExecutor = class {
5672
5980
  outputStr = known?.safeMessage ?? "Tool execution failed";
5673
5981
  errorCode = known?.code ?? "TOOL_FAILED";
5674
5982
  retryPolicy = known?.retryPolicy ?? "never";
5983
+ const outcome = known?.dispatchOutcome ?? (errorCode === "OUTCOME_UNKNOWN" || known && known.retryPolicy === "after_user_action" ? "unknown" : "failed");
5984
+ execution = {
5985
+ version: 1,
5986
+ dispatchOutcome: outcome
5987
+ };
5675
5988
  }
5676
5989
  const resultBlock = {
5677
5990
  type: "tool_result",
5678
5991
  toolUseId: call.id,
5679
5992
  content: outputStr,
5680
5993
  isError,
5681
- ...errorCode ? { errorCode, retryPolicy } : {}
5994
+ ...errorCode ? { errorCode, retryPolicy } : {},
5995
+ execution
5682
5996
  };
5683
5997
  if (resultBlock.isError && resultBlock.errorCode) {
5684
5998
  resultBlock.content = JSON.stringify({
@@ -5691,7 +6005,7 @@ var ToolExecutor = class {
5691
6005
  for (const hook of this.hooks.get("tool:after")) {
5692
6006
  await hook(
5693
6007
  hookCtx,
5694
- { id: call.id, name: call.name, input: currentInput },
6008
+ { id: call.id, name: call.name, input: finalInput },
5695
6009
  { output: outputStr, isError }
5696
6010
  );
5697
6011
  }
@@ -5704,11 +6018,19 @@ var ToolExecutor = class {
5704
6018
  resultBlock.isError = true;
5705
6019
  resultBlock.errorCode = "HOOK_ERROR";
5706
6020
  resultBlock.retryPolicy = "after_user_action";
6021
+ resultBlock.execution = {
6022
+ version: 1,
6023
+ dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
6024
+ hostError: {
6025
+ phase: "hook",
6026
+ code: "HOOK_ERROR"
6027
+ }
6028
+ };
5707
6029
  }
5708
6030
  resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
5709
6031
  if (resultBlock.isError && resultBlock.retryPolicy === "never") {
5710
6032
  this.neverFailures.add(
5711
- createHash11("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex")
6033
+ createHash11("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
5712
6034
  );
5713
6035
  }
5714
6036
  try {
@@ -5718,13 +6040,25 @@ var ToolExecutor = class {
5718
6040
  durationMs: resultBlock.durationMs
5719
6041
  });
5720
6042
  } catch {
5721
- return failure(
5722
- "Tool outcome is unknown because the audit result could not be written",
5723
- "OUTCOME_UNKNOWN",
5724
- "after_user_action"
5725
- );
6043
+ resultBlock.isError = true;
6044
+ resultBlock.errorCode = "OUTCOME_UNKNOWN";
6045
+ resultBlock.retryPolicy = "after_user_action";
6046
+ resultBlock.content = JSON.stringify({
6047
+ code: "OUTCOME_UNKNOWN",
6048
+ retryPolicy: "after_user_action",
6049
+ message: "Tool outcome is unknown because the audit result could not be written"
6050
+ });
6051
+ resultBlock.execution = {
6052
+ version: 1,
6053
+ dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
6054
+ hostError: {
6055
+ phase: "audit",
6056
+ code: "AUDIT_UNAVAILABLE"
6057
+ }
6058
+ };
6059
+ return resultBlock;
5726
6060
  }
5727
- await this.onToolResult?.(tool, currentInput, resultBlock);
6061
+ await this.onToolResult?.(tool, finalInput, resultBlock);
5728
6062
  return resultBlock;
5729
6063
  }
5730
6064
  /**
@@ -7499,6 +7833,7 @@ var AgentLoop = class {
7499
7833
  let consecutiveErrors = 0;
7500
7834
  let completed = false;
7501
7835
  let overflowRetried = false;
7836
+ const toolSnapshot = "snapshot" in this.tools ? this.tools.snapshot() : this.tools;
7502
7837
  while (step < this.maxSteps) {
7503
7838
  step++;
7504
7839
  const turnStartTime = Date.now();
@@ -7509,7 +7844,7 @@ var AgentLoop = class {
7509
7844
  let request = {
7510
7845
  systemPrompt: this.systemPrompt,
7511
7846
  messages: structuredClone(this.projectHistory?.(history) ?? history),
7512
- tools: visibleTools(this.tools.list(), this.getMode(), this.permissionRules).map(
7847
+ tools: visibleTools(toolSnapshot.list(), this.getMode(), this.permissionRules).map(
7513
7848
  toToolDefinition
7514
7849
  ),
7515
7850
  signal: this.signal,
@@ -7518,7 +7853,30 @@ var AgentLoop = class {
7518
7853
  const beforeModelHooks = this.hooks.get("model:before");
7519
7854
  const hookCtx = { signal: this.signal };
7520
7855
  for (const hook of beforeModelHooks) {
7521
- request = await hook(hookCtx, request);
7856
+ const modified = await hook(hookCtx, request);
7857
+ if (modified) {
7858
+ request = modified;
7859
+ }
7860
+ }
7861
+ if (request.tools) {
7862
+ for (const t of request.tools) {
7863
+ const original = toolSnapshot.get(t.name);
7864
+ if (!original) {
7865
+ throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
7866
+ stage: "request",
7867
+ retryable: false
7868
+ });
7869
+ }
7870
+ if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
7871
+ throw new ModelError(
7872
+ `Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
7873
+ {
7874
+ stage: "request",
7875
+ retryable: false
7876
+ }
7877
+ );
7878
+ }
7879
+ }
7522
7880
  }
7523
7881
  if (this.prepareRequest) request = yield* this.prepareRequest(request, history);
7524
7882
  yield {
@@ -8258,6 +8616,14 @@ var AgentSession = class {
8258
8616
  });
8259
8617
  this.plugins.push(plugin);
8260
8618
  }
8619
+ /**
8620
+ * 获取当前会话已成功挂载的扩展插件只读列表。
8621
+ *
8622
+ * 保证返回当前状态的独立切片,外部修改不污染内部挂载表。
8623
+ */
8624
+ getPlugins() {
8625
+ return [...this.plugins];
8626
+ }
8261
8627
  /**
8262
8628
  * 空闲时原子替换角色的模型绑定;本方法不改变当前角色或工具权限。
8263
8629
  * @param profile 模型协议、请求目标和输出预算。
@@ -8324,6 +8690,10 @@ var AgentSession = class {
8324
8690
  getMode() {
8325
8691
  return this.mode;
8326
8692
  }
8693
+ /** 获取当前会话生效的权限规则列表。 */
8694
+ getPermissionRules() {
8695
+ return this.permissionRules;
8696
+ }
8327
8697
  /** 返回最近一次成功装载的来源路径,不暴露指令正文。 */
8328
8698
  getInstructionSources() {
8329
8699
  return this.instructionSnapshot?.sources.map((source) => source.path) ?? [];
@@ -8452,8 +8822,23 @@ var AgentSession = class {
8452
8822
  options?.signal
8453
8823
  );
8454
8824
  }
8825
+ /**
8826
+ * 受控任务准备入口:在同一运行锁内异步准备文本块/来源,预算和存储成功后才进入模型。
8827
+ *
8828
+ * @param task 用户显式任务正文(或命令描述)。
8829
+ * @param prepare 异步任务准备函数;在锁内执行,若准备失败或取消则释放锁且零 append。
8830
+ * @param options 可选中止信号与显式主任务角色。
8831
+ * @returns 结构化事件的异步流。
8832
+ * @throws {SessionBusyError} 当前会话忙碌时抛出。
8833
+ */
8834
+ runPreparedTask(task, prepare, options) {
8835
+ return cancellableGenerator(
8836
+ (signal) => this.runOperation(task, { signal, role: options?.role }, void 0, prepare),
8837
+ options?.signal
8838
+ );
8839
+ }
8455
8840
  /** 单次任务执行主体;调用方 return 的中止与 finally 清理由外层统一协调。 */
8456
- async *runOperation(userInput, options, skillName) {
8841
+ async *runOperation(userInput, options, skillName, prepare) {
8457
8842
  this.assertIdle("start another run");
8458
8843
  const role = options?.role ?? this.primaryRole;
8459
8844
  const { profile, provider } = this.resolvePrimaryBinding(role);
@@ -8537,12 +8922,22 @@ var AgentSession = class {
8537
8922
  profile.contextWindow,
8538
8923
  profile.maxOutputTokens
8539
8924
  ).inputBudget;
8925
+ const toolSnapshot = this.tools.snapshot();
8926
+ let prepared;
8927
+ if (prepare) {
8928
+ prepared = await prepare({ signal: options?.signal, runId, tools: toolSnapshot });
8929
+ if (options?.signal?.aborted) throw new AbortError();
8930
+ if (prepared.skillName) skillName = prepared.skillName;
8931
+ }
8540
8932
  await this.skillRegistry?.refresh(options?.signal);
8541
8933
  this.skillRuntime?.syncTools();
8934
+ const extraPreparedTokens = prepared?.content ? typeof prepared.content === "string" ? estimateSkillTokens(prepared.content) : prepared.content.filter(isTextBlock).reduce((sum, b) => sum + estimateSkillTokens(b.text), 0) : 0;
8542
8935
  const promptAssembler = this.createPromptAssembler(
8543
8936
  profile,
8544
8937
  this.context.project(this.history),
8545
- 32 + estimateSkillTokens(userInput)
8938
+ 32 + estimateSkillTokens(userInput) + extraPreparedTokens,
8939
+ toolSnapshot.list(),
8940
+ toolSnapshot
8546
8941
  );
8547
8942
  this.skillRuntime?.begin(
8548
8943
  runId,
@@ -8551,7 +8946,7 @@ var AgentSession = class {
8551
8946
  const current = {
8552
8947
  ...previousRequest ?? { systemPrompt: promptAssembler.assemble() },
8553
8948
  messages: [...this.context.project(this.history), ...budgetToolMessages],
8554
- tools: previousRequest?.tools ?? visibleTools(this.tools.list(), this.mode, this.permissionRules).map((tool) => ({
8949
+ tools: previousRequest?.tools ?? visibleTools(toolSnapshot.list(), this.mode, this.permissionRules).map((tool) => ({
8555
8950
  name: tool.name,
8556
8951
  description: tool.description,
8557
8952
  parameters: tool.parameters
@@ -8559,7 +8954,7 @@ var AgentSession = class {
8559
8954
  };
8560
8955
  return Math.max(
8561
8956
  0,
8562
- inputBudget - (this.estimator ?? new UnicodeTokenEstimator()).estimate(current).total - (previousRequest ? 0 : estimateSkillTokens(userInput) + 32)
8957
+ inputBudget - (this.estimator ?? new UnicodeTokenEstimator()).estimate(current).total - (previousRequest ? 0 : estimateSkillTokens(userInput) + extraPreparedTokens + 32)
8563
8958
  );
8564
8959
  },
8565
8960
  () => previousRequest?.messages ?? this.context.project(this.history)
@@ -8571,6 +8966,51 @@ var AgentSession = class {
8571
8966
  await this.authorizeSkill(skillName, runId, options?.signal);
8572
8967
  preparedSkill = await this.skillRuntime.prepare(skillName, "user", options?.signal);
8573
8968
  }
8969
+ if (prepared?.verifyIdentity) {
8970
+ const valid = await prepared.verifyIdentity();
8971
+ if (!valid) {
8972
+ throw new KapibalaError("Task preparation identity changed before commit", {
8973
+ code: "MCP_PROMPT_CHANGED",
8974
+ retryPolicy: "never"
8975
+ });
8976
+ }
8977
+ }
8978
+ const userContent = [{ type: "text", text: userInput }];
8979
+ if (preparedSkill)
8980
+ userContent.push({
8981
+ type: "text",
8982
+ text: preparedSkill.text,
8983
+ skillSource: preparedSkill.source
8984
+ });
8985
+ if (prepared?.content) {
8986
+ if (typeof prepared.content === "string") {
8987
+ userContent.push({
8988
+ type: "text",
8989
+ text: prepared.content,
8990
+ ...prepared.promptSource ? { promptSource: prepared.promptSource } : {}
8991
+ });
8992
+ } else {
8993
+ userContent.push(...prepared.content);
8994
+ }
8995
+ }
8996
+ const userMessage = {
8997
+ id: createMessageId(),
8998
+ interactionId: runId,
8999
+ state: "completed",
9000
+ role: "user",
9001
+ content: userContent,
9002
+ timestamp: Date.now()
9003
+ };
9004
+ const taskBudgetCheck = (this.estimator ?? new UnicodeTokenEstimator()).estimate({
9005
+ systemPrompt: promptAssembler.assemble(),
9006
+ messages: [userMessage]
9007
+ }).total;
9008
+ if (taskBudgetCheck > inputBudget) {
9009
+ throw new ContextOverflowError(400, {
9010
+ message: "Prepared task content exceeds context input budget",
9011
+ stage: "request"
9012
+ });
9013
+ }
8574
9014
  this.totalTurns++;
8575
9015
  if (this.store && supportsSessionState(this.store)) {
8576
9016
  const currentProject = resolveSessionProject(this.cwd);
@@ -8584,24 +9024,39 @@ var AgentSession = class {
8584
9024
  });
8585
9025
  }
8586
9026
  runStarted = true;
8587
- const userMessage = {
8588
- id: createMessageId(),
8589
- interactionId: runId,
8590
- state: "completed",
8591
- role: "user",
8592
- content: [{ type: "text", text: userInput }],
8593
- timestamp: Date.now()
8594
- };
8595
- if (preparedSkill)
8596
- userMessage.content.push({
8597
- type: "text",
8598
- text: preparedSkill.text,
8599
- skillSource: preparedSkill.source
8600
- });
8601
9027
  if (this.store) {
8602
9028
  await this.store.append(userMessage);
8603
9029
  }
8604
9030
  this.history.push(userMessage);
9031
+ if (prepared?.promptSource) {
9032
+ const persistence = this.store ? "disk" : "memory";
9033
+ yield {
9034
+ type: "mcp_prompt_loaded",
9035
+ server: prepared.promptSource.server,
9036
+ name: prepared.promptSource.name,
9037
+ persistence,
9038
+ promptSource: prepared.promptSource,
9039
+ runId
9040
+ };
9041
+ try {
9042
+ await this.eventLogger.recordAudit({
9043
+ level: "info",
9044
+ event: "mcp.prompt_loaded",
9045
+ sessionId: this.sessionId,
9046
+ runId,
9047
+ fields: {
9048
+ server: prepared.promptSource.server,
9049
+ promptName: prepared.promptSource.name,
9050
+ contentDigest: prepared.promptSource.contentDigest,
9051
+ persistence
9052
+ }
9053
+ });
9054
+ } catch (auditErr) {
9055
+ const msg = `MCP prompt audit recording failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`;
9056
+ this.logger?.(msg);
9057
+ this.onDiagnostic?.(msg);
9058
+ }
9059
+ }
8605
9060
  if (preparedSkill && this.skillRuntime) {
8606
9061
  this.skillRuntime.acceptUser(preparedSkill);
8607
9062
  for (const event of await this.commitSkills(runId)) yield event;
@@ -8612,7 +9067,7 @@ var AgentSession = class {
8612
9067
  const executor = new ToolExecutor({
8613
9068
  hardDenial: (tool) => this.skillRuntime?.denial(tool),
8614
9069
  onToolResult: (tool, _input, result) => this.skillRuntime?.acceptResult(tool, result),
8615
- tools: this.tools,
9070
+ tools: toolSnapshot,
8616
9071
  hooks: this.hooks,
8617
9072
  rootDir: this.rootDir,
8618
9073
  signal: options?.signal,
@@ -8734,12 +9189,12 @@ var AgentSession = class {
8734
9189
  const skills = this.skillRuntime;
8735
9190
  const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
8736
9191
  return (async function* () {
8737
- const prepared = yield* preparation;
8738
- skills?.assertProjection(prepared.messages);
8739
- previousRequest = structuredClone({ ...prepared, signal: void 0 });
9192
+ const prepared2 = yield* preparation;
9193
+ skills?.assertProjection(prepared2.messages);
9194
+ previousRequest = structuredClone({ ...prepared2, signal: void 0 });
8740
9195
  lastPromptTokens = void 0;
8741
9196
  lastEstimatedPromptTokens = context.getSnapshot()?.estimate.total;
8742
- return prepared;
9197
+ return prepared2;
8743
9198
  })();
8744
9199
  },
8745
9200
  onUsage: async (request, usage, attemptId) => {
@@ -8758,7 +9213,7 @@ var AgentSession = class {
8758
9213
  },
8759
9214
  provider,
8760
9215
  executor,
8761
- tools: this.tools,
9216
+ tools: toolSnapshot,
8762
9217
  hooks: this.hooks,
8763
9218
  systemPrompt: assembledSystemPrompt,
8764
9219
  maxSteps: this.maxSteps,
@@ -8964,11 +9419,12 @@ var AgentSession = class {
8964
9419
  if (this.activeRun) throw new SessionBusyError(operation);
8965
9420
  }
8966
9421
  /** 实际请求、静态统计和手动压缩共用目录空间推导;先计正文、工具和非目录提示,再预算整段目录。 */
8967
- createPromptAssembler(profile, messages, extraTokens = 0) {
8968
- const tools = visibleTools(this.tools.list(), this.mode, this.permissionRules);
9422
+ createPromptAssembler(profile, messages, extraTokens = 0, toolList, toolRegistry) {
9423
+ const registry = toolRegistry ?? this.tools;
9424
+ const tools = visibleTools(toolList ?? registry.list(), this.mode, this.permissionRules);
8969
9425
  const options = {
8970
9426
  skills: this.skillRegistry,
8971
- tools: this.tools,
9427
+ tools: registry,
8972
9428
  rootDir: this.rootDir,
8973
9429
  customInstructions: this.customSystemPrompt,
8974
9430
  instructionSources: this.instructionSnapshot?.sources,
@@ -9268,6 +9724,7 @@ export {
9268
9724
  OpenAIResponsesProvider,
9269
9725
  PathSandbox,
9270
9726
  PermissionPolicy,
9727
+ PreDispatchError,
9271
9728
  PromptAssembler,
9272
9729
  SESSION_SCHEMA_VERSION,
9273
9730
  SKILL_LIMITS,