@sema-agent/client-core 0.77.0 → 0.77.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +5 -4
  3. package/dist/abortableSleep.d.ts +0 -28
  4. package/dist/abortableSleep.js +0 -28
  5. package/dist/adapt/arms.d.ts +0 -46
  6. package/dist/adapt/arms.js +9 -544
  7. package/dist/adapt/ids.d.ts +0 -58
  8. package/dist/adapt/ids.js +0 -63
  9. package/dist/adapt/instanceLedger.d.ts +0 -25
  10. package/dist/adapt/instanceLedger.js +0 -20
  11. package/dist/adapt/panelTasks.d.ts +0 -69
  12. package/dist/adapt/panelTasks.js +0 -95
  13. package/dist/adapt/textSegmentAuthority.d.ts +1 -137
  14. package/dist/adapt/textSegmentAuthority.js +0 -54
  15. package/dist/adapt/textStream.d.ts +0 -252
  16. package/dist/adapt/textStream.js +2 -281
  17. package/dist/adapt/toolCards.d.ts +0 -46
  18. package/dist/adapt/toolCards.js +0 -23
  19. package/dist/adapt/turnFlags.d.ts +0 -54
  20. package/dist/adapt/turnFlags.js +1 -17
  21. package/dist/adapt/wireShapes.d.ts +0 -92
  22. package/dist/adapt/wireShapes.js +0 -83
  23. package/dist/adapt.d.ts +0 -55
  24. package/dist/adapt.js +1 -120
  25. package/dist/adapter/activeRunSelfHeal.d.ts +22 -515
  26. package/dist/adapter/activeRunSelfHeal.js +10 -625
  27. package/dist/adapter/downstream/eventToSdkMessage.d.ts +2 -305
  28. package/dist/adapter/downstream/eventToSdkMessage.js +2 -867
  29. package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -314
  30. package/dist/adapter/downstream/terminalToSdkResult.js +13 -560
  31. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +0 -95
  32. package/dist/adapter/downstream/turnUsageToModelUsage.js +2 -31
  33. package/dist/adapter/runStream.d.ts +0 -206
  34. package/dist/adapter/runStream.js +6 -596
  35. package/dist/adapter/types.d.ts +0 -111
  36. package/dist/adapter/types.js +0 -29
  37. package/dist/agentSession/backgroundView.d.ts +0 -106
  38. package/dist/agentSession/backgroundView.js +3 -49
  39. package/dist/agentSession/contract.d.ts +0 -98
  40. package/dist/agentSession/contract.js +0 -12
  41. package/dist/agentsWireCaps.d.ts +0 -72
  42. package/dist/agentsWireCaps.js +4 -72
  43. package/dist/approvalsStreamLiveCapability.d.ts +0 -25
  44. package/dist/approvalsStreamLiveCapability.js +0 -31
  45. package/dist/argvFlagValue.d.ts +0 -33
  46. package/dist/argvFlagValue.js +3 -35
  47. package/dist/attachmentsWireCaps.d.ts +0 -127
  48. package/dist/attachmentsWireCaps.js +0 -144
  49. package/dist/autoModeUnavailable.d.ts +0 -120
  50. package/dist/autoModeUnavailable.js +0 -144
  51. package/dist/classifierStatus.d.ts +0 -72
  52. package/dist/classifierStatus.js +0 -167
  53. package/dist/classifierVerdictWire.d.ts +0 -54
  54. package/dist/classifierVerdictWire.js +0 -133
  55. package/dist/clientContextWireCaps.d.ts +0 -37
  56. package/dist/clientContextWireCaps.js +0 -36
  57. package/dist/clientSlice.d.ts +0 -63
  58. package/dist/clientSlice.js +0 -45
  59. package/dist/cloudConfigWireCaps.d.ts +0 -86
  60. package/dist/cloudConfigWireCaps.js +2 -65
  61. package/dist/compensations.d.ts +0 -52
  62. package/dist/compensations.js +2 -63
  63. package/dist/controlRouter.d.ts +2 -208
  64. package/dist/controlRouter.js +1 -129
  65. package/dist/coreValuePorts.d.ts +0 -84
  66. package/dist/coreValuePorts.js +0 -29
  67. package/dist/decideReceipt.d.ts +0 -110
  68. package/dist/decideReceipt.js +0 -84
  69. package/dist/detachWire.d.ts +0 -130
  70. package/dist/detachWire.js +1 -130
  71. package/dist/deviceExecutorManagementCapability.d.ts +2 -35
  72. package/dist/deviceExecutorManagementCapability.js +1 -37
  73. package/dist/diagnostics.d.ts +0 -8
  74. package/dist/diagnostics.js +0 -8
  75. package/dist/diff/patch.d.ts +0 -17
  76. package/dist/diff/patch.js +0 -19
  77. package/dist/effectiveFacts.d.ts +0 -40
  78. package/dist/effectiveFacts.js +0 -22
  79. package/dist/effortWire.d.ts +0 -11
  80. package/dist/effortWire.js +0 -12
  81. package/dist/engineAgentPanelStore.d.ts +0 -169
  82. package/dist/engineAgentPanelStore.js +13 -293
  83. package/dist/engineCapReader.d.ts +0 -62
  84. package/dist/engineCapReader.js +1 -43
  85. package/dist/engineCapsCache.d.ts +0 -177
  86. package/dist/engineCapsCache.js +0 -199
  87. package/dist/engineCapsGenerationGuard.d.ts +0 -2
  88. package/dist/engineCapsGenerationGuard.js +0 -11
  89. package/dist/engineErrorCodes.d.ts +1 -316
  90. package/dist/engineErrorCodes.js +0 -427
  91. package/dist/engineHttpTools.d.ts +0 -29
  92. package/dist/engineHttpTools.js +0 -29
  93. package/dist/engineIdentity.d.ts +0 -83
  94. package/dist/engineIdentity.js +0 -88
  95. package/dist/engineInlineTaskStats.d.ts +0 -57
  96. package/dist/engineInlineTaskStats.js +1 -42
  97. package/dist/engineNoticeCodes.d.ts +0 -190
  98. package/dist/engineNoticeCodes.js +0 -182
  99. package/dist/engineSessionParam.d.ts +0 -30
  100. package/dist/engineSessionParam.js +0 -52
  101. package/dist/engineToolLabelStore.d.ts +0 -29
  102. package/dist/engineToolLabelStore.js +0 -31
  103. package/dist/engineWireSdk.d.ts +0 -90
  104. package/dist/engineWireSdk.js +0 -74
  105. package/dist/engineWireTarget.d.ts +0 -14
  106. package/dist/engineWireTarget.js +0 -39
  107. package/dist/env/localeGeo.d.ts +0 -12
  108. package/dist/env/localeGeo.js +2 -77
  109. package/dist/env/localeTag.d.ts +0 -34
  110. package/dist/env/localeTag.js +0 -32
  111. package/dist/env/uiLanguage.d.ts +0 -13
  112. package/dist/env/uiLanguage.js +0 -25
  113. package/dist/envFlag.d.ts +0 -37
  114. package/dist/envFlag.js +0 -40
  115. package/dist/executionLaneCapability.d.ts +0 -48
  116. package/dist/executionLaneCapability.js +0 -54
  117. package/dist/finalVerifyWire.d.ts +0 -67
  118. package/dist/finalVerifyWire.js +3 -43
  119. package/dist/fleet/fleetLedger.d.ts +0 -308
  120. package/dist/fleet/fleetLedger.js +10 -429
  121. package/dist/fleet/fleetProjection.d.ts +0 -240
  122. package/dist/fleet/fleetProjection.js +0 -189
  123. package/dist/fleet/fleetRowAgentType.d.ts +0 -6
  124. package/dist/fleet/fleetRowAgentType.js +1 -32
  125. package/dist/fleet/workflowSizeWarning.d.ts +0 -47
  126. package/dist/fleet/workflowSizeWarning.js +1 -46
  127. package/dist/fleetAgentPanelProjection.d.ts +0 -48
  128. package/dist/fleetAgentPanelProjection.js +8 -173
  129. package/dist/fleetTaskDesc.d.ts +0 -39
  130. package/dist/fleetTaskDesc.js +0 -69
  131. package/dist/forkWireCaps.d.ts +0 -23
  132. package/dist/forkWireCaps.js +1 -24
  133. package/dist/gateOutcome.d.ts +0 -140
  134. package/dist/gateOutcome.js +0 -85
  135. package/dist/gateVocabulary.d.ts +0 -114
  136. package/dist/gateVocabulary.js +1 -161
  137. package/dist/goalStopHook.d.ts +0 -127
  138. package/dist/goalStopHook.js +0 -178
  139. package/dist/headlessPermissionModeWire.d.ts +0 -76
  140. package/dist/headlessPermissionModeWire.js +1 -155
  141. package/dist/headlessReconnectWire.d.ts +0 -84
  142. package/dist/headlessReconnectWire.js +13 -67
  143. package/dist/hitl/approvalDecisionNoteAudit.d.ts +0 -49
  144. package/dist/hitl/approvalDecisionNoteAudit.js +0 -56
  145. package/dist/hitl/approvalOutcomeNote.d.ts +0 -2
  146. package/dist/hitl/approvalOutcomeNote.js +0 -20
  147. package/dist/hitl/approvalResolution.d.ts +0 -113
  148. package/dist/hitl/approvalResolution.js +0 -66
  149. package/dist/hitl/approvalsFeed.d.ts +0 -183
  150. package/dist/hitl/approvalsFeed.js +12 -243
  151. package/dist/hitl/armedGateRegistry.d.ts +0 -55
  152. package/dist/hitl/armedGateRegistry.js +0 -146
  153. package/dist/hitl/askGateWire.d.ts +0 -86
  154. package/dist/hitl/askGateWire.js +1 -95
  155. package/dist/hitl/askParkRowRouting.d.ts +0 -123
  156. package/dist/hitl/askParkRowRouting.js +1 -89
  157. package/dist/hitl/crashConverged.d.ts +0 -148
  158. package/dist/hitl/crashConverged.js +0 -214
  159. package/dist/hitl/editedRuleTextPrecheck.d.ts +0 -109
  160. package/dist/hitl/editedRuleTextPrecheck.js +0 -71
  161. package/dist/hitl/frameRouter.d.ts +0 -134
  162. package/dist/hitl/frameRouter.js +6 -378
  163. package/dist/hitl/gateIdentity.d.ts +0 -39
  164. package/dist/hitl/gateIdentity.js +0 -41
  165. package/dist/hitl/gateLedger.d.ts +0 -267
  166. package/dist/hitl/gateLedger.js +0 -121
  167. package/dist/hitl/hitlBridge.d.ts +7 -431
  168. package/dist/hitl/hitlBridge.js +7 -476
  169. package/dist/hitl/hitlHostSurface.d.ts +0 -150
  170. package/dist/hitl/hitlHostSurface.js +0 -169
  171. package/dist/hitl/livePendingAsk.d.ts +0 -91
  172. package/dist/hitl/livePendingAsk.js +0 -74
  173. package/dist/hitl/localAllowRule.d.ts +0 -62
  174. package/dist/hitl/localAllowRule.js +1 -33
  175. package/dist/hitl/parkOwnership.d.ts +0 -56
  176. package/dist/hitl/parkOwnership.js +0 -22
  177. package/dist/hitl/parkResolver.d.ts +3 -88
  178. package/dist/hitl/parkResolver.js +8 -349
  179. package/dist/hitl/parkRowBirthWait.d.ts +2 -26
  180. package/dist/hitl/parkRowBirthWait.js +3 -93
  181. package/dist/hitl/persistedRulesWire.d.ts +13 -358
  182. package/dist/hitl/persistedRulesWire.js +0 -310
  183. package/dist/hitl/planReviewWire.d.ts +4 -176
  184. package/dist/hitl/planReviewWire.js +9 -312
  185. package/dist/hitl/resumeRunningCard.d.ts +0 -105
  186. package/dist/hitl/resumeRunningCard.js +0 -105
  187. package/dist/hitl/sessionPolicyWire.d.ts +11 -191
  188. package/dist/hitl/sessionPolicyWire.js +0 -149
  189. package/dist/hitl/suspendedReopen.d.ts +3 -24
  190. package/dist/hitl/suspendedReopen.js +0 -14
  191. package/dist/hitl/toolApprovalWire.d.ts +5 -1308
  192. package/dist/hitl/toolApprovalWire.js +5 -945
  193. package/dist/hooksWireCaps.d.ts +0 -38
  194. package/dist/hooksWireCaps.js +0 -190
  195. package/dist/host.d.ts +0 -105
  196. package/dist/host.js +0 -33
  197. package/dist/hostEnv.d.ts +0 -14
  198. package/dist/hostEnv.js +0 -13
  199. package/dist/imagesWireCaps.d.ts +0 -21
  200. package/dist/imagesWireCaps.js +0 -22
  201. package/dist/index.d.ts +0 -134
  202. package/dist/index.js +0 -476
  203. package/dist/interactiveHalt.d.ts +5 -153
  204. package/dist/interactiveHalt.js +0 -111
  205. package/dist/interactiveToolsWire.d.ts +0 -62
  206. package/dist/interactiveToolsWire.js +1 -67
  207. package/dist/leaderConflict.d.ts +0 -59
  208. package/dist/leaderConflict.js +0 -48
  209. package/dist/limitsWire.d.ts +0 -125
  210. package/dist/limitsWire.js +2 -120
  211. package/dist/liveInitToolFace.d.ts +0 -77
  212. package/dist/liveInitToolFace.js +1 -109
  213. package/dist/liveModelCatalog.d.ts +0 -52
  214. package/dist/liveModelCatalog.js +0 -48
  215. package/dist/liveQuestionStore.d.ts +0 -87
  216. package/dist/liveQuestionStore.js +1 -56
  217. package/dist/mcpLiveness.d.ts +0 -151
  218. package/dist/mcpLiveness.js +0 -122
  219. package/dist/mcpPanel.d.ts +0 -104
  220. package/dist/mcpPanel.js +0 -49
  221. package/dist/mcpReconnect.d.ts +0 -58
  222. package/dist/mcpReconnect.js +1 -69
  223. package/dist/mcpWireCaps.d.ts +0 -55
  224. package/dist/mcpWireCaps.js +1 -13
  225. package/dist/memoryComplianceCapability.d.ts +3 -57
  226. package/dist/memoryComplianceCapability.js +0 -57
  227. package/dist/memoryEntriesWire.d.ts +6 -198
  228. package/dist/memoryEntriesWire.js +0 -144
  229. package/dist/memoryOriginCapability.d.ts +3 -53
  230. package/dist/memoryOriginCapability.js +0 -50
  231. package/dist/memorySpecWire.d.ts +0 -116
  232. package/dist/memorySpecWire.js +0 -150
  233. package/dist/model/catalog.d.ts +0 -111
  234. package/dist/model/catalog.js +0 -87
  235. package/dist/model/catalogLoader.d.ts +0 -114
  236. package/dist/model/catalogLoader.js +2 -139
  237. package/dist/model/modelSupplyRules.d.ts +1 -55
  238. package/dist/model/modelSupplyRules.js +0 -62
  239. package/dist/model/providerAuth.d.ts +0 -103
  240. package/dist/model/providerAuth.js +2 -38
  241. package/dist/model/providerCatalog.d.ts +0 -45
  242. package/dist/model/providerCatalog.js +0 -37
  243. package/dist/model/providerPresets.d.ts +0 -33
  244. package/dist/model/providerPresets.js +2 -91
  245. package/dist/model/tierVocabulary.d.ts +0 -31
  246. package/dist/model/tierVocabulary.js +0 -27
  247. package/dist/modelBudgetRule.d.ts +0 -39
  248. package/dist/modelBudgetRule.js +0 -39
  249. package/dist/modelCapabilityProbe.d.ts +0 -175
  250. package/dist/modelCapabilityProbe.js +0 -147
  251. package/dist/modelWireCaps.d.ts +0 -13
  252. package/dist/modelWireCaps.js +0 -13
  253. package/dist/notifications.d.ts +0 -233
  254. package/dist/notifications.js +14 -492
  255. package/dist/oneShotWireCaps.d.ts +0 -34
  256. package/dist/oneShotWireCaps.js +0 -35
  257. package/dist/ownKey.d.ts +0 -33
  258. package/dist/ownKey.js +0 -33
  259. package/dist/panelRunningHistory.d.ts +0 -28
  260. package/dist/panelRunningHistory.js +0 -43
  261. package/dist/peerFrames.d.ts +0 -71
  262. package/dist/peerFrames.js +0 -168
  263. package/dist/peerLaneCapability.d.ts +0 -44
  264. package/dist/peerLaneCapability.js +0 -52
  265. package/dist/permissionRuleIssue.d.ts +0 -30
  266. package/dist/permissionRuleIssue.js +0 -78
  267. package/dist/permissionRulesWriteCapability.d.ts +0 -46
  268. package/dist/permissionRulesWriteCapability.js +0 -54
  269. package/dist/permissionWireCaps.d.ts +0 -37
  270. package/dist/permissionWireCaps.js +0 -37
  271. package/dist/postureKnob.d.ts +0 -71
  272. package/dist/postureKnob.js +0 -86
  273. package/dist/principalWire.d.ts +0 -17
  274. package/dist/principalWire.js +0 -17
  275. package/dist/printToolResultFrame.d.ts +0 -100
  276. package/dist/printToolResultFrame.js +0 -33
  277. package/dist/promptProfileWireCaps.d.ts +0 -13
  278. package/dist/promptProfileWireCaps.js +0 -13
  279. package/dist/readFacePosture.d.ts +0 -43
  280. package/dist/readFacePosture.js +0 -46
  281. package/dist/request/printNotification.d.ts +0 -20
  282. package/dist/request/printNotification.js +0 -55
  283. package/dist/request/taskRequest.d.ts +0 -249
  284. package/dist/request/taskRequest.js +10 -496
  285. package/dist/resumeRefusalCopy.d.ts +0 -136
  286. package/dist/resumeRefusalCopy.js +1 -117
  287. package/dist/retainBackgroundWireCaps.d.ts +0 -48
  288. package/dist/retainBackgroundWireCaps.js +0 -48
  289. package/dist/retryStatus.d.ts +2 -221
  290. package/dist/retryStatus.js +0 -107
  291. package/dist/rewindWireCaps.d.ts +0 -27
  292. package/dist/rewindWireCaps.js +0 -24
  293. package/dist/runCancelContext.d.ts +1 -20
  294. package/dist/runCancelContext.js +0 -34
  295. package/dist/runTerminal.d.ts +0 -273
  296. package/dist/runTerminal.js +0 -177
  297. package/dist/sandboxWire.d.ts +0 -38
  298. package/dist/sandboxWire.js +0 -82
  299. package/dist/scenarioWire.d.ts +0 -60
  300. package/dist/scenarioWire.js +0 -68
  301. package/dist/scratchpadWireCaps.d.ts +0 -11
  302. package/dist/scratchpadWireCaps.js +0 -32
  303. package/dist/sdkWireTransit.d.ts +0 -38
  304. package/dist/sdkWireTransit.js +0 -30
  305. package/dist/seam.d.ts +18 -853
  306. package/dist/seam.js +0 -52
  307. package/dist/seatContract.d.ts +0 -555
  308. package/dist/seatContract.js +5 -304
  309. package/dist/selfOrchestrationDenial.d.ts +0 -189
  310. package/dist/selfOrchestrationDenial.js +0 -162
  311. package/dist/selfOrchestrationWireCaps.d.ts +0 -38
  312. package/dist/selfOrchestrationWireCaps.js +0 -38
  313. package/dist/sessionMap.d.ts +0 -60
  314. package/dist/sessionMap.js +0 -37
  315. package/dist/sessionMemoryStatus.d.ts +11 -114
  316. package/dist/sessionMemoryStatus.js +0 -112
  317. package/dist/sessionModelLatch.d.ts +0 -29
  318. package/dist/sessionModelLatch.js +0 -40
  319. package/dist/sessionPolicyCapability.d.ts +3 -53
  320. package/dist/sessionPolicyCapability.js +0 -52
  321. package/dist/sessionSlot.d.ts +0 -26
  322. package/dist/sessionSlot.js +0 -17
  323. package/dist/skillsWireCaps.d.ts +0 -87
  324. package/dist/skillsWireCaps.js +0 -38
  325. package/dist/sqlEngineCapability.d.ts +0 -118
  326. package/dist/sqlEngineCapability.js +0 -134
  327. package/dist/sseIdleTriage.d.ts +0 -79
  328. package/dist/sseIdleTriage.js +1 -80
  329. package/dist/steering.d.ts +0 -54
  330. package/dist/steering.js +0 -63
  331. package/dist/subagent/engineCompactWire.d.ts +0 -34
  332. package/dist/subagent/engineCompactWire.js +4 -134
  333. package/dist/subagent/engineDelegatedPrompt.d.ts +0 -31
  334. package/dist/subagent/engineDelegatedPrompt.js +1 -103
  335. package/dist/subagent/engineRowStopGate.d.ts +0 -20
  336. package/dist/subagent/engineRowStopGate.js +0 -49
  337. package/dist/subagent/engineSubagentOutput.d.ts +0 -7
  338. package/dist/subagent/engineSubagentOutput.js +1 -51
  339. package/dist/subagent/engineSubagentResume.d.ts +2 -279
  340. package/dist/subagent/engineSubagentResume.js +2 -129
  341. package/dist/subagent/engineSubagentSteer.d.ts +0 -26
  342. package/dist/subagent/engineSubagentSteer.js +0 -63
  343. package/dist/subagent/engineSubagentTail.d.ts +0 -38
  344. package/dist/subagent/engineSubagentTail.js +2 -110
  345. package/dist/subagent/engineTaskHandleWire.d.ts +11 -65
  346. package/dist/subagent/engineTaskHandleWire.js +20 -131
  347. package/dist/subagent/subagentOwnerAbsence.d.ts +0 -2
  348. package/dist/subagent/subagentOwnerAbsence.js +0 -25
  349. package/dist/subagentContentStore.d.ts +3 -260
  350. package/dist/subagentContentStore.js +7 -540
  351. package/dist/systemReminderTag.d.ts +0 -49
  352. package/dist/systemReminderTag.js +0 -61
  353. package/dist/toolResult.d.ts +1 -211
  354. package/dist/toolResult.js +1 -358
  355. package/dist/toolRoster.d.ts +0 -140
  356. package/dist/toolRoster.js +0 -66
  357. package/dist/typePins.d.ts +0 -46
  358. package/dist/types/engineState.d.ts +0 -71
  359. package/dist/types/engineState.js +0 -16
  360. package/dist/ultracodeWireCaps.d.ts +0 -75
  361. package/dist/ultracodeWireCaps.js +0 -92
  362. package/dist/unrefTimer.d.ts +0 -27
  363. package/dist/webSearchBackendCapability.d.ts +0 -72
  364. package/dist/webSearchBackendCapability.js +0 -79
  365. package/dist/webSearchWireCaps.d.ts +0 -37
  366. package/dist/webSearchWireCaps.js +0 -34
  367. package/dist/websearch/searchProviderPresets.d.ts +0 -81
  368. package/dist/websearch/searchProviderPresets.js +0 -27
  369. package/dist/wireErrorTriage.d.ts +4 -160
  370. package/dist/wireErrorTriage.js +0 -171
  371. package/dist/wireRefusalCopy.d.ts +0 -28
  372. package/dist/wireRefusalCopy.js +0 -28
  373. package/dist/workflow.d.ts +0 -40
  374. package/dist/workflow.js +0 -52
  375. package/dist/workflowClient.d.ts +0 -122
  376. package/dist/workflowClient.js +8 -360
  377. package/dist/workflowMonitor.d.ts +2 -72
  378. package/dist/workflowMonitor.js +0 -26
  379. package/dist/writeProtectionCapability.d.ts +0 -112
  380. package/dist/writeProtectionCapability.js +0 -109
  381. package/docs/INTEGRATION-CLIENTS.md +62 -1
  382. package/package.json +1 -1
@@ -1,120 +1,13 @@
1
- /**
2
- * ⇄ B7 批搬迁(2026-07-27,设计稿 §3 B7):cli `src/sema/liveToolApprovalWire.ts`(508 行)——
3
- * **拆**(设计稿原文:「liveToolApprovalWire.ts | 509 | …… | **拆分**,B7;工具 schema 属
4
- * agent-types 域」)。
5
- *
6
- * ── 拆缝在哪 ────────────────────────────────────────────────────────────────────────────────
7
- * **进包(本文件)**:gate 判定(`isFsApprovalGate` / `toolNameIsFsWrite`)· 结构等值
8
- * (`structurallyEqual`,ctrl+g amend 的「真编辑过」判据)· 同步帧契约与判别式
9
- * (`ToolApprovalFrame` / `isToolApprovalFrame` / `isFromSubagent`)· 徽章合成
10
- * (`subagentBadgeFor`)· gate 文案兜 path(`pathFromGateMessage`)· **两条决断腿的编排**
11
- * (`surfaceFsApprovalAndDecide` durable-park 腿 / `surfaceToolApprovalFrameAndRespond` 同步帧腿)。
12
- * **留宿主**:**卡本身**(`surfaceApprovalCard`)—— 它要 leader `ToolUseConfirm` 队列、壳的
13
- * vendored `Tool` 注册表(`FileWriteTool`/`FileEditTool`/`NotebookEditTool`/`EngineWorkflowTool`)、
14
- * `applyPermissionUpdates` 与 `AppState.toolPermissionContext`。那是**端的渲染与权限态**,
15
- * web/桌面各有各的形(浏览器根本没有 Ink 三选卡)。落点 = 下面的 `ApprovalCardPort`。
16
- *
17
- * 🔴 **拆缝的失效形**([paired-mechanisms-must-share-premise]):卡口没装 ⇒ 两条腿都拿不到人的
18
- * 决断。**这不是「静默不启用」可以接受的面** —— 同步帧腿上引擎正同步阻塞着,不 respond 就干等
19
- * TTL;durable 腿上 run 卡在 suspended。所以本口:
20
- * · **计 miss**(`approvalCardPortMisses()`,宿主自检恒应为 0);
21
- * · 缺席时返回 `{kind:'failed', reason:'no approval card port installed …'}` —— 与壳原文
22
- * 「no leader ToolUseConfirm queue registered(print/non-REPL mode)」**同一条 fail-soft 语义**
23
- * (print/headless 本来就没有卡面),调用方照旧走 fail-closed deny / 诚实红。
24
- *
25
- * 🔴 搬迁差分(逐条,零行为变化):
26
- * 1. `toolForName`(4 个 `require('../tools/…')`)整条留宿主 —— 工具注册表是壳资产。
27
- * 2. `getLeaderToolUseConfirmQueue` / `getLeaderSetToolPermissionContext` / `applyPermissionUpdates` /
28
- * `getAppStateStoreRef` / `syntheticAssistantMessage` / `stubToolUseContext` 全部随卡留宿主。
29
- * 3. `logForDebugging` → `hostLog('debug'|'error', …)`(本包零 `utils/debug`,portability 门盯着)。
30
- * 4. `require('./fleetClient.js').readEngineActiveBgTasks()` → 静态 import 包内
31
- * `fleet/fleetLedger.js` 的同名读口(B6 已搬入;延迟 require 的理由「纯 wire 模块不背 fleet 图
32
- * 冷启动」在包内不成立 —— 同一个包)。⚠️ try/catch 保留:台账不可用时兜 taskId 短形那一支
33
- * 仍要成立。
34
- *
35
- * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
36
- *
37
- * liveToolApprovalWire — 权限三选卡的壳侧半场([816] 壳侧承诺① / [819]② / [820] 四模式表,2026-07-14)。
38
- *
39
- * WIRE(server 1.191 已到货,[830]①):引擎对 fs 写工具(Write/Edit/NotebookEdit)出 ask ——
40
- * 真腿=同步 `tool_approval` 帧(named SSE,redactDeep+16KiB 帽)→ 三选卡 → POST
41
- * /v1/tool-approvals/:id/respond {allow|allow_session|deny}(本文件下半)。durable park 腿
42
- * (checkpointGate → approvals.decide)🔴 **现势(REF-CC-023/031,2026-08-02)**:不是「旧引擎兜底」——
43
- * [2150] S1 半场(83-87 行)证实产品默认 durable 投递下,引擎对 Bash 出的 durable park 就是**活路径**
44
- * (r1 E5 fail-stuck 实例);synthApprovalProbe 合成单测仍在,但这条腿今天会被真用户真流量走到。
45
- * 本模块把 gate 面翻成壳的 VENDORED CC 权限对话框(PermissionRequest → FileWritePermissionRequest,
46
- * 三选文案逐字 CC 207:`Yes` / `Yes, allow all edits during this session (shift+tab)`[cwd 外变体带
47
- * dirname] / `No`,题干 `Do you want to create <basename>?` verb=create/overwrite/write to)——零新 UI,
48
- * 键行为(1/2/3、shift+tab 聚焦第 2 项、esc=reject)全部继承 vendor 组件。
49
- *
50
- * 决断腿:
51
- * - Yes(accept-once)→ HitlBridge.decideTool({decision:'approve'})(D-1 绑定 verbatim,合 [819]②)。
52
- * - Yes-allow-all(accept-session)→ 同 approve,且 permissionUpdates 里 vendor 产出的
53
- * `{type:'setMode', mode:'acceptEdits', destination:'session'}` 经 leader setter 落壳 AppState →
54
- * seamQuery 的 permissionModeField 下一 turn 起 stamp `permissionMode:"acceptEdits"`([816] 壳侧
55
- * 承诺②:accept-session=升 intent,wire 已通)。cwd 外变体还带 addDirectories(session)——照 CC 收。
56
- * - No(reject)→ decideTool({decision:'deny'});调用方(liveHitlAskWire)对该 call 的重放 tool_end
57
- * stamp REJECT_MESSAGE,vendor 的 renderToolUseRejectedMessage 渲 `User rejected <op> to <path>`
58
- * (subtle 色,CC 逐字)。
59
- *
60
- * 🔴 fail-soft 铁律(与 ask 桥同款):leader 队列未注册(print/非 REPL)、pending 蒸发、decide 409/404
61
- * 都返回 failed —— 调用方回退「flush + 原样终帧」的诚实红,绝不更糟。
62
- * 🔴 UNTRUSTED:gate args 为模型作文(service 已 redact),只渲染绝不回喂;决断只带 decision 枚举。
63
- *
64
- * ── #144 半场(client-core 0.28.0)────────────────────────────────────────────────────────────
65
- * ① **透传** `persistedRuleShadowed`(server ≥7.13.0,来源锚 = engine fixture
66
- * `@sema-agent/server/dist/tool-approval.d.ts` 同名键;ADDITIVE、`tool_approval` only)——
67
- * 帧 → {@link ApprovalCardRequest.persistedRuleShadowed},壳据此渲「你的规则仍在,只是这次被
68
- * 要求逐次确认」。🔴 它是 **display 值,不是身份通道**:脱敏后的展示文本,不保证与规则店里那条
69
- * 规则逐字节相同、更不是它的 id;下游「删除/撤销规则」面拿它当身份用 = 删错或删不掉。
70
- * ② **deny 归因通道**({@link ApprovalCardDenyDecision.reason}):两腿不同形 ——
71
- * durable `/decide` **有** reason 位 ⇒ 原文上 wire(包内按 server 4096 上限截断);
72
- * live 帧腿 `/v1/tool-approvals/:id/respond`:✅ **#229 已兑现**(0.29.0 respond-note 批,
73
- * server ≥7.15.0)—— 端点收 **`note`**(不是 reason;与 durable `AskDecisionBody.note`
74
- * 同词同源同一列 `decision_note`),deny reason 经 {@link RespondToolApprovalOpts.note} 上 wire。
75
- * 发送前置两道门:能力位 `approvalDecisionNote`(SDK 6.16 成文「位缺席就别发」)+ 2048 上限
76
- * (server 超限响亮 400 且**连决断一起拒**,所以超限 ⇒ 整条不发 + 留痕,诚实缺席优先于
77
- * 静默截断);真落行与否看 ack 的 `noteRecorded`(缺席 ≠ false)。
78
- * 🔴 方向纪律:reason/note 只做归因,绝不参与裁决;缺席 ⇒ 现状字节不变。
79
- */
80
1
  import { DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
81
- // B-042(0.60.0):取件身份闸与**行路由同源**的单一谓词。见 `surfaceFsApprovalAndDecide` 里的调用点注。
82
- // ⚠️ 这条 import 与 `frameRouter → toolApprovalWire` 构成一个 ESM 环(askParkRowRouting → frameRouter
83
- // → 本文件)。环安全的判据不是「跑起来没炸」而是**结构**:三个模块在环上的顶层求值都只铸函数声明
84
- // 与字面量常量,没有任何一处在**模块体**里读另一模块的绑定(读全发生在函数体内,那时环早已闭合)。
85
- // 单例门与 portability 门各自盯着这条性质的一半;新增顶层 `const X = <另一模块的绑定>` 会破坏它。
86
2
  import { askParkRowArm } from './askParkRowRouting.js';
87
3
  import { hostLog } from '../host.js';
88
4
  import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
89
5
  import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
90
6
  import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded, surfaceEditRefusedOnBlindAsk, surfaceRuleArmNotSent, surfaceRuleArmRejected } from './hitlHostSurface.js';
91
7
  import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
92
- // 0.67.0(core 7.14.0 #688 C3):`ruleStoreUnreadable` 的**闭二词判据**。词表属主 = core
93
- // (`RULE_STORE_UNREADABLE_KINDS`),镜像与措辞都在 `gateVocabulary.ts` 那个唯一铸点上 ——
94
- // 本文件**不抄第二份字面量**(B-025 的根因就是手抄)。
95
8
  import { isRuleStoreUnreadableKind } from '../gateVocabulary.js';
96
- // 🔴 0.67.0:`ClassifierUnavailableView` 的 type-only import 随卡面那一格**整只退役**(core 7.14.0
97
- // 把 `classifierUnavailable` 从 ask / park / carry / summary 六个载体上删掉,继任者是拒绝面的
98
- // `GateDisposition.denied.cause`)。事实位今天由 `gateOutcome.ts` 的 `disposition.cause` 与公面读器
99
- // `classifierDenyCauseOf` 承载,卡上不再有它的对偶 —— 见 `autoModeUnavailable.ts` 顶注。
100
- // B-070 / L-200(0.65.0):`/decide` 200 回执的**单一读面**(`handoffTaskId` 换挂句柄 +
101
- // `executionOutcome` 缺席=未知)。判定归包,端零自拼。
102
9
  import { readDecideReceipt } from '../decideReceipt.js';
103
- // ── sdk 8.2.0 单源闭词表(S-134 锚②;0.58.0 提货批 / B-025 修根)──────────────────────────────
104
- // 🔴 **值级 import,不是 type-only**:三张 `as const` 数组是**运行期**判据(窄读器按它判 kind /
105
- // match 是不是闭集成员)。本包此前把 `match` 的二词闭集**手抄**在读器里,而 core design/382 /
106
- // #510 把词表扩到四员、batch 成员加了 `directoryRead` 臂 —— 手抄的那份没跟,合法候选被整条/
107
- // 整只丢掉(B-025)。单源化之后「词表加员」这件事在**上游一处**发生,本包跟着走。
108
- // 🔴 值级 import 的代价已记账:peer 地板随本批抬到 `>=8.2.0`(run-sdk-floor-test.mjs 的 ①c 把
109
- // 声明与门常量逐位绑死),包总入口闭包的外部包集合仍恒等于 `{diff, @sema-agent/sdk}`(singleton 门)。
110
- // ── sdk 8.3.0 单源闭词表(S-125③ / S-114 / S-15 第五单;0.59.0 提货批)────────────────────────
111
- // 同上一条纪律的第二例:`ruleOffersAbsence` 的三词、`denialLimitFallback.limit` 的二词都是
112
- // **上游属主**的闭集。本包**不拿它们做窄读判定**(两处都按开集透传,见各自帧键 JSDoc),再导出
113
- // 的用途是让三端写 `switch` 时有一份**与包同一个数组对象**的词表可数,而不是各自抄字面量 ——
114
- // B-025 的根因正是手抄。⇒ 值级 import 让 peer 地板在编译期真被钉到 8.3.0。
115
10
  import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS, } from '@sema-agent/sdk';
116
- /** fs 写权限 gate 判定:未来的一等 kind(tool_approval)或按 toolName(server 桥首批=fs 写三件,
117
- * [820] 表)。AskUserQuestion 永不进这里(ask 桥先判)。 */
118
11
  export function isFsApprovalGate(gate) {
119
12
  if (!gate)
120
13
  return false;
@@ -126,22 +19,14 @@ export function toolNameIsFsWrite(name) {
126
19
  const n = name.replace(/[\s_-]+/g, '').toLowerCase();
127
20
  return (n === 'write' || n === 'filewrite' || n === 'edit' || n === 'fileedit' || n === 'multiedit' || n === 'notebookedit');
128
21
  }
129
- /** shell 执行工具判定(Bash)。[2150] S1 半场:引擎对 Bash 发 durable park 时,fs 谓词不认 ⇒
130
- * park 落「其余 gate 透传」= run 留 suspended、session 锁死(r1 E5 fail-stuck 实例)。 */
131
22
  export function toolNameIsShellExec(name) {
132
23
  return name.replace(/[\s_-]+/g, '').toLowerCase() === 'bash';
133
24
  }
134
- /** 工具审批 gate 判定(durable park 分诊用的**超集**谓词):一等 kind / fs 写三件 / shell 执行。
135
- * decide 腿(approvals.decide)对两族同构;卡形分发归壳 ApprovalCardPort(toolForName 已含
136
- * bash → BashPermissionRequest 映射,sema-cli a35e2b2)。`isFsApprovalGate` 保留原语义不动
137
- * (公开面消费者按 fs 写理解它,把 Bash 塞进去是撒谎)。 */
138
25
  export function isToolApprovalGate(gate) {
139
26
  if (isFsApprovalGate(gate))
140
27
  return true;
141
28
  return typeof gate?.toolName === 'string' && toolNameIsShellExec(gate.toolName);
142
29
  }
143
- /** 结构等值(键序无关深比较)——updatedInput「真编辑过」判定用。zod parse 会产新引用与重排,
144
- * 引用比较/JSON.stringify 串比较都会假报「编辑过」。 */
145
30
  export function structurallyEqual(a, b) {
146
31
  if (a === b)
147
32
  return true;
@@ -162,35 +47,17 @@ export function structurallyEqual(a, b) {
162
47
  return false;
163
48
  return ka.every(k => structurallyEqual(a[k], b[k]));
164
49
  }
165
- /**
166
- * 三张闭词表的**运行期**再导出(sdk 8.2.0 `as const` 单源)。
167
- *
168
- * 🔴 **端拿它做判定,别再手抄字面量** —— 本包自己的窄读器就读这两张表(B-025 的根因正是手抄);
169
- * 再导出让三端与包**共用同一份数组对象**,词表加员时一处改、四处跟。
170
- */
171
50
  export { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS };
172
- /**
173
- * 两张新闭词表的**运行期**再导出(sdk 8.3.0 `as const` 单源;与上面三张同一条纪律)。
174
- *
175
- * ⚠️ **本包自己不拿它们做窄读判定**,这与 `RULE_OFFER_MATCHES` 一族刻意不同,理由写在两处帧键的
176
- * JSDoc 里:`origin` 与 `ruleOffersAbsence` 的词表属主是 core、server 侧已按闭集拒过词表外的值,
177
- * 包再校一遍只会在 core 加员当天把一个**合法**值判没(`wiring_manifest.autoMode.reason` 的
178
- * 透传纪律逐字同规)。再导出是给端的 `switch` 一份可数的表 —— 端仍必须带 `default` 臂。
179
- */
180
51
  export { RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS };
181
- /** 卡口「无决断收口」结局值(单源;见 {@link ApprovalCardDecision})。 */
182
52
  export const RETRACTED_CARD_DECISION_KIND = 'retracted';
183
- // W1(design/161):sessionKey → 注册表;零参 API = DEFAULT_SESSION_KEY 兼容层(cli 装配不动)。
184
53
  const cardPortByKey = createSessionSlot();
185
54
  const cardPortMissesByKey = new Map();
186
55
  function bumpCardPortMiss(sessionKey) {
187
56
  cardPortMissesByKey.set(sessionKey, (cardPortMissesByKey.get(sessionKey) ?? 0) + 1);
188
57
  }
189
- /** 装卡口(传 null 卸)。返回还原函数。 */
190
58
  export function installApprovalCardPort(port) {
191
59
  return installApprovalCardPortFor(DEFAULT_SESSION_KEY, port);
192
60
  }
193
- /** W1 带 key 变体:装/卸某会话的卡口(多会话宿主每会话一键,互不顶盖)。 */
194
61
  export function installApprovalCardPortFor(sessionKey, port) {
195
62
  const prev = cardPortByKey.get(sessionKey);
196
63
  cardPortByKey.set(sessionKey, port);
@@ -198,38 +65,25 @@ export function installApprovalCardPortFor(sessionKey, port) {
198
65
  cardPortByKey.set(sessionKey, prev);
199
66
  };
200
67
  }
201
- /** 🔴 宿主自检:恒应为 0。非 0 = 有 gate 到达时卡口不在,那次审批走了 fail-closed 路径。 */
202
68
  export function approvalCardPortMisses() {
203
69
  return approvalCardPortMissesFor(DEFAULT_SESSION_KEY);
204
70
  }
205
- /** W1 带 key 变体(miss 计数逐键)。 */
206
71
  export function approvalCardPortMissesFor(sessionKey) {
207
72
  return cardPortMissesByKey.get(sessionKey) ?? 0;
208
73
  }
209
- /** 卡口在不在(壳原文 `getLeaderToolUseConfirmQueue()` 那个存在性判据的等价物)。 */
210
74
  export function hasApprovalCardPort() {
211
75
  return hasApprovalCardPortFor(DEFAULT_SESSION_KEY);
212
76
  }
213
- /** W1 带 key 变体。 */
214
77
  export function hasApprovalCardPortFor(sessionKey) {
215
78
  return approvalCardPortFor(sessionKey) !== null;
216
79
  }
217
- /** W1 读端:某会话的卡口(未装 = null;**不计 miss** —— miss 只在决断腿真到达时计)。 */
218
80
  export function approvalCardPortFor(sessionKey) {
219
81
  return cardPortByKey.get(sessionKey) ?? null;
220
82
  }
221
- /** 测试钩:卸口 + 清计数(只卸口不清计数会让「未装配时 misses 为 0」的断言恒假 —— host.ts 同款教训)。 */
222
83
  export function _resetApprovalCardPortForTest() {
223
84
  cardPortByKey.clear();
224
85
  cardPortMissesByKey.clear();
225
86
  }
226
- /**
227
- * 共享卡面:弹 vendored CC 三选卡(PermissionRequest)→ 等用户决断。两条腿共用:
228
- * - 旧 durable park 腿(surfaceFsApprovalAndDecide → HitlBridge.decideTool)
229
- * - 新同步帧腿(server 1.191 `tool_approval` 帧 → POST /v1/tool-approvals/:id/respond)
230
- * 决断落地(decide/respond)由调用方做;卡的渲染/键行为/本地 permissionUpdates
231
- * (accept-session 升 intent,[816] 壳侧承诺② —— 与决断通道无关,两腿都要)由**宿主卡口**做。
232
- */
233
87
  export function surfaceApprovalCard(req) {
234
88
  const cardPort = approvalCardPortFor(DEFAULT_SESSION_KEY);
235
89
  if (cardPort === null) {
@@ -238,20 +92,7 @@ export function surfaceApprovalCard(req) {
238
92
  }
239
93
  return cardPort(req);
240
94
  }
241
- /**
242
- * REF-CC-030(2026-08-02):三个决断入口(`surfaceApprovalCard` 本身 / `surfaceFsApprovalAndDecide` /
243
- * `surfaceToolApprovalFrameAndRespond`,后两者都经 `surfaceApprovalCard`)今天全硬读
244
- * DEFAULT_SESSION_KEY——keyed 卡口 API(`installApprovalCardPortFor` 等)建好了但没有任何调用点
245
- * 带非默认键透传。多会话宿主如果照 W1 文档按会话键装口,会 100% miss 在默认键上,而它那边
246
- * 「我装了啊」——原文案 `host forgot installApprovalCardPort` 会把这个键不匹配误导成没装。
247
- * 这里在 miss 发生时顺手核一眼「是不是有别的键装了口」,点名两种不同的失效,而不需要先把三个
248
- * 决断入口的签名都改成 `sessionKey?: string` 透传(那是更大的一次改动,候 W1 消费者真正到位)。
249
- */
250
95
  function cardPortMissReason() {
251
- // 🔴 判「装了没」必须读**值**,不能只读键:`installApprovalCardPortFor(key, null)` 是文档化的
252
- // 卸口姿势(签名 `ApprovalCardPort | null`),而 SessionSlot.set 只对 `undefined` 删键 —— 卸完
253
- // 键还在、值为 null。只看 `keys()` 会把「卸干净了、谁都没装」误诊成「装在别的键上」,恰好把这张
254
- // 卡要分开说的两种失效说反。
255
96
  const otherKeysInstalled = cardPortByKey
256
97
  .keys()
257
98
  .some((k) => k !== DEFAULT_SESSION_KEY && cardPortByKey.get(k) != null);
@@ -260,63 +101,12 @@ function cardPortMissReason() {
260
101
  }
261
102
  return 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)';
262
103
  }
263
- // deny 归因窄化(0.28.0 发版扫描 F1 收编):单源下沉 hitlBridge —— 三条 reason 出口(卡 deny /
264
- // canUseTool deny / plan-review)共用一把,坐标与理由见 hitlBridge.ts 的导出处顶注。
265
- /**
266
- * 弹 CC 三选卡 → 等决断 → 经 HitlBridge decide(durable park 腿,server 桥前的 checkpointGate 面;
267
- * 合成单测/旧引擎兜底保留)。decide 成功即引擎驱到下一状态(sync);调用方随后 attach runs.events
268
- * 续流(与 ask 桥完全同 loop)。
269
- *
270
- * @param argsByCall tool_start.args(toolCallId → args)——题干/diff 的一手源;缺则 pending.input。
271
- */
272
- export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signal,
273
- /**
274
- * 这张 park 的**待批 call 身份**(server ≥7.41.0 的 `done{suspended}.result.toolCallId` /
275
- * durable `suspended` 事件同键;供给见 `frameRouter.parkGatedCallId`,缺席是常态)。
276
- *
277
- * 🔴 它必须从**取件**这一步就起作用,不能只拿去删扣留帧(0.41.0 件④ 的异源复审 finding①):
278
- * 取件按 taskId + 族谓词选**第一行**,同 task 同族多行排队时那可能是旁观者 —— 只用身份删帧就成了
279
- * split-brain(决断绑的是旁观者行、删掉的是主角帧):用户其实在批准/拒绝**另一个**工具调用,
280
- * 而真 park 原地不动、反复重挂。
281
- * 🔴 **不 fail-closed**:身份在场但队列里没有对应行时**照旧回落**既有两条腿(taskId + 族谓词)。
282
- * 这个 join(checkpoint 的 `pendingAction.toolCallId` ↔ `/v1/approvals` 行的 `toolCallId`/
283
- * `boundCallId`)本仓没有对真 server 的实证,而 fail-closed 的失效面是**每一次审批都决断不了**;
284
- * 回落的失效面则与本键出现之前逐字节相同。方向按代价不对称取:宁可退回原状,不赌一个没实证的键。
285
- */
286
- parkGatedCallId) {
287
- // REF-CC-031(2026-08-02):卡口缺席判据不在此前置重复一遍 —— `surfaceApprovalCard`(下方调用)
288
- // 已经计 miss + 出 typed `{kind:'failed', reason:...}`,card.kind==='failed' 走既有汇流分支
289
- // (下面 switch 的 'failed' 臂);两处各写一份文案会在 SEMA_DEBUG 里出现两种措辞,取决于走的是
290
- // 哪条腿。文案单源 = `surfaceApprovalCard` 那一份。
291
- // pending 行 = gate 的权威源 + D-1 绑定 surface(与 ask 桥同姿势)。REF-CC-029(2026-08-02):
292
- // 单源取件 —— 这里取到的行原样传进下面**四条决断腿的每一次** `bridge.decideTool(...)`
293
- // (preResolvedPending 入参:aborted / allow+remember / allow 纯批 / deny),不再让 bridge 自己
294
- // 二次 list(),消掉 hitl2-01/hitl2-08 的 TOCTOU 窗。
295
- // 🔴 常驻判据不是「看得见 pending 这个实参」,是 pure 门那四条「approvals.list 全程恰好 1 次」——
296
- // 漏传一条腿,形状断言全绿而计数当场翻红(回炉车前的实况:只有 aborted 臂传了)。
297
- const found = await findPendingForTask(deps.client, taskId, (toolName) => toolName !== undefined && (toolNameIsFsWrite(toolName) || toolNameIsShellExec(toolName)), signal ? { signal } : undefined, parkGatedCallId, // 见本参数头注:身份在场 ⇒ 逐字命中优先于「同族第一行」;缺席/不命中 ⇒ 原两腿
298
- // ── B-042 修根(0.60.0):身份闸与**行路由同源** ────────────────────────────────────────────
299
- // 病形 = [paired-mechanisms-must-share-premise]:三端把一行**路由**进本臂用的是
300
- // `askParkRowRouting.askParkRowArm`(kind ∈ ask 门族 ∧ 不是 ask 工具 ⇒ `tool-gate`),而这里的
301
- // 身份闸此前是 `isToolApprovalGate`,只有两条腿(一等 `kind === 'tool_approval'` 放行任意
302
- // toolName,或 fs 写 / shell 的**名字腿**)。于是 `gateKind:"irreversible_ask"` +
303
- // 工具名不在 fs 三件 + Bash 里的行(MCP 远端工具 / WebFetch / NotebookEdit / agent 工具 …)
304
- // **路由说「是我的」、取件说「不是」**:身份逐字命中却被族闸判出局 ⇒ `identityConflicts` 收紧
305
- // 候选池 ⇒ 池空 ⇒ `no_pending` ⇒ 卡永远重开不了。内建 fs/Bash 行只是恰好走了名字腿才没中招。
306
- // 🔴 修 = 让这一格读**同一只**谓词:`askParkRowArm(row) === 'tool-gate'`。名字腿**只留给**
307
- // 上面那个 `matches`(它服务的是 leg 2「同族第一行」,那一腿本来就该按工具名挑)。
308
- // 🔴 **不会顺手放宽到外族**:`findPendingForTask` 先用 `askParkForeignGateKind` 把
309
- // plan_review / resource_limit / 未来新词整族滤掉,本闸只在那批**可决行**里再分问答/工具两臂
310
- // —— 两道闸串联的语义与修前逐字相同,变的只是「工具臂认不认得非 fs/Bash 的工具名」。
311
- (row) => askParkRowArm(row) === 'tool-gate');
312
- // #357:取件腿的结构化判别位原样透传(见 `FsApprovalOutcome.code` 头注)——「这一拍队列里没有
313
- // 本 run 的可决行」与「approvals.list 自身炸了」自此对消费点结构可分,不再靠文案子串猜。
104
+ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signal, parkGatedCallId) {
105
+ const found = await findPendingForTask(deps.client, taskId, (toolName) => toolName !== undefined && (toolNameIsFsWrite(toolName) || toolNameIsShellExec(toolName)), signal ? { signal } : undefined, parkGatedCallId, (row) => askParkRowArm(row) === 'tool-gate');
314
106
  if (!found.ok)
315
107
  return { kind: 'failed', stage: 'fetch', reason: found.reason, ...(found.code !== undefined ? { code: found.code } : {}) };
316
108
  const { pending, gatedCallId } = found;
317
109
  const toolName = typeof pending.toolName === 'string' ? pending.toolName : '';
318
- // 工具入参:优先流上 tool_start.args(完整、零额外语义);缺则 pending.input(service 已 redact+限长
319
- // ——大 Write 的 content 可能被截,对话框 diff 退化但仍可决断)。
320
110
  const args = (gatedCallId !== undefined ? argsByCall.get(gatedCallId) : undefined) ??
321
111
  pending.input ??
322
112
  {};
@@ -324,96 +114,31 @@ parkGatedCallId) {
324
114
  return { kind: 'failed', stage: 'input', gatedCallId, reason: 'gate has no tool input payload' };
325
115
  }
326
116
  const bridge = new HitlBridge(deps.client, taskId);
327
- // A-028.3:卡身份键经 gateIdentity 唯一铸口(呈现台账/壳侧重开腿同源,字面不再各铸各的)。
328
117
  const callKey = approvalCallKey(gatedCallId, taskId);
329
- // [C170] 答问②半场(0.29.0):durable 富行的两个展示键随卡透传 —— 修前这里是四位闭集,
330
- // server 7.16.0 起行上就有的 ruleSuggestions/governanceForced 在「行 → 卡」重铸处整段丢失
331
- // (feed 行原样透传零丢失,丢的只有这处)。governanceForced 条件 stamp 只认 === true(缺席
332
- // 纪律与活卡腿同款:缺席=无治理来源证据,绝不写 false);候选走 readRuleOfferSupply 同一把
333
- // 合形窄化,落**只读键**(红线见 ApprovalCardRequest.ruleOffersReadOnly 顶注:/decide
334
- // 无规则位,落可兑付位=假 affordance)。
335
- // #334(0.43.0):行上的活键换成 `ruleOffers`(server ≥7.46.0 `PendingCheckpoint.ruleOffers`);
336
- // SDK 7.2.0 的 `PendingCheckpoint` 声明尚无此键 ⇒ **结构视图读**(与本函数下方 `riskDescriptor.probeCause`
337
- // 同款姿势),旧键 `ruleSuggestions` 保读兼容(7.44 及更旧引擎仍在场),两键经同一把窄读器归一。
338
118
  const ruleOffersReadOnly = readRuleOfferSupply(pending.ruleOffers, pending.ruleSuggestions);
339
- // #280 件1(0.30.4):durable 行腿的探针因由载体 = `riskDescriptor.probeCause`(server 对
340
- // riskDescriptor 整体透传,与活卡帧同值)。SDK 6.17.2 的 riskDescriptor 声明尚无此键 ⇒ 结构
341
- // 视图读(与 caps 防御读同款姿势),类型半场候 SDK 班车;载体形不合 ⇒ 不铸键(内部结构不校,
342
- // 归呈卡端窄读器 —— isWireRecordCarrier 顶注)。
343
- // 🔴 **`ruleEvidence` 在本腿刻意零 stamp**(#281 件4b,0.30.8):sdk 7.1.0 的 `PendingCheckpoint`
344
- // 与 `riskDescriptor` 都**没有**这一位 —— 上游今天只在活卡帧上铸它,耐久路无对偶。凭一个
345
- // 「大概也叫这个名字」的猜测去读 `riskDescriptor.ruleEvidence`,读到的恒是 undefined(白写),
346
- // 而万一上游选了别的载体名/别的语义,那就是拿一个不同的量冒充同一位。⇒ 诚实零投影 + 登记
347
- // (run-durable-card-display-keys-test.mjs 的 NOT_PROJECTED 账),上游补位后按 probeCause 的
348
- // 双源合流形跟批。
349
119
  const durableProbeCause = pending.riskDescriptor?.probeCause;
350
- // ── 0.64.2 件⑥(cli B-058 让位臂的输入;**durable 腿两位对偶**)────────────────────────────
351
- // 病:本处「行 → 卡」重铸此前只投 5 位,而这两位在 `/v1/approvals` 行上**今天就有**:
352
- // · `persistedRuleShadowed` ← 行的 **`riskDescriptor.shadowedRule`**(⚠️ **键名与活卡帧顶层不同**:
353
- // 帧上是顶层 `persistedRuleShadowed`,行上 server 把它放在 riskDescriptor 下并逐字脱敏 ——
354
- // engine 7.69.0 fixture `http/routes/approvals-assistant.js` 的 `redactPendingDisclosures`
355
- // `typeof rd.shadowedRule === "string" ? redactSecrets(rd.shadowedRule) : undefined`,
356
- // 再由 `projectPendingForWire` 随行上 wire。名字不同正是它此前被漏掉的原因);
357
- // · `ruleOffersAbsence` ← 行上**同名**键(server 补投那天零改动生效;今天两只 checkpoint 店的
358
- // `listPending` 还没把它扁平化上来 —— 与本文件 `classifierUnavailable` 的 durable 腿同一格记账)。
359
- // 🔴 **为什么是行为面而不只是补一格显示**:这两位是壳「让位臂」的输入 —— 活卡腿早就有,durable 腿
360
- // 因为不投,一条**被记住的 allow 规则清不掉的 ask** 在耐久路上看起来就是一条普通 ask,于是被
361
- // 自动 decide approve(cli B-058 离线真跑:default 车道 + `Bash(grep:*)` + 带 shadowedRule 的
362
- // park 行 ⇒ **零卡 + decide approve**)。包只保证这两位到得了卡口,让位判定归端。
363
- // 🔴 判据与活卡腿逐字同族:**非空串才 stamp**(空串是坏值不是「空规则」),词表**不校**(开集),
364
- // 坏形/缺席 ⇒ 键不 stamp(**绝不折成空串**:一格空白的规则解释比没有解释更坏)。
365
- // 🔴 stamp 的是**原文字节**(不 trim、不截、不改写),与活卡腿同族口径 —— 展示值一改写就不再是
366
- // 「你那条规则」的原文;长度夹取归端。UNTRUSTED-for-display,且**不是身份通道**
367
- // (见 {@link ApprovalCardRequest.persistedRuleShadowed} 的同名警示)。
368
120
  const durableShadowedRule = pending.riskDescriptor?.shadowedRule;
369
121
  const durableRuleOffersAbsence = pending.ruleOffersAbsence;
370
- // 0.67.0(core 7.14.0 #688 C3):durable 腿的「规则店读不出来」孪生位 —— 载体 = 行上的
371
- // `ruleStoreUnreadable`(core `checkpoint-store.d.ts:818` 写在 `PendingAction.tool_approval` 上,
372
- // 摘要行 `:1924-1926` 同名;行是 `pendingAction` 的**扁平投影**,`ruleOffers`/`hasBidiControls`
373
- // 都是同一条路落到顶层的)。sdk 8.8.0 的 `PendingCheckpoint` 尚无本键声明 ⇒ **结构视图读**
374
- // (与同函数 `ruleOffers` / `riskDescriptor.probeCause` 同款姿势),类型半场候 sdk 班车。
375
- // 🔴 **与 `ruleEvidence` 的「刻意零 stamp」不同裁,理由要说清**:那一位在上游**根本没有耐久对偶**,
376
- // 读 `riskDescriptor.ruleEvidence` 是在猜一个载体名(猜对了是白写,猜错了是拿另一个量冒充);
377
- // 本位的对偶**是上游逐字声明的**(core 摘要行的顶注逐字:「The row's
378
- // `PendingAction.tool_approval.ruleStoreUnreadable` (#688 C3), echoed ONLY when the stored word is
379
- // a」member),键名同形、语义同源 —— 不是猜。
380
- // ⚠️ **今天的供给缺口,如实记(不是「接通了」)**:本腿吃的是 `/v1/approvals` 行,而两只 checkpoint
381
- // 店的 `listPending` 把 `pendingAction` 扁平化时带不带这一位由 server 决定;本包这一侧零改动
382
- // 随 server 补投生效 —— 与上一代 `classifierUnavailable` 的 durable 腿同一格记账。
383
122
  const durableRuleStoreUnreadable = readRuleStoreUnreadable(pending.ruleStoreUnreadable);
384
- deps.onPresented?.(); // L-80:真要交给卡口了才算呈过(规则直决 / 取件失败都不走到这一行)
123
+ deps.onPresented?.();
385
124
  const card = await surfaceApprovalCard({
386
125
  toolName,
387
126
  args,
388
127
  callKey,
389
128
  ...(signal ? { signal } : {}),
390
129
  ...(pending.governanceForced === true ? { governanceForced: true } : {}),
391
- // S-30①(0.48.0):durable 行的 bidi 披露位随卡透传。**同一个病形的第 N 例** —— 这处「行 → 卡」
392
- // 重铸是闭形的,server ≥7.53 起行上就有的这一位在这里整键丢失([C170] 那次 ruleSuggestions /
393
- // governanceForced 逐字同一个坐标)。本批由 durable-card 门 ⑦ 段的**富行键集普查**当天抓出
394
- // (红文逐字:「无账:hasBidiControls」),不是人眼发现的。
395
- // 🔴 条件 stamp **只认严格 true**,与 governanceForced / inputHasBidi 同族:上游契约是
396
- // 「`true` 或缺席,恒不写 false/null」,这里认宽了就会把一个 falsy 值渲成披露。
397
130
  ...(pending.hasBidiControls === true ? { hasBidiControls: true } : {}),
398
131
  ...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
399
132
  ...(isWireRecordCarrier(durableProbeCause) ? { probeCause: durableProbeCause } : {}),
400
- // 0.67.0:与活卡腿**同一把窄读器、同一个卡位**(键路同形 ⇒ 端一把读器吃两条腿)。
401
- // 缺席 ⇒ 键不 stamp;判据与供给缺口见上面那段头注。
402
133
  ...(durableRuleStoreUnreadable !== undefined
403
134
  ? { ruleStoreUnreadable: durableRuleStoreUnreadable }
404
135
  : {}),
405
- // 件⑥(0.64.2):durable 腿的两位对偶(判据与「为什么是行为面」见上面那段头注)。
406
- // 🔴 落位是**活卡腿同一个卡位**(`persistedRuleShadowed` / `ruleOffersAbsence`),不是新键:
407
- // 两条腿说的是同一件事,端读一个形;载体名不同只是上游两条腿各自的形,不该漏到卡面上。
408
136
  ...(typeof durableShadowedRule === 'string' && durableShadowedRule.trim() !== ''
409
137
  ? { persistedRuleShadowed: durableShadowedRule }
410
138
  : {}),
411
139
  ...(typeof durableRuleOffersAbsence === 'string' && durableRuleOffersAbsence !== ''
412
140
  ? { ruleOffersAbsence: durableRuleOffersAbsence }
413
141
  : {}),
414
- // #348(0.44.0)durable 腿的对偶:行上**本来就存着**这个值(server `parked-decide.ts`;SDK
415
- // `PendingCheckpoint.toolCallId?: string | null` 直证)。与活卡帧腿同一个卡位、同一条缺席纪律 ——
416
- // `null`(SDK 声明的第二种缺席形)与空串一并降键缺席,绝不把 `null` 折成串。
417
142
  ...(typeof pending.toolCallId === 'string' && pending.toolCallId !== ''
418
143
  ? { toolCallId: pending.toolCallId }
419
144
  : {}),
@@ -422,53 +147,20 @@ parkGatedCallId) {
422
147
  case 'failed':
423
148
  return { kind: 'failed', stage: 'card', gatedCallId, reason: card.reason };
424
149
  case 'retracted':
425
- // 0.74.1(CC-67):撤卡不是决断 —— 零 decide,引擎那头的 ask 照旧挂着(由它的 TTL / 别的决断口结算)。
426
150
  hostLog('debug', `liveHitlAskWire: approval card retracted without a decision for ${gatedCallId ?? '(no call)'} — nothing sent`);
427
151
  return { kind: 'retracted', gatedCallId };
428
152
  case 'aborted':
429
- // turn 被中断(Esc/Ctrl+C):把挂着的 ask 用一次 **TOOL 级 deny** 结算掉(server
430
- // `ASSISTANT-WIRE-CONTRACT.md` §4a;0.42.0 撤稿 —— 原文写的是「cancel-by-deny(contract/04
431
- // §2.4)」,两条前提均已作废,逐条见 `parkResolver.ts` 同臂的撤稿段)。REF-CC-023(2026-08-02):
432
- // 有界观察(observeCancelByDeny)取代裸 `.catch(()=>{})` 全吞 —— 与 askGateWire.ts 同款
433
- // reason 文案,同一次事故(件3,中断事故修复批 G,2026-07-15)的两条腿现在共用同一个观察器。
434
153
  observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
435
154
  return { kind: 'aborted', gatedCallId };
436
155
  case 'allow':
437
156
  try {
438
- // accept-session(三选第 2 项)在 durable-park 腿的引擎侧半场:decide 带 remember:"session"
439
- // (server 1.191 approvalExemptionStore.grant)→ 同 session 该工具后续 ask 免 park 免卡
440
- // (frame 腿的 allow_session 对偶)。老 server 不识别 remember ⇒ 400 —— 回退纯 approve
441
- // (本地 acceptEdits intent 已升,下一 turn 起 permissionMode 就位,同 turn 内可能再弹卡=
442
- // 修复前行为,fail-soft 不更糟)。
443
157
  if (card.allowSession) {
444
158
  try {
445
159
  const raw = await bridge.decideTool({ decision: 'approve', remember: 'session', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, signal ? { signal } : undefined, pending);
446
- // B-070:回执**原样读一次**(缺席即缺席);它不是「已解决」的证据,见 `receipt` 位顶注。
447
160
  const receipt = readDecideReceipt(raw);
448
161
  return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
449
162
  }
450
163
  catch (e) {
451
- // 🔴 [2393] hitl-F4(2026-08-02):回退臂**只**兜「老 server 不识别 remember ⇒ 400 未知键」
452
- // 这一形。`HitlSafetyError` 是 hitlBridge 的**安全信号**闭集(hitlBridge.ts:9 / decideTool
453
- // 头注末句逐字:「A binding mismatch (409) is re-raised as a `HitlSafetyError`
454
- // ('binding_mismatch') — the caller re-presents, NEVER auto-retries」),旧的 catch-all
455
- // 把它一并吞了,然后**立刻自动重发**一次纯 approve —— 正是那条铁律禁的动作:
456
- // binding 不匹配意味着「人看见的那一行在他决断期间被换掉了」,自动重试等于替人对一件
457
- // 他没看过的事按了 Yes。`no_pending` 同族(那一行已经没了,重试同样只会再失败一次)。
458
- // 上抛给外层 catch ⇒ typed `failed` ⇒ 调用方走 fail-soft 诚实红,由人重新决断。
459
- // Inkglow-1085 P0a:瞬断耗尽同样**不许**回退纯 approve —— 引擎此刻根本够不着,再补一发
460
- // 纯 approve 只是再烧一轮超时,还把「传输断了」错标成「老 server 不识别 remember」。
461
- // 上抛给外层 catch ⇒ retryExhausted 判别位 ⇒ parkResolver 走重呈臂。
462
- // 🔴 **第三条再抛(#363,0.47.0;异源复审 [high] 采纳,真病)**:拒体带 `currentPending`
463
- // ⇒ 引擎**结构化地告诉我们**「你决的那一行已经不是当前的了,当前是这一条」——
464
- // 那和 `binding_mismatch` 是同一件事的另一种说法(人看见的那一行在他决断期间被换掉了),
465
- // 只是走的是 stale 那条码。修前它落进这条 catch-all,被当成「老 server 不识别 remember」
466
- // **并立刻用人对旧卡给的答案再发一次纯 approve** —— 正是本臂上方那条铁律禁的动作;
467
- // 而且顺带把这枚指路键吞掉(第二发若也失败,外层拍平拿到的是**第二个**错误,首发的
468
- // `currentPending` 从此不存在)。⇒ 上抛,由外层拍平臂原样带上指路键,人重新决断。
469
- // ⚠️ 边界如实说:本条只收「引擎给了指路键」这一格;「回退臂是不是应该收窄成**只**兜
470
- // 400 未知键」是**本批之前就在的**分类问题(见 §7b 登记),不在本批射程内 ——
471
- // 在一个 additive 批里悄悄改掉老引擎兼容腿的宽度,是另一种越界。
472
164
  if (e instanceof HitlSafetyError || e instanceof DecideTransportRetryExhaustedError)
473
165
  throw e;
474
166
  if (readDecideCurrentPending(e) !== undefined)
@@ -481,9 +173,7 @@ parkGatedCallId) {
481
173
  return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
482
174
  }
483
175
  catch (e) {
484
- // #363:S-02 指路键原样透传(读不出 ⇒ 键不铸,形状逐字节不变)。见 FsApprovalOutcome 同名位。
485
176
  const currentPending = readDecideCurrentPending(e);
486
- // 0.60.0(候包件 ②):引擎机器码原样透传(单源 `readWireErrorCode`;读不出 ⇒ 键不铸)。
487
177
  const wireCode = readWireErrorCode(e);
488
178
  const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
489
179
  return {
@@ -491,7 +181,6 @@ parkGatedCallId) {
491
181
  stage: 'decide',
492
182
  gatedCallId,
493
183
  reason: `decide(approve) failed: ${String(e)}`,
494
- // Inkglow-1085 P0a:瞬断耗尽的判别位(在场即真)—— 语义失败缺席,reason 字节不变。
495
184
  ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
496
185
  ...(currentPending !== undefined ? { currentPending } : {}),
497
186
  ...(wireCode !== undefined ? { errorCode: wireCode } : {}),
@@ -500,18 +189,11 @@ parkGatedCallId) {
500
189
  }
501
190
  case 'deny':
502
191
  try {
503
- // #144 半场(0.28.0):卡给的拒因原样上 wire(`/decide` 本就有 reason 位)—— 壳侧三处权限
504
- // 规则自动 deny(rule-deny / dontAsk / headless fail-closed)的真实拒因此前只能落 debug 日志,
505
- // 引擎与运维队列上看到的都是那句无差别的缺省文案。缺席/空白 ⇒ 缺省,现状字节不变。
506
- // 🔴 归因不改裁决:无论 reason 是什么,这一支恒 `decision:'deny'`。
507
192
  const raw = await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(card.reason, `task ${taskId}`) ?? DEFAULT_DENY_REASON }, gatedCallId, signal ? { signal } : undefined, pending);
508
- // B-070 同形存量:拒绝腿与放行腿**一次改齐**(两条 decide 腿都要带回执 —— 一条 deny 同样
509
- // 可能走 workflow 车道换挂,也同样可能拿到幂等回放形的 `executionOutcome`)。
510
193
  const receipt = readDecideReceipt(raw);
511
194
  return { kind: 'decided', gatedCallId, denied: true, ...(receipt !== undefined ? { receipt } : {}) };
512
195
  }
513
196
  catch (e) {
514
- // #363:与 allow 臂**同形同源**(同形存量清剿:两条 decide 失败腿一次改齐,不许只修当格)。
515
197
  const currentPending = readDecideCurrentPending(e);
516
198
  const wireCode = readWireErrorCode(e);
517
199
  const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
@@ -528,24 +210,16 @@ parkGatedCallId) {
528
210
  }
529
211
  }
530
212
  }
531
- /** 帧上 `readRootCandidate` 的窄读(模块内部,不出公面):在场且形对 ⇒ 新铸两键形(`covers: "exact"` 在场时三键);缺席 / 坏形 ⇒ undefined(键不置)。 */
532
213
  function readReadRootCandidate(v) {
533
214
  if (typeof v !== 'object' || v === null)
534
215
  return undefined;
535
216
  const o = v;
536
217
  if (typeof o.dir !== 'string' || o.dir.length === 0 || o.clearsThisAsk !== true)
537
218
  return undefined;
538
- // `covers` 域 = {缺席, "exact"}。出闭集(含键在场而值 undefined)⇒ **整只座不过境**,不是「摘掉坏成员、其余照发」:
539
- // 两种读法在人那一侧是相反的动作(逐字添加 vs 加目录),发一半会让人按错的那一种回授。与上游同一处置。
540
219
  if ('covers' in o)
541
220
  return o.covers === 'exact' ? { dir: o.dir, clearsThisAsk: true, covers: 'exact' } : undefined;
542
221
  return { dir: o.dir, clearsThisAsk: true };
543
222
  }
544
- /**
545
- * 本地值拷贝的**运行时键集镜像**([1950]② 建议形):对账测试拿它与 SDK 0.1.6+ 的同源锚
546
- * `TOOL_APPROVAL_FRAME_KEYS` 比对——SDK additive 增键时对账当天红,不再人肉追平。
547
- * 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
548
- */
549
223
  export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
550
224
  'type',
551
225
  'approvalId',
@@ -557,76 +231,20 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
557
231
  'message',
558
232
  'args',
559
233
  'argsOmitted',
560
- // sdk 6.9.0 追平(2026-08-08,#3004 跟修批):server 7.5.0 起真发 `governanceForced`,core 5.9.0 W1
561
- // 起真发 `delegation` —— 镜像滞后了两代,于是这两键在包内**零消费面**(治理徽标端到端不可达,
562
- // [1947] 那次 `toolCallId` 滞后的同款形)。本门(run-approval-frame-keys-test.mjs)对 SDK 运行期
563
- // 锚逐元素相等,所以这次滞后是被门抓到的,不是人肉发现的。
564
234
  'governanceForced',
565
- // sdk 6.14.0 追平(#225 件1,client-core 0.26.0):server 7.12.0 起真发规则候选 —— 门
566
- // (run-approval-frame-keys-test.mjs)在 bump SDK 当天恰红抓获本键缺席,本次补齐。
567
- // ⚠️ **退役键**(server 7.46.0 起同文件 0 命中):留在镜像里是因为 sdk 8.2.0 的运行期锚仍含它,
568
- // 删了对账门的「SDK 锚的每个键本仓都有」当场红;本包对它只做归一读(见帧上同名键的 JSDoc)。
569
235
  'ruleSuggestions',
570
- // #334/[5223](client-core 0.43.0):server 7.46.0 起把规则候选整体换成判别联合 `ruleOffers`
571
- // (core 5.58.0 design/375;engine 7.46.0 fixture 的 `@sema-agent/server/dist/tool-approval.d.ts`
572
- // 直证,同文件旧键 0 命中)。✅ **sdk 8.2.0 锚已含本键**(S-134 锚②)⇒ 0.58.0 提货批按退出
573
- // 条件删掉了对账门里那条 AHEAD_OF_ANCHOR 领先登记,本键回到「逐元素相等」的常态。
574
236
  'ruleOffers',
575
- // S-15 第五单(client-core 0.59.0):server 7.55.0 起真发 `ruleOffersAbsence`(core #490 修②;
576
- // engine 7.60.0 fixture 直证)。⚠️ 与 `ruleOffers` **同形不同命**:它是**追平**不是领先 ——
577
- // sdk 8.3.0 的运行期锚已含本键(node 直读 27 项实证)⇒ **不**进 AHEAD_OF_ANCHOR
578
- // (往那张表里塞一个锚已有的键,它的第二条退出条件当场红)。
579
237
  'ruleOffersAbsence',
580
- // S-114(client-core 0.59.0):server 7.57.0 起真发 `denialLimitFallback`(core 7.4.0 #548;
581
- // engine 7.60.0 fixture 直证)。同上一条:sdk 8.3.0 锚已含本键 ⇒ 追平,不进领先表。
582
- // 🔴 本键与 `requiresRealApproval` 是孪生键,但两者在镜像里各占一格 —— 合并会让「只有一位在场」
583
- // 的坏形无处显形。
584
238
  'denialLimitFallback',
585
- // 0.67.0(core 7.14.0 #688 C3):`ruleStoreUnreadable`。⚠️ 它**换掉**的是上一代同位的
586
- // `classifierUnavailable` —— core 7.14.0 把那一格从六个载体上整族退役(ask 那几处自 #661 起就没写过),
587
- // 本包按 clean-cut 删镜像成员、删读器、删卡位,**不留别名**(登记在 run-retired-vocabulary-census)。
588
- // ⚠️ 与 `persistedRuleShadowed`/`probeCause` 同形:sdk 8.8.0 的运行期锚**尚无**本键 ⇒ **领先**一代,
589
- // 进对账门的 AHEAD_OF_ANCHOR 带退出条件登记(sdk 8.9.x 补上那天那条登记自红逼删,回到逐元素相等)。
590
239
  'ruleStoreUnreadable',
591
- // CC-08(0.71.0):core 7.19.0 #803 根外读卡附读根候选;sdk 9.6.0 锚第 30 员。
592
240
  'readRootCandidate',
593
- // S-125③/#564(client-core 0.59.0):server 7.57.0 起真发 `origin`(core 7.5.0 `ASK_ORIGINS`
594
- // 八词;engine 7.60.0 fixture 直证)。同上:sdk 8.3.0 锚已含本键 ⇒ 追平,不进领先表。
595
241
  'origin',
596
- // #341/[5214]③(client-core 0.43.0):server 7.46.0 起真发 `inputHasBidi`(E-14 Trojan Source 族;
597
- // 7.44 fixture 同文件 0 命中)。仍**领先** sdk 8.2.0 锚(node 直读 18 项无本键)⇒ AHEAD_OF_ANCHOR 登记。
598
242
  'inputHasBidi',
599
- // 🔴 **同形族扫的产物**(client-core 0.43.0,[same-shape-residue-constitution]):本批的病形是
600
- // 「镜像滞后于 server 实发键」。族扫 = 把 engine 7.46.0 fixture 的 `ToolApprovalFrame` 23 个声明键
601
- // 与本镜像逐一对表,而不是只补触发本批的那两个 —— `parked`(#329,server 7.44.0 及更早就在,
602
- // `tool_approval_complete` only)就是这次捞出来的**存量**漏键。镜像补齐,消费面另立项(见帧上注)。
603
243
  'parked',
604
- // #144(client-core 0.28.0):server 7.13.0 起真发 `persistedRuleShadowed`(证据=engine fixture
605
- // 的 `@sema-agent/server/dist/tool-approval.d.ts`)。⚠️ 本键**领先** SDK 运行期锚一代
606
- // (sdk 6.14.0/6.15.0 的 TOOL_APPROVAL_FRAME_KEYS 都还没有它)⇒ 对账门里有一条**带退出条件**的
607
- // 领先登记(AHEAD_OF_ANCHOR):SDK 锚一旦补上,那条登记当场红,逼人删登记而不是让豁免长住。
608
244
  'persistedRuleShadowed',
609
- // #280 件1(client-core 0.30.4):server 7.21.0-rc(#253)起真发 `probeCause`(直证 = engine
610
- // 7.22.0 fixture 的 `@sema-agent/server/dist/tool-approval.d.ts`)。当时本键**领先** SDK 运行期锚
611
- // 一代(sdk 6.17.2 尚无)⇒ 对账门里挂着一条带退出条件的领先登记(AHEAD_OF_ANCHOR)。
612
- // ✅ **锚已追平**(0.30.8 提货,sdk 7.1.0 的 `TOOL_APPROVAL_FRAME_KEYS` 已含本键)⇒ 那条登记按它
613
- // 自己的退出条件当场翻红、同批删除,回到逐元素相等 —— #229 那批删 `persistedRuleShadowed`
614
- // 登记时走的是同一条路。领先表回空 = 健康态。
615
245
  'probeCause',
616
- // #281 件4b(client-core 0.30.8):server 7.23.0 / core 5.35.0 design/252 G-2 起真发 `ruleEvidence`。
617
- // ⚠️ 与上面两例**不同形**:sdk 7.1.0 的运行期锚**已经含**本键(node 直读实证)⇒ 这是一次**追平**,
618
- // 不是领先,故**不**进 AHEAD_OF_ANCHOR(往那张表里塞一个锚已有的键,它的第二条退出条件当场红)。
619
246
  'ruleEvidence',
620
- // #283(client-core 0.36.0):server 7.33.0 起真发 `requiresRealApproval`(core 5.37 铸的安全类 ask
621
- // 出身位,投到帧顶层)。⚠️ 与 `ruleEvidence` **不同形**、与 `persistedRuleShadowed`/`probeCause` 同形:
622
- // sdk 7.1.0 的运行期锚**尚无**本键(node 直读实证:锚 17 项)⇒ 这是一次**领先**,进对账门的
623
- // AHEAD_OF_ANCHOR 带退出条件登记(SDK 追平当天那条登记自红逼删,回到逐元素相等)。
624
247
  'requiresRealApproval',
625
- // [4851] 头号请托(client-core 0.38.2):server 7.34.0 起真发窗三键(#288/[4429]② cli 自己请托,
626
- // server src/tool-approval.ts 三键声明+条件 stamp)。⚠️ 与 requiresRealApproval 同形=**领先** SDK
627
- // 运行期锚(sdk 8.2.0 锚 18 项,node 直读实证无三键)⇒ 进对账门 AHEAD_OF_ANCHOR 带退出条件登记
628
- // (SDK 追平当天登记自红逼删)。0.38.1 把三键丢在包边界外 = [4845]「卡到期屏幕零反应」的直接成因
629
- // (壳读器 readAskDeadlineMs 早已预埋,缺的一直是包边界这一格——#283 逐字同形第四例)。
630
248
  'expiresInMs',
631
249
  'expiresAtMs',
632
250
  'serverNowMs',
@@ -635,74 +253,27 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
635
253
  ];
636
254
  const _mirrorPin = [true, true];
637
255
  void _mirrorPin;
638
- /**
639
- * `delegation` 的结构读(wire 是 JSON:注入面/旧 server/比本包新一版的 server 都可能给别的形)。
640
- * 坏形一律降**缺席** —— 与 `readToolApprovalRespondAck` 同族纪律:「拿到一个不好用的值」不如
641
- * 「诚实地什么都没有」。`parentToolCallId` 是卡的归属锚、`depth` 决定「第几层」的措辞,两者任一
642
- * 不成形就整体不透传(半个链比没有链更容易被渲成错话);`agentName` 非串时单独降缺席。
643
- *
644
- * **不导出**(2026-08-08 收货修):它是本模块的边界窄化器,唯一消费点就在下面那个卡口调用;导出会
645
- * 给公面加一个 `unknown` 入参签名(typeshape 门的 unknown-出境棘轮 +1),而它的行为在 pure 门里由
646
- * `surfaceToolApprovalFrameAndRespond` 的坏形/正形素材端到端覆盖 —— 真消费路径比直调守卫更强。
647
- */
648
256
  function isToolApprovalDelegation(v) {
649
257
  if (v === null || typeof v !== 'object')
650
258
  return false;
651
259
  const d = v;
652
- // 归属锚:必须是**非空白**串(`' '` 当 tool-call id 用会让卡按一个空锚归属)。
653
260
  if (typeof d.parentToolCallId !== 'string' || d.parentToolCallId.trim().length === 0)
654
261
  return false;
655
- // 深度:上游语义是「宿主直接子代 = 1」⇒ **正安全整数**。0 / 负数 / 小数都不是可渲染的层数,
656
- // 而 `Number.isFinite` 会把它们全放过(对抗复审命中:`depth: 0` / `-1` / `1.5` 曾一律判真)。
657
262
  if (typeof d.depth !== 'number' || !Number.isSafeInteger(d.depth) || d.depth < 1)
658
263
  return false;
659
264
  return d.agentName === undefined || typeof d.agentName === 'string';
660
265
  }
661
- /**
662
- * 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
663
- * (server 1.258 [1549]①3,旧代际兼容)。
664
- *
665
- * 🔴 **`=== true` 是在场判别,不是布尔求值**(#307 S47 复核结论,2026-08-19)。`fromSubagent`
666
- * 的 wire 形是 `?: true`(见上方声明),所以运行期只可能是 `true` 或缺席;一个**显式 `false`**
667
- * 只能来自注入面或不合契约的实现,它承载的信息是「这个载体不合契约」,**不是**「引擎明确判定不是
668
- * 子代」。故这里刻意**不**把 `false` 当成否定证据:它与缺席同档 —— 落回 `sourceTaskId` 在场性
669
- * 那条旧代际权宜臂。
670
- *
671
- * 为什么这个方向是对的(而不是「显式 false ⇒ 直接 return false」):
672
- * · 本判据的**唯一消费面是展示归属**(`workerBadge` 徽章 + `delegation` 出处链,见下方卡口),
673
- * 不参与任何放行/收窄决策 —— 两个方向的代价不对称:多一枚徽章只是噪声,少一枚徽章是把
674
- * 「这是子代发起的」这条事实对用户藏起来;
675
- * · `sourceTaskId` 本身就是 server 只对后台子代 lane 才出的键(见其声明),它在场是**独立的**
676
- * 子代证据。让一个不合契约的 `false` 去否决一条独立成立的证据,等于让注入面拿到一个
677
- * 「隐藏子代身份」的开关;
678
- * · 与本文件 `governanceForced` 的「缺席 ≠ false」同族纪律:在场即真的键上,非 `true` 一律读作
679
- * 「没有这条证据」,而不是「有一条相反的证据」。
680
- * 反漂移:类型侧 `?: true` 已让**编译期**的显式 false 不可能构造;本臂守的是运行期(wire/注入面)
681
- * 的越界载体,pure 门有对应负控(`fromSubagent:false` + sourceTaskId 在场 ⇒ 仍判子代)。
682
- */
683
266
  export function isFromSubagent(frame) {
684
267
  if (frame.fromSubagent === true)
685
268
  return true;
686
269
  return typeof frame.sourceTaskId === 'string' && frame.sourceTaskId.length > 0;
687
270
  }
688
- /** 上面那个闭集的运行期真源(ack 判形要用;词表与类型由下方 `Covers` 双向钉住)。 */
689
271
  const RESPOND_DECISIONS = ['allow', 'allow_session', 'deny'];
690
272
  const _respondDecisionPins = [true, true];
691
273
  void _respondDecisionPins;
692
274
  function isRespondDecision(v) {
693
275
  return typeof v === 'string' && RESPOND_DECISIONS.includes(v);
694
276
  }
695
- /**
696
- * respond 回执的**结构化读口**(wire 是 JSON:注入面可能是旧 liveClient 的 raw fetch,也可能是
697
- * 比本包新一版的 SDK)。坏形一律降 `undefined` —— 与 `controlRouter.errCodes` 同族纪律:
698
- * 「拿到一个不好用的值」不如「诚实地什么都没有」。两个可选布尔位非布尔时**单独**降缺席
699
- * (它们直接决定要不要渲「本会话全放行」,透传一个 `'yes'` 字符串会让 `!== false` 这类读法说反话)。
700
- *
701
- * 🔴 `decision` 判到 **wire 三词闭集**而不是「是不是串」(对抗复审二轮):ack 上那两个布尔位会
702
- * 触发**高危安全声明**(「工具正在用原始入参跑」),所以一个 `decision:'garbage'` 的回执绝不许被
703
- * 当成合规 ack 放行。**相关性**(「这个 ack 是不是**这一次**审批的回执」)不在本函数判 —— 本函数
704
- * 只认形,对不对得上由 {@link surfaceToolApprovalFrameAndRespond} 拿着 frame 与刚发出的 decision 核。
705
- */
706
277
  export function readToolApprovalRespondAck(v) {
707
278
  if (v === null || typeof v !== 'object')
708
279
  return undefined;
@@ -719,40 +290,18 @@ export function readToolApprovalRespondAck(v) {
719
290
  decision: o.decision,
720
291
  ...(typeof o.rememberApplied === 'boolean' ? { rememberApplied: o.rememberApplied } : {}),
721
292
  ...(typeof o.updatedInputForwarded === 'boolean' ? { updatedInputForwarded: o.updatedInputForwarded } : {}),
722
- // #225 件1(sdk 6.14.0):兑付回执两位。缺席 ≠ false(未带 persistRule 的回决/旧 server 字段
723
- // 省略);非布尔/非串一律降缺席 —— 「规则存没存上」直接决定壳的诚实告知,透传坏形会说反话。
724
293
  ...(typeof o.rulePersisted === 'boolean' ? { rulePersisted: o.rulePersisted } : {}),
725
294
  ...(typeof o.ruleRefusal === 'string' && o.ruleRefusal !== ''
726
295
  ? { ruleRefusal: o.ruleRefusal }
727
296
  : {}),
728
- // #229(sdk 6.16.0):这次回决的 note 有没有**真落进持久行**。缺席 ≠ false(未带 note 的回决/
729
- // 老 server 字段省略);非布尔降缺席 —— 它决定「审计理由记没记上」的诚实告知。
730
297
  ...(typeof o.noteRecorded === 'boolean' ? { noteRecorded: o.noteRecorded } : {}),
731
- // #340 编辑臂回显(server ≥7.44):**真正落盘**的规范文本(可能与人敲的原字节不同)。
732
- // 非空串才收 —— 空串是坏值不是「空规则」,回显一格空白比不回显更坏。
733
298
  ...(typeof o.persistedRule === 'string' && o.persistedRule !== '' ? { persistedRule: o.persistedRule } : {}),
734
- // #334 批臂回执(server ≥7.46.0):落地的全体成员规范文本,展示序。
735
- // 🔴 **整只判形**(不逐条丢):合取批的回执是一份清单,漏掉一条就是对人少报了一次授权 ——
736
- // 非数组 / 空 / 含非串或空串成员 ⇒ 整只降缺席(诚实「不知道」优先于半份清单)。
737
299
  ...(isNonEmptyStringArray(o.persistedRules) ? { persistedRules: o.persistedRules } : {}),
738
- // P-39 归属锚(server ≥7.48.0):同样**整只判形**,理由见该位注。
739
300
  ...(() => {
740
301
  const anchors = readPersistedRuleAnchors(o.persistedRuleAnchors);
741
302
  return anchors !== undefined ? { persistedRuleAnchors: anchors } : {};
742
303
  })(),
743
304
  };
744
- // 🔴 **两位规范文本回显的「同份 ack 内矛盾形」**(异源对抗复审 [high] 采纳,0.43.0)——
745
- // 这与本函数不做的**相关性**判定(「这个 ack 是不是这一次审批的回执」,归编排层)是两件事:
746
- // 下面判的全是**这一个对象自己**说不通的话,属于形的范畴。
747
- // ① 单数与复数**同场**:server 的两个回显是互斥臂的产物(`editedArmEcho` 只在 single 成功时
748
- // 铸、`persistedRules` 只在 batch 成功时铸),同场 = 这份回执在说两次不同的授权;
749
- // ② 回显在场而 `rulePersisted !== true`:server 的成功回显恒随 `withFlag(true, …)` 一起铸,
750
- // 「没存上」却报出一条「存成了什么」是自相矛盾 —— 照收会让端渲一句「你的规则已存为 X」,
751
- // 而 `rulePersisted` 那一位同时在说没存上。
752
- // 两形都**只丢这两位**(不牵连整份 ack):`rememberApplied`/`updatedInputForwarded` 是更重的
753
- // 安全告知位,不该被一条规则回显的坏形连坐掉。
754
- // 🔴 归属锚随它所归属的那份清单一起判:单数回显是**编辑臂**的产物,批臂的归属锚与它同场 =
755
- // 这份回执在说两次不同的授权(与下面①同一条理由,只是多一位要一起丢)。
756
305
  if (out.persistedRule !== undefined && (out.persistedRules !== undefined || out.persistedRuleAnchors !== undefined)) {
757
306
  delete out.persistedRule;
758
307
  delete out.persistedRules;
@@ -764,17 +313,6 @@ export function readToolApprovalRespondAck(v) {
764
313
  delete out.persistedRules;
765
314
  delete out.persistedRuleAnchors;
766
315
  }
767
- // 🔴 **跨位自洽**(P-39,0.44.0):锚是 `persistedRules` 的逐成员分解 —— server 两处铸点都在同一个
768
- // `persisted` 结果上取 `rules` 与 `members`,所以条数恒等。不等 = 这份回执对「到底存了什么」
769
- // 自相矛盾。**只丢锚、保留 `persistedRules`**:additive 位到货绝不回头削弱既有位的现行为
770
- // (老 server 上本来就没有锚,那条路仍按基数门走)。
771
- // 🔴 **逐位置同文本**(异源对抗复审二轮 [high] 采纳):server 侧两位是**同一个数组**的两种投影 ——
772
- // dist `rules-consent.js` 逐字 `{ rules: anchored.map(a => a.rule), members: anchored }`,且两者
773
- // 同按 `memberIndex` 排序 ⇒ `persistedRules[i] === anchors[i].rule` 是**铸造期保证**,不等只可能
774
- // 来自被改过/串了台的回执。
775
- // ⚠️ 与本文件「**不复判文本**」那条纪律不冲突:那条禁的是拿回执文本去比 **offer 上的**文本
776
- // (落盘的是规范化后的字节,与 offer 按设计可以不同 —— 比它就是装第二个判官);这里比的是
777
- // **同一份回执内部**两个位互相说的话,属「这个对象自己说不通」的形的范畴。
778
316
  if (out.persistedRuleAnchors !== undefined &&
779
317
  (out.persistedRules === undefined ||
780
318
  out.persistedRules.length !== out.persistedRuleAnchors.length ||
@@ -783,23 +321,9 @@ export function readToolApprovalRespondAck(v) {
783
321
  }
784
322
  return out;
785
323
  }
786
- /** {@link ToolApprovalRespondAckView.persistedRules} 的整只判形(见该位注:半份清单比没有更坏)。 */
787
324
  function isNonEmptyStringArray(v) {
788
325
  return Array.isArray(v) && v.length > 0 && v.every(x => typeof x === 'string' && x !== '');
789
326
  }
790
- /**
791
- * {@link ToolApprovalRespondAckView.persistedRuleAnchors} 的**整只**判形(P-39,server ≥7.48.0)。
792
- *
793
- * 逐条要求三键齐、`offerIndex`/`memberIndex` 是**非负安全整数**、`rule` 非空串;再加两条**同只回执内
794
- * 自洽**:
795
- * · `offerIndex` **整表同值** —— server 的 map 闭包捕获的是同一个 `persistRule.batchOfferIndex`
796
- * (dist 直证),混合值的表不可能由合规 server 铸出;
797
- * · `memberIndex` **不重复** —— 两条锚认领同一个成员,那份归属表就不是它自称的逐成员分解。
798
- * 任一条不满足 ⇒ **整只降缺席**(不逐条丢):半份归属表会让相关性门拿着残表放行,比没有锚更坏。
799
- *
800
- * 🔴 下标**绝不折算**(不 `|0`、不 `Math.trunc`):一个被折过的下标指向的是**另一条** offer /
801
- * 另一个成员,那正是本位要证伪的东西。
802
- */
803
327
  function readPersistedRuleAnchors(v) {
804
328
  if (!Array.isArray(v) || v.length === 0)
805
329
  return undefined;
@@ -824,44 +348,15 @@ function readPersistedRuleAnchors(v) {
824
348
  seenMembers.add(memberIndex);
825
349
  out.push({ offerIndex, memberIndex, rule });
826
350
  }
827
- // 🔴 **座位必须恰好铺满 `0..n-1`**(异源对抗复审二轮 [high] 采纳)。这不是本包发明的严格,是
828
- // **server 自己的不变量**(dist `rules-consent.js` 的 `anchorBatchMembers`,逐字):
829
- // · `anchored.length !== displayed.rules.length || distinctSeats !== anchored.length`
830
- // ⇒ **整个不发 members**(锚要么是与人看见的那只 offer **一一对应的满覆盖**,要么缺席);
831
- // · 发之前还 `anchored.sort((a,b) => a.memberIndex - b.memberIndex)`。
832
- // ⇒ 合规锚表的 memberIndex 集合恒 = `{0..n-1}`。越界座位是 server 铸不出的形。
833
- // ⚠️ 只校**集合**不校**顺序**:排序是 server 今天的实现细节,而集合是它成文的不变量
834
- // ([verdict-must-accept-stronger-form]:别把一个合法的更强形判成回归)。
835
- // ⚠️ 「7.48 引擎 + 锚缺席」是**合法形**(上面两条 warn 分支就是它)⇒ 缺席绝不可读作可疑。
836
351
  for (const seat of seenMembers)
837
352
  if (seat >= out.length)
838
353
  return undefined;
839
354
  return out;
840
355
  }
841
- /** 非负安全整数(下标位共用;`Number.isSafeInteger` 已排除 NaN/Infinity/小数)。 */
842
356
  function isNonNegativeSafeInt(v) {
843
357
  return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
844
358
  }
845
- /**
846
- * 从 respond 抛出来的东西上读 {@link ToolApprovalRespondRefusal}。**永不抛、永不造**。
847
- *
848
- * 键位口径与 {@link import('../wireErrorTriage.js').classifyTurnWireError} 同源([2055] 死键
849
- * 纪律:只认活键 `errorCode`,退役的 `code` 槽不做兼容)。
850
- *
851
- * @returns 三位**全缺席**时返回 `undefined` —— 一个三位皆空的 refusal 对象是「有拒句」的假象。
852
- *
853
- * 🔴 **文本取值序**(异源复审 [medium] 修):`err.message` 非空串 > 抛出物本身是非空串 >
854
- * 受保护的 `String(err)`,且**只接受**真有内容的结果 —— `[object Object]` / `null` /
855
- * `undefined` 三种占位串一律当作「读不出」。
856
- * 🔴 **`String(err)` 用 try 包住**:抛出物可以自带 `Symbol.toPrimitive` / `toString` 钩子并在里面
857
- * 抛错。本函数的唯一调用点在 `surfaceToolApprovalFrameAndRespond` 的 catch 里,那里的契约是
858
- * 「respond 失败 ⇒ 返回 unresolved」;让一个不可信的转换钩子把这条收敛路径变成 reject,
859
- * 等于给注入面开了一个「让整次审批消费抛出去」的口。
860
- */
861
359
  export function readToolApprovalRespondRefusal(err) {
862
- // 🔴 **取属性本身就可能抛**(异源复审 [medium] 的形比它自己说的还宽一档,施工时实撞):
863
- // 抛出物可以是一个带 `get message() { throw }` 的对象 —— 连 `typeof e.message` 这一步都会炸,
864
- // 根本走不到下面的转换。所以逐位读**各自**包 try,读不动就当那一位缺席。
865
360
  const pick = (key) => {
866
361
  try {
867
362
  return err?.[key];
@@ -882,17 +377,13 @@ export function readToolApprovalRespondRefusal(err) {
882
377
  ...(errorCode !== undefined ? { errorCode } : {}),
883
378
  ...(message !== undefined ? { message } : {}),
884
379
  };
885
- // 三选一联合的**构造侧证明**:哪一位在场就从哪一位收窄(与上面的 early-return 合起来,
886
- // 「至少有一位」在编译期与运行期是同一条不变量,不是两份各自维护的承诺)。
887
380
  if (status !== undefined)
888
381
  return { ...rest, status };
889
382
  if (errorCode !== undefined)
890
383
  return { ...rest, errorCode };
891
384
  return { ...rest, message: message };
892
385
  }
893
- /** 无信息量的占位串:`String()` 对这几类抛出物的产物,它们不是「拒句」。 */
894
386
  const USELESS_REFUSAL_TEXTS = new Set(['[object Object]', 'null', 'undefined', '']);
895
- /** 见 {@link readToolApprovalRespondRefusal} 的取值序与 try 保护。读不出 ⇒ `undefined`。 */
896
387
  function refusalTextOf(err, message) {
897
388
  if (typeof message === 'string' && message.length > 0)
898
389
  return message;
@@ -903,31 +394,16 @@ function refusalTextOf(err, message) {
903
394
  coerced = String(err);
904
395
  }
905
396
  catch {
906
- // 不可信的 toString/Symbol.toPrimitive 钩子抛了 —— 诚实缺席,绝不让它逃出 catch。
907
397
  return undefined;
908
398
  }
909
399
  return USELESS_REFUSAL_TEXTS.has(coerced) ? undefined : coerced;
910
400
  }
911
- /**
912
- * 机读码的**日志安全形**(异源复审 [medium] 采纳,0.42.0):`errorCode` 来自 UNTRUSTED 应答体且
913
- * 无长度/控制字符/换行约束,把它原样插进宿主日志 = 可伪造日志行、可注入终端控制序列、还能把一段
914
- * 服务端正文当「码」漏进 sink。
915
- *
916
- * 🔴 **只有形合的才照写**(真机读码的字符集):`[A-Za-z0-9._:-]{1,64}`。形不合 ⇒ 只报**长度**,
917
- * 正文一个字节都不进日志(与本文件 note 那条「留痕只写元数据」逐字同纪律)。
918
- * 🔴 这**不影响**交还调用方的那一份:{@link ToolApprovalRespondRefusal.errorCode} 仍是**原样**
919
- * ——净化的是日志这条外溢面,不是契约。
920
- */
921
401
  const MACHINE_CODE_SHAPE = /^[A-Za-z0-9._:-]{1,64}$/;
922
402
  function logSafeErrorCode(code) {
923
403
  if (code === undefined)
924
404
  return 'none';
925
405
  return MACHINE_CODE_SHAPE.test(code) ? code : `<unprintable len=${code.length}>`;
926
406
  }
927
- /** 结构性识别流上的 tool_approval 帧(named SSE frame,payload.type === 帧名)。
928
- * ⚠️ 口径更正(2026-08-08):此处原写「非 AgentEvent arm」—— SDK #185a 起这两个帧**是**
929
- * `AgentEvent` 的臂了(durable 腿也回放),所以结构识别与 union 收窄两条路都成立;本函数仍按
930
- * 结构读(不依赖类型收窄),因为它同时服务 raw SSE 与 durable 回放两条入口。 */
931
407
  export function isToolApprovalFrame(ev) {
932
408
  const e = ev;
933
409
  return (!!e &&
@@ -935,73 +411,18 @@ export function isToolApprovalFrame(ev) {
935
411
  typeof e.approvalId === 'string' &&
936
412
  e.approvalId.length > 0);
937
413
  }
938
- /** core `fs-write-gate-policy` 的 ask 文案(逐字,具名常量便于对表)。 */
939
414
  const FS_WRITE_GATE_ASK_PATTERN = /^approve write to "([^"]+)"\?$/;
940
- /** 从 gate 的 ask 文案(core fs-write-gate-policy `approve write to "<path>"?`)兜出 path——argsOmitted
941
- * 且流上无 tool_start.args 时的最后退化(卡仍可弹,diff 退化为空写)。
942
- * REF-CC-035(2026-08-02):锚定 core 的精确文案前缀,不是「任意一对引号」—— 旧正则
943
- * `/"([^"]+)"/` 对任何含引号的 gate 文案都会命中并**静默给错 path**(不是不命中,是给错值,
944
- * 而 path 是审批卡展示给用户做安全决策的对象)。锚定失败即诚实缺席(返回 undefined,调用方
945
- * 落回 `{}` 空写而不是猜一个;[honest-absence-not-fabricated-zero] 同族)。 */
946
415
  function pathFromGateMessage(message) {
947
416
  if (typeof message !== 'string')
948
417
  return undefined;
949
418
  return FS_WRITE_GATE_ASK_PATTERN.exec(message)?.[1];
950
419
  }
951
- /**
952
- * `ruleSuggestions` 的结构读(wire 是 JSON:注入面/坏 mock/异代 server 都可能给别的形)。
953
- * 逐项窄化:`rule` 非空串 + `match` 二词闭集 合形才保留(`command` 非串时单独降缺席);
954
- * 全部不合形/非数组 ⇒ 整体缺席 —— 卡形退 0.25.0 字节(不渲「不再询问」档),绝不拿半条候选
955
- * 渲一个按下去必被 server 拒的选项。上限 8 条(server 端候选本就 ≤2,超长=坏形,截不留痕会
956
- * 掩盖注入,整体降缺席)。
957
- */
958
- /**
959
- * **开集记录键的载体形判**(#280 件1 起 `probeCause` 用;#281 件4b 起 `ruleEvidence` 同用):
960
- * 非 null 非数组对象才搬运 —— 这两键的 wire 契约载体都是**记录**,串/数/数组/null 是坏载体,
961
- * 不是「另一种因由 / 另一种证据」。
962
- *
963
- * 🔴 刻意**只判到载体这一层**:内部成员的校验(probeCause 的 code/roots/further;ruleEvidence 的
964
- * 值 ⊕ 具名缺席词、矛盾形、整只拒读)归**呈卡端窄读器**(壳 `probeCauseNote.readProbeCauseKey` /
965
- * `ruleEvidenceNote.readRuleEvidenceKey`,各自与 server 铸点 `readProbeCause` / `readRuleEvidence`
966
- * 同向)。包内校严一档就会把上游 additive 内键挡在边界外,还让「谁把键窄没的」在排障时多一层嫌疑。
967
- * 🔴 **一把判词服务多键**是设计而非省事:两条决断腿(活帧 / durable 行)× 多个开集记录键共用同一个
968
- * 载体判据,判词绝不各写各的(各写一份 = 同一个问题有了两个属主,分叉那天没人发现)。
969
- */
970
- /** 窗三键的坏形闸:三键定义域按 SDK 契约本就非负(`expiresInMs = max(0, …)`,0 合法=窗恰好
971
- * 到点)。裸 `Number.isFinite` 会放行负数 —— 与铸卡注承诺的「负数=坏形降缺席」相反(同文件
972
- * `delegation.depth` 曾同款教训,异源对抗复审第二次命中同形)。 */
973
420
  function isNonNegativeFinite(v) {
974
421
  return typeof v === 'number' && Number.isFinite(v) && v >= 0;
975
422
  }
976
423
  function isWireRecordCarrier(v) {
977
424
  return typeof v === 'object' && v !== null && !Array.isArray(v);
978
425
  }
979
- /**
980
- * `denialLimitFallback` 的**窄读器**(S-114,0.59.0)—— wire 是 JSON:注入面 / 旧 server / 比本包新
981
- * 一版的 server 都可能给别的形,坏形一律降**缺席**(与 `readToolApprovalDelegation` 同族纪律)。
982
- *
983
- * 🔴 **四成员全必填,缺一整只丢**(与 server `approval-card.ts` 的 `readDenialLimitFallback`
984
- * `.strict()` 同判据,与 core 的形一字不差):半只对象上的 `consecutive` 会被人当成**真实的
985
- * 连续拒次数**读,而它可能只是一个恰好在场的键。「一格空白的计数」比不渲这张卡更坏。
986
- * 🔴 **两个计数与窗按「有限非负数」判,不折 0**:`Number.isFinite` 单独会放行负数,而三者的定义域
987
- * 按上游契约本就非负(计数是次数,窗是 ms 且 `0` = 不武装,是**合法读数**不是缺席)。
988
- * NaN / Infinity / 负数 = 坏形降缺席,**绝不猜**(同文件 `delegation.depth` 的旧教训)。
989
- * 🔴 **`limit` 按开集读**(非空串即收):闭二词表 `DENIAL_LIMIT_KINDS` 只作再导出给端数,
990
- * 包内**不拿它做判定** —— 词表属主是 core,抄一份就是给自己立第二个判官(B-025 的病形);
991
- * server 侧已按闭集拒过词表外的值,包再校一遍只会在 core 加员当天把一个合法值判没。
992
- * ⚠️ 这条与「四成员全必填」不矛盾:必填说的是**在场性**,开集说的是**取值域**。
993
- *
994
- * 🆕 **0.64.2 起在公面上**(cli L-174③;此前是模块私有):壳侧此前自持一份同判据的副本
995
- * (`src/sema/askFrameNotes.ts`),那是**第二个判官** —— 上游哪天在四成员上加一位、或把某一位的
996
- * 定义域改了,两份判据各漂各的,而屏上看到的是哪一份取决于素材走了哪条腿。导出的用途正是让那份
997
- * 副本整只退役:端读**包内窄读产物**({@link ApprovalCardRequest.denialLimitFallback})时不必再判
998
- * 一遍,端拿到**裸帧/裸行**时也有同一把读器可用。
999
- * 🔴 **导出的是读器,不是许可**:`autoDenyAfterMs` 仍然只许渲倒计时(窗的执行全在引擎),
1000
- * 这一条不因它上了公面而松动。
1001
- * ⚠️ **与 server `.strict()` 的一格差**(L-120③,如实写在两侧):对象上**多出的成员**本读器剥后
1002
- * 保留四键,server 侧整只拒收 —— 包比 server 宽一格,方向是「多键不误杀」。端不要把「包读出来了」
1003
- * 当成「server 也会收」。
1004
- */
1005
426
  export function readDenialLimitFallback(v) {
1006
427
  if (!isWireRecordCarrier(v))
1007
428
  return undefined;
@@ -1021,43 +442,12 @@ export function readDenialLimitFallback(v) {
1021
442
  autoDenyAfterMs: d.autoDenyAfterMs,
1022
443
  };
1023
444
  }
1024
- /**
1025
- * `ruleStoreUnreadable` 的**过境窄读器**(0.67.0;core 7.14.0 #688 C3)—— 两条卡腿(活卡帧 /
1026
- * durable park 行)共用的**唯一**一把:两处键路同形,各写一份就是两份台账各漂各的。
1027
- *
1028
- * 🔴 **闭二词按闭集判**(与本文件其余开集位刻意不同,理由在上游的形上):core 在**唯一 stamp 点**
1029
- * (`gate-lanes.ts` `ruleStoreUnreadableOf`)把政策自报的值**剥掉**之后才盖章,合同逐字
1030
- * 「A policy's self-declared value is removed at the stamp」⇒ 一个表外值**结构上**到不了消费端;
1031
- * 真读到就是坏形,而把坏形渲成一句机制解释会把人指到错的地方(`store` 去修部署的店 /
1032
- * `call` 去改自己那条规则,两条下一步相反)。这与 `origin`/`ruleOffersAbsence` 的开集纪律不矛盾:
1033
- * 那两位在 server 侧**只判非空串**,core 加词的当天一个**合法**值就会带着帧到达。
1034
- * 🔴 **只交那个词**:载体是一个裸串(不是对象)—— 不顺手读载体上别的东西。
1035
- * 🔴 **非串 / 空串 / 表外词 ⇒ 缺席**,绝不折成空串(一格空白的机制解释比没有解释更坏)。
1036
- * **不导出**:它是本模块的边界窄化器,唯一消费点是下面两个卡口调用;导出会给公面再加一个 `unknown`
1037
- * 入参签名(typeshape 门的 unknown-出境棘轮 +1),而它的行为由两条腿的端到端素材覆盖。
1038
- */
1039
445
  function readRuleStoreUnreadable(v) {
1040
- // 🔴 0.68.0 / L-245:谓词已是**型守卫** ⇒ 这里的 `as string` 退役(一处 cast 少一处)。
1041
446
  return isRuleStoreUnreadableKind(v) ? v : undefined;
1042
447
  }
1043
- /**
1044
- * `match` 是不是**闭词表成员** —— 表来自 sdk 8.2.0 `RULE_OFFER_MATCHES`(`exact` / `prefix` /
1045
- * `wildcard` / `subpath`),**本包不再手抄字面量**。
1046
- *
1047
- * 🔴 **B-025 的根因就是手抄**:0.43.0 铸这只读器时按当时 server 的两员词表写死
1048
- * `match !== 'exact' && match !== 'prefix'`,core design/382 / #510 扩到四员之后这里没跟 ⇒
1049
- * 一条合法的 `wildcard` / `subpath` 候选被判成坏形丢掉。读同一张 `as const` 数组之后,
1050
- * 「词表加员」在上游一处发生,本包自动跟上;门里另有**双向全等钉**(本包认的词集 ≡ sdk 数组
1051
- * 成员集,任一侧加员而另一侧没跟 ⇒ 当场红)。
1052
- */
1053
448
  function isRuleOfferMatch(v) {
1054
449
  return typeof v === 'string' && RULE_OFFER_MATCHES.includes(v);
1055
450
  }
1056
- /**
1057
- * 一条 `single` offer / 一个 `command` 成员共用的三位窄化(`rule` 非空串 + `match` **四词闭集** +
1058
- * `command` 非空串)。任一不合形 ⇒ `undefined`(半条候选渲出来是一格没有正文、按下去必被 server
1059
- * 拒的选项)。
1060
- */
1061
451
  function readRuleTriple(v) {
1062
452
  if (v === null || typeof v !== 'object')
1063
453
  return undefined;
@@ -1066,25 +456,10 @@ function readRuleTriple(v) {
1066
456
  return undefined;
1067
457
  if (!isRuleOfferMatch(o.match))
1068
458
  return undefined;
1069
- // 上游形上 `command` 必填(渲染用的命令模式)——缺席/非串 ⇒ 整项不合形丢弃。
1070
459
  if (typeof o.command !== 'string' || o.command === '')
1071
460
  return undefined;
1072
461
  return { rule: o.rule, match: o.match, command: o.command };
1073
462
  }
1074
- /**
1075
- * 一个 `batch` 成员的窄化(sdk 8.2.0 判别联合 {@link RuleOfferBatchMember};B-025 修根)。
1076
- *
1077
- * 判别位 `kind` 读 sdk `RULE_OFFER_BATCH_MEMBER_KINDS`(`command` / `directoryRead`),外加一条
1078
- * **pre-B3 兼容臂**:`kind` **缺席**的成员按 `command` 归一(server ≥7.46.0 到 design/382 §2.3 B3
1079
- * 落地之间铸的成员没有判别位 —— 窄读域只许等于或宽于铸点域,绝不更窄)。
1080
- *
1081
- * 🔴 **不识的 `kind`(在场但不是表里的词)⇒ `undefined`,调用方据此丢掉整只 batch**,
1082
- * 绝不丢单个成员:core design/382 §2.3 规范性降级臂逐字 "drops the WHOLE batch offer —
1083
- * never the single member"(少一条成员的合取批把「对 N 条说 yes」渲成「对 N−1 条说 yes」,
1084
- * 比不渲更坏),旁边那条完整诚实的 `single` 照留。
1085
- * 🔴 `segment` 是每个臂都必带的渲染座(非空串);`directoryRead` 臂**没有** `match`/`command`,
1086
- * 拿三元组窄读器去读它正是 0.57.0 把整只 batch 判假的那一形。
1087
- */
1088
463
  function readRuleOfferBatchMember(v) {
1089
464
  if (v === null || typeof v !== 'object')
1090
465
  return undefined;
@@ -1092,13 +467,7 @@ function readRuleOfferBatchMember(v) {
1092
467
  const seg = o.segment;
1093
468
  if (typeof seg !== 'string' || seg === '')
1094
469
  return undefined;
1095
- // pre-B3 兼容臂:判别位**缺席** = 历史形的 `command` 成员(那一代 wire 上根本没有 `kind`)。
1096
- // 🔴 归一放在最前面,后面只有**一个**成员性判据 —— 缺席与在场两条路走同一道闸,不留第二个判官。
1097
470
  const kind = o.kind === undefined ? 'command' : o.kind;
1098
- // 🔴 **成员性判到 sdk 的闭词表,不手抄字面量**(异源对抗复审 r1 [medium] 逼出的同形残余):
1099
- // 本函数初稿把 `'directoryRead'` / `'command'` 两个词**手写**在分支条件里,导入的
1100
- // `RULE_OFFER_BATCH_MEMBER_KINDS` 只被再导出、运行期一次都没读 —— 那正是 B-025 本身的病形
1101
- // (`match` 手抄两员)在成员这一维上的**残余**,只是当天恰好抄对了。抄对的表明天也会过期。
1102
471
  if (typeof kind !== 'string')
1103
472
  return undefined;
1104
473
  if (!RULE_OFFER_BATCH_MEMBER_KINDS.includes(kind))
@@ -1118,20 +487,8 @@ function readRuleOfferBatchMember(v) {
1118
487
  return undefined;
1119
488
  return { kind: 'command', ...triple, segment: seg };
1120
489
  }
1121
- // 🔴 **表内、但本包还没有读器的 kind** ⇒ 与表外同处置(丢 ⇒ 调用方丢整只 batch),绝不猜一个形。
1122
- // 上游往闭词表加员而本包没跟时走这条路:行为是 fail toward asking(合取批不渲,兄弟 single 照留),
1123
- // 而门 ⑨c 的「⊇ 成员 kind 在读器里走得通」那一腿会**当天红**,逼人补读器 —— 静默降级与显形二者兼得。
1124
490
  return undefined;
1125
491
  }
1126
- /**
1127
- * `uncoveredDetail`(design/382 §3.5 **additive** 明细座)的窄化。
1128
- *
1129
- * 🔴 **只丢坏行,绝不因为它否决整只 batch**:additive 位到货**不许回头削弱既有位的现行为** ——
1130
- * 老引擎压根不铸这一位,server 自己对坏形的座也是「只丢座不丢批」。整只读空 ⇒ 本位缺席
1131
- * (**不是**空数组:空数组会被端读成「已确认没有未覆盖段」,而真源恒是 `uncoveredSegments`)。
1132
- * 🔴 `reason` 判到 sdk 8.2.0 的闭三词集(`redirection` / `no_rule_form` / `cap_overflow`);
1133
- * 不识的 reason 只丢那一行 —— 它是**给人看的因由**,不驱动任何兑付动作。
1134
- */
1135
492
  function readUncoveredDetail(v) {
1136
493
  if (!Array.isArray(v))
1137
494
  return undefined;
@@ -1152,52 +509,9 @@ function readUncoveredDetail(v) {
1152
509
  }
1153
510
  return out.length > 0 ? out : undefined;
1154
511
  }
1155
- /** offer **条数**的坏形帽。server 侧执法帽是 `MAX_RULE_OFFERS`=4(core 契约本就 ≤2),本包留 8 ——
1156
- * [4050] 随批立的纪律:**窄读域只许等于或宽于铸点域,绝不更窄**;超帽 = 坏形/注入,整只降缺席
1157
- * (截了不留痕会掩盖注入)。 */
1158
512
  const MAX_RULE_OFFERS_TOLERATED = 8;
1159
- /** 一条 `batch` 的**成员**条数坏形帽(core 契约 1..5,server 容忍帽 8)。与上面那个刻意分家:
1160
- * 两者数的是不同的东西(几个选项 vs 一个选项里几条规则),混成一个数会让上游放宽任一维时误伤另一维。 */
1161
513
  const MAX_RULE_OFFER_BATCH_MEMBERS_TOLERATED = 8;
1162
- /**
1163
- * `uncoveredDetail` 的**行数**坏形帽(0.58.0)。
1164
- *
1165
- * 🔴 **锚在上游的铸点域上,不是按兄弟帽类比**(异源对抗复审 r1 [medium] 真病修):本位初稿写的是
1166
- * `8`,理由「与成员帽同源同量级」—— 那是**类比不是取证**,而且判错了源。真实铸点域:
1167
- * · server 的执法帽是 `MAX_UNCOVERED_DETAIL_ROWS = **32**`(`@sema-agent/server`
1168
- * `src/approval-card.ts`:zod `.max(32)` 与座级预筛 `screenUncoveredDetailSeat` 同用);
1169
- * · 未覆盖段数**根本不受成员帽约束** —— core 逐字 `uncoveredSegments: uncoveredDetail.length`
1170
- * (`permission-rule-model.ts`),一只**只有 1 个成员**的 batch 可以带 9 条明细(9 个带重定向
1171
- * 的段各自 mint 不出规则形,进不了 `rules[]` 却每段都要给一个「为什么还会问」)。
1172
- * ⇒ 帽 8 是**比铸点域更窄**的窄读域,正是 [4050] 那条纪律禁的形:一只合法的 9~32 行明细座被整座
1173
- * 静默丢掉,人失去「这几段为什么还会问」。边界钉在门 ⑨d(9 / 32 必收、33 才丢)。
1174
- * 🔴 **取等于而不是再加倍**:与 {@link MAX_RULE_OFFERS_TOLERATED}(server 执法帽 4,本包留 8)不同 ——
1175
- * 那一位没有可锚的上游常量,余量是纯容忍;本位有**具名铸点常量**,取等于就是「窄读域等于铸点域」,
1176
- * 再自行加倍反而是又一次凭感觉编数。上游哪天放宽,门 ⑨d 的 33 行负控会当天红,逼人跟着抬。
1177
- */
1178
514
  const MAX_RULE_OFFER_UNCOVERED_DETAIL_TOLERATED = 32;
1179
- /**
1180
- * `ruleOffers`(server ≥7.46.0 的判别联合)的结构读。
1181
- *
1182
- * 🔴 **[C228]/L-103(0.57.0)起本口是公面**(additive 导出,语义与字节一字未改)。此前它只经
1183
- * **卡端口**({@link ApprovalCardRequest.ruleOffers})出包 —— 不走卡端口架构的宿主(浏览器端没有
1184
- * Ink 三选卡,自己拿帧渲)只能在自己那边**重铸一遍**同一把窄读器,而这把窄读器承载的是
1185
- * **兑付安全**判据(原始下标不前移、逐条丢坏、闭集 kind),重铸一次 = 多一份会各自漂的判官。
1186
- * ⇒ 公面出口是「判定归包、呈现归端」在这一条腿上的兑现,不是便利函数。
1187
- * 🔴 **两代 wire 键请走 {@link readRuleOfferSupply}**:本函数只读**新键**(server ≥7.46.0 的
1188
- * `ruleOffers`);退役键 `ruleSuggestions`(server ≤7.45)的归一在那一口,两键的取舍序也在那里
1189
- * (新键在场即定局,绝不混编)。手里只有新键才直接用本口。
1190
- * 🔴 **`offerIndex` 的定义域随腿不同**,消费前必读 {@link RuleOffer} 顶注:活卡帧腿上它是合法
1191
- * **选择键**(可当 `persistRule.batchOfferIndex` 回兑),durable 行腿上它只是展示/对账座
1192
- * (server `boundedRuleOffers` 已压紧过一次)——**本函数不知道调用方在哪条腿上**,分辨是调用方的事。
1193
- *
1194
- * 🔴 **逐条丢坏、原始下标不前移**:坏 offer 逐条丢弃(一条坏的不该让另一条真的消失,与 server
1195
- * `boundedRuleOffers` 同向),但留下来的每一条都带**原始 wire 下标** {@link RuleOffer.offerIndex} ——
1196
- * core 的契约原话:做不到保住原始下标的消费端「must suppress its persistence actions entirely」,
1197
- * 因为 `batch` 臂的兑付键就是下标。压紧 = 人点的第 k 个与服务端兑的第 k 个指向两条不同规则。
1198
- * 🔴 `kind` 是**闭集判别位**:不认识的 kind ⇒ 丢这一条(不猜、不降级成 single)。
1199
- * 全部不合形/非数组/超帽 ⇒ 整体缺席(卡不渲「不再询问」档)。
1200
- */
1201
515
  export function readRuleOffers(v) {
1202
516
  if (!Array.isArray(v))
1203
517
  return undefined;
@@ -1221,19 +535,12 @@ export function readRuleOffers(v) {
1221
535
  continue;
1222
536
  if (o.rules.length === 0 || o.rules.length > MAX_RULE_OFFER_BATCH_MEMBERS_TOLERATED)
1223
537
  continue;
1224
- // `uncoveredSegments` 是**诚实余量披露**,不是装饰位:非有限非负整数 ⇒ 整条 batch 不合形。
1225
- // 渲一个编出来的 0 等于对用户说「兑完这批就全覆盖了」,而那句话没有证据
1226
- // ([honest-absence-not-fabricated-zero])。
1227
538
  const uncovered = o.uncoveredSegments;
1228
539
  if (typeof uncovered !== 'number' || !Number.isSafeInteger(uncovered) || uncovered < 0)
1229
540
  continue;
1230
541
  const members = [];
1231
542
  let memberShapeOk = true;
1232
543
  for (const raw of o.rules) {
1233
- // 🔴 batch 成员**不逐条丢**:合取批是「对全体成员一次说是」,少一条成员就是另一次授权 ——
1234
- // 渲一个缺了成员的批,人点的与引擎兑的不是同一批规则。任一成员不合形(含 `kind` 不识)
1235
- // ⇒ 整条 batch 丢。这是 core design/382 §2.3 的**规范性**降级臂,不是本包的保守选择:
1236
- // 逐字 "drops the WHOLE batch offer — never the single member ... and never the whole card"。
1237
544
  const member = readRuleOfferBatchMember(raw);
1238
545
  if (member === undefined) {
1239
546
  memberShapeOk = false;
@@ -1243,7 +550,6 @@ export function readRuleOffers(v) {
1243
550
  }
1244
551
  if (!memberShapeOk)
1245
552
  continue;
1246
- // additive 明细座(0.58.0):只丢坏行、读空即缺席,**绝不**因为它否决整只 batch。
1247
553
  const detail = readUncoveredDetail(item.uncoveredDetail);
1248
554
  out.push({
1249
555
  kind: 'batch',
@@ -1254,16 +560,9 @@ export function readRuleOffers(v) {
1254
560
  });
1255
561
  continue;
1256
562
  }
1257
- // 不认识的 kind(含 kind 缺席)⇒ 丢这一条,原始下标照旧不给后面的成员用。
1258
563
  }
1259
564
  return out.length > 0 ? out : undefined;
1260
565
  }
1261
- /**
1262
- * 退役键 `ruleSuggestions`(server ≤7.45)的**归一读** —— 每一项都是「整条命令一条规则」,
1263
- * 逐字对应新形的 `kind:'single'` 臂。7.44 及更旧引擎仍在场,所以这条兼容臂不是包袱。
1264
- * 🔴 归一后仍带原始下标:旧形没有 batch 臂 ⇒ 下标不会被当选择键用,但保住它让两代素材在
1265
- * 卡面/对账面上是**同一个形**(消费端零分支)。
1266
- */
1267
566
  function readLegacyRuleSuggestions(v) {
1268
567
  if (!Array.isArray(v))
1269
568
  return undefined;
@@ -1278,43 +577,11 @@ function readLegacyRuleSuggestions(v) {
1278
577
  }
1279
578
  return out.length > 0 ? out : undefined;
1280
579
  }
1281
- /**
1282
- * 两代 wire 供给 → **包内单一形**(#334/[5223],0.43.0):新键优先,新键整只读不出来才看旧键。
1283
- *
1284
- * 🔴 **[C228]/L-103(0.57.0)起本口是公面**(additive 导出,语义与字节一字未改;0.56.0 及更早的
1285
- * 内部名是 `readOfferSupply`,**纯改名**没有第二个消费点)。宿主手里拿到的是**一整帧/一整行**,
1286
- * 上面同时可能有 `ruleOffers`(新)与 `ruleSuggestions`(旧)两个键 —— 这一口是三端唯一该调的那个:
1287
- * `readRuleOfferSupply(frame.ruleOffers, frame.ruleSuggestions)`。
1288
- * 两代键的取舍序是**判据不是便利**(见下面两段红条),端各写一遍必然在 `null` 那一格上各错一遍。
1289
- *
1290
- * 🔴 **新键在场即定局,绝不混编**:新键**有载体**而读出空(数组在但全条坏形、或压根不是数组)
1291
- * 也**不**回落旧键 —— 一台 7.46 引擎不会同时按两代形铸候选,拿旧键顶上去等于把一份异源素材
1292
- * 冒充成这次 ask 的候选。
1293
- *
1294
- * 🔴 **`null` 与 `undefined` 同视为「新键缺席」**(异源对抗复审 [medium] 追问后的**明示裁定**,
1295
- * 不是漏判):判据是**代价不对称**——
1296
- * · 认 null 为缺席的失效面:一台 **7.46** 引擎把新键发成 `null` **且**同时发了旧键。这不可能
1297
- * 发生:7.46 的三腿上旧键一个字都不铸(engine fixture 直证,同文件 0 命中)⇒ 回落读到的
1298
- * 只会是 `undefined`,结果与「整只缺席」逐字节相同,没有异源素材可混;
1299
- * · 认 null 为坏形的失效面:任何把「缺席」序列化成 `null` 的中转层(JSON 规范化包装器、
1300
- * 某些 SQL/JSONB 读面、mock)会让**所有 ≤7.45 引擎**的「不再询问」档整段消失 —— 那正是
1301
- * [5223] 这一批要修的病本身,只是换了个触发条件。
1302
- * ⇒ 取「null == 缺席」。安全面上它**不新增**任何攻击面:一个能塞 `{ruleOffers:null, ruleSuggestions:[…]}`
1303
- * 的注入面,同样能只塞 `{ruleSuggestions:[…]}`,而后者为了兼容 7.44 本来就必须收。
1304
- */
1305
580
  export function readRuleOfferSupply(offers, legacy) {
1306
- // 🔴 `null` 与 `undefined` 同视为缺席(理由见顶注的代价不对称段);其余一切载体 = 新键在场,
1307
- // 读出什么就是什么,**绝不**再看旧键。
1308
581
  if (offers !== undefined && offers !== null)
1309
582
  return readRuleOffers(offers);
1310
583
  return readLegacyRuleSuggestions(legacy);
1311
584
  }
1312
- /** 子代审批帧的归属徽章([1535] cli 半场):名一手源=帧上 sourceAgentName(core 1.378 RB-39②
1313
- * 显式展示身份,server redact 后透传,候 server 班车到货即亮);缺席退 fleet 在飞台账行名
1314
- * (`${agentType}: ${description}` 合成形;⚠️ sourceTaskId 实测=子代 sessionId 非 a… handle,
1315
- * 台账按 handle 键常 miss);再退 sourceTaskId 短形——徽章永在场,归属永不静默。截 32c 防破行。
1316
- * 🔴 搬迁差分:cli 用延迟 require 拉 `fleetClient.js`(理由=纯 wire 模块不背 fleet 图冷启动);
1317
- * 在包内 fleet 台账就是同一个包的模块,改静态 import。try/catch 保留(台账不可用的兜底那一支)。 */
1318
585
  function subagentBadgeFor(frame) {
1319
586
  let name = typeof frame.sourceAgentName === 'string' && frame.sourceAgentName.length > 0 ? frame.sourceAgentName : '';
1320
587
  if (!name && typeof frame.sourceTaskId === 'string' && frame.sourceTaskId.length > 0) {
@@ -1325,7 +592,6 @@ function subagentBadgeFor(frame) {
1325
592
  name = row.name;
1326
593
  }
1327
594
  catch {
1328
- /* 台账不可用 → taskId 短形兜底 */
1329
595
  }
1330
596
  }
1331
597
  if (!name)
@@ -1334,21 +600,9 @@ function subagentBadgeFor(frame) {
1334
600
  name = `${name.slice(0, 31)}…`;
1335
601
  return { name, color: 'cyan' };
1336
602
  }
1337
- /**
1338
- * 消费一张 `tool_approval` 帧:弹三选卡 → respond。引擎此刻同步阻塞在本帧上(fail-closed 全腿:
1339
- * TTL 5min/abort/emit 失败⇒deny),所以本函数 await 卡决断是安全的(流上不会再来别的内容帧)。
1340
- *
1341
- * fail-closed:卡面不可用(print 模式无卡口/enqueue 失败)⇒ 立刻 respond deny,不让引擎
1342
- * 干等 TTL。respond 自身失败(404=已过期/settled)⇒ 'unresolved'(诚实放行后续帧,引擎已自决)。
1343
- *
1344
- * @returns {@link ToolApprovalFrameOutcome} —— 决断 + server ack(ack 缺席 = 未知,不是 false)。
1345
- */
1346
- /** respond `note` 位的 server 上限(server `MAX_DECISION_NOTE_CHARS` = 2048,与 durable
1347
- * `AskDecisionBody.note` 同一列;超限 server **响亮 400 且连决断一起拒**,所以本包发送前有界)。 */
1348
603
  const MAX_RESPOND_NOTE_CHARS = 2048;
1349
604
  export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamArgs, signal, lane) {
1350
605
  const toolName = typeof frame.toolName === 'string' ? frame.toolName : 'Write';
1351
- // 工具入参:流上 tool_start.args(完整一手源)> 帧 args(redacted+16KiB 帽)> message 兜 path。
1352
606
  let args = streamArgs !== undefined && streamArgs !== null
1353
607
  ? streamArgs
1354
608
  : frame.args !== undefined && frame.args !== null
@@ -1356,9 +610,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1356
610
  : undefined;
1357
611
  let wireNote;
1358
612
  if (typeof args !== 'object' || args === null) {
1359
- // [1543]③:args>16KiB 超帽时 server 出帧带 argsOmitted:true 而无 args。卡面提示走
1360
- // ToolUseConfirm.wireNote 超集位(2026-07-23 落位,清 [1543]③ 记账)——dim 行不进 args,
1361
- // 不会随「编辑并批准」的 updatedInput 转发污染工具入参。
1362
613
  if (frame.argsOmitted === true) {
1363
614
  wireNote = 'tool arguments exceeded the wire cap and were omitted — the diff below is reconstructed from the gate message, not the full payload';
1364
615
  hostLog('debug', `liveToolApprovalWire: frame ${frame.approvalId} args omitted (>16KiB wire cap) — card falls back to message-derived path`);
@@ -1372,26 +623,12 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1372
623
  const p = pathFromGateMessage(frame.message);
1373
624
  args = p !== undefined ? { file_path: p } : {};
1374
625
  }
1375
- // [1535]:子代的写审批浮到宿主卡(server 1.258 broker 腿)——卡头渲归属徽章(`· @name`),
1376
- // 用户知道在替谁批。判别=fromSubagent 显式键(core 1.378)优先、sourceTaskId 在场性权宜式兜底;
1377
- // 宿主自身 ask 两键恒缺席=徽章缺席,卡形不变。
1378
- // 🔴 治理位与委派链透传(2026-08-08):两键在帧上**只在为真/在场时**出现,所以这里也按在场性
1379
- // 条件 stamp —— 绝不 `governanceForced: frame.governanceForced === true`(那会把「缺席」折成
1380
- // 显式 `false`,而缺席的语义是「没有治理来源的证据」,不是「这门可以被表态掀掉」)。
1381
- // #334(0.43.0):新键 `ruleOffers`(server ≥7.46.0 判别联合)优先,旧键 `ruleSuggestions`
1382
- // (≤7.45)归一成 `kind:'single'` 兜底 —— 两代经同一把窄读器,包内出口只有一个形。
1383
626
  const ruleOffers = readRuleOfferSupply(frame.ruleOffers, frame.ruleSuggestions);
1384
- // S-114(0.59.0):限额回落卡的四成员窄读**算一次**(读器是纯函数,但两次调用会让「上卡的那一份
1385
- // 与判在场的那一份是不是同一个对象」在读代码时需要推理 —— 单点求值把它变成显然的)。
1386
627
  const denialLimitFallback = readDenialLimitFallback(frame.denialLimitFallback);
1387
628
  const card = await surfaceApprovalCard({
1388
629
  toolName,
1389
630
  args: args,
1390
- // A-028.3:live 帧腿的卡身份键经 gateIdentity 唯一铸口(帧上无 gatedCallId,以 approvalId 铸)。
1391
631
  callKey: liveFrameCallKey(frame.approvalId),
1392
- // #348(0.44.0):帧上的 core tool-call id 透传到卡口 —— 卡与 transcript 里 `tool_use` 块之间唯一的
1393
- // wire 事实锚。**非空串才 stamp**(空串是坏值不是「没有调用」,同文件既有窄读同族纪律);
1394
- // 缺席 ⇒ 键缺席,宿主按缺席降级(绝不拿 approvalId 冒充,那正是 #348 的病形)。
1395
632
  ...(typeof frame.toolCallId === 'string' && frame.toolCallId !== ''
1396
633
  ? { toolCallId: frame.toolCallId }
1397
634
  : {}),
@@ -1400,16 +637,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1400
637
  ...(wireNote !== undefined ? { wireNote } : {}),
1401
638
  ...(lane?.argsUnavailable === true ? { argsUnavailable: true } : {}),
1402
639
  ...(frame.governanceForced === true ? { governanceForced: true } : {}),
1403
- // [4851]:窗三键透传,三闸合取(异源对抗复审 P1/P2 收编):
1404
- // ① `lane.windowIsCurrent === true` —— durable 账本重放的历史帧带的是铸帧时刻的旧余量,
1405
- // 给它续窗 = 过期 ask 渲出精确假倒计时(壳孪生族同名闸的本包席位,见 LaneOpts 注);
1406
- // ② **非负有限数**才 stamp —— `Number.isFinite` 单独放行负数,而三键定义域按 SDK 契约
1407
- // 本就非负(`expiresInMs = max(0, …)`;0 合法 = 窗恰好到点)。负数/NaN/Infinity =
1408
- // 坏形降缺席,绝不折 0 或猜(同文件 delegation.depth 曾同款教训);
1409
- // ③ 三键各自独立 stamp:server 承诺同门在场,但包边界不替上游把「半套在场」湮灭成全缺席
1410
- // —— 读器自己按「expiresAtMs 优先、expiresInMs 兜底」消化半套。
1411
- // 任一键 stamp 时同步盖 `receivedAtMs`(本地收帧锚):卡口串行阻塞下,排队靠后的卡若按
1412
- // 「呈现那一刻」起算余量,窗被整段还回去 —— 壳读器明文优先这枚锚。
1413
640
  ...(lane?.windowIsCurrent === true
1414
641
  ? {
1415
642
  ...(isNonNegativeFinite(frame.expiresInMs) ? { expiresInMs: frame.expiresInMs } : {}),
@@ -1420,81 +647,30 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1420
647
  : {}),
1421
648
  }
1422
649
  : {}),
1423
- // 🔴 跨字段不变量(对抗复审命中):`delegation` 只在**子代 ask** 的门上在场(SDK 头注:
1424
- // "Present on the same door as fromSubagent (child asks only)")。只校对象形不校这一条 ⇒
1425
- // 一个**宿主自己**的破坏性工具审批也能带着 delegation 到卡口,被呈成「某层子代发起的」——
1426
- // 把归属这件安全展示位渲错,正是本键存在的反面。`isToolApprovalFrame` 只验 type/approvalId,
1427
- // 所以 raw SSE / durable 回放里的坏形到得了这里,必须在本层挡住。
1428
650
  ...(isFromSubagent(frame) && isToolApprovalDelegation(frame.delegation)
1429
651
  ? { delegation: frame.delegation }
1430
652
  : {}),
1431
- // #225 件1 / #334 换形(0.43.0):规则候选透传(合形项;缺席/坏形 ⇒ 键不 stamp,卡形不渲该档)。
1432
- // 落位是**包内单一形出口** `ruleOffers`,两代 wire 键同经 readRuleOfferSupply 归一。
1433
653
  ...(ruleOffers !== undefined ? { ruleOffers } : {}),
1434
- // #144:被越级的持久规则原文(UNTRUSTED-for-display)。窄化=**非空白串才 stamp**,坏形降缺席
1435
- // (server 明说空串不铸键:「空串是坏值不是『空规则』」)—— 一格空白的规则解释比没有解释更坏。
1436
- // 🔴 stamp 的是**原文字节**(不 trim、不截、不改写):展示值一改写就不再是「你那条规则」的原文;
1437
- // 渲染面的长度夹取归壳,与 `message`/`sourceAgentName` 同族口径(本模块对展示文本零加工)。
1438
654
  ...(typeof frame.persistedRuleShadowed === 'string' && frame.persistedRuleShadowed.trim() !== ''
1439
655
  ? { persistedRuleShadowed: frame.persistedRuleShadowed }
1440
656
  : {}),
1441
- // #280 件1(0.30.4):探针因由随卡透传 —— 载体形判之外零校验零改写(内部结构归呈卡端窄读器,
1442
- // isWireRecordCarrier 顶注);缺席/坏载体 ⇒ 键不 stamp,卡形字节不变。
1443
657
  ...(isWireRecordCarrier(frame.probeCause) ? { probeCause: frame.probeCause } : {}),
1444
- // #281 件4b(0.30.8):规则出处证据随卡透传 —— 同上,只判载体形、**成员级一概不校**
1445
- // (值 ⊕ 具名缺席词、未知缺席词、矛盾形一律原样上卡:判决权单源在端侧窄读器,
1446
- // 且端侧窄读域只许 ≥ 铸点域)。缺席/坏载体 ⇒ 键不 stamp,卡形与 0.30.7 字节不变。
1447
- // 🔴 呈现谓词归端(无治理部署下每只 ask 都带全具名缺席,[4050] live 实测 / [4051] 定性);
1448
- // 本层若替端做「全 not_wired 就别给了」的过滤,就是替引擎把一条真事实湮灭掉。
1449
658
  ...(isWireRecordCarrier(frame.ruleEvidence) ? { ruleEvidence: frame.ruleEvidence } : {}),
1450
- // #283(0.36.0):安全类 ask 出身位透传([4390]③ 请托的正位解 —— 壳侧读器早已预埋,缺的一直是
1451
- // 包边界这一格)。条件 stamp **只认严格 true**,与 governanceForced 同一条纪律:缺席的语义是
1452
- // 「这不是一次安全类 ask」,把它折成显式 false 会让宿主把「没有证据」读成「已确认可自动放行」,
1453
- // 而本键存在的全部理由正是在这种门上把自动放行(记住的规则 / allow_session / bypass 姿态)拦下。
1454
659
  ...(frame.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1455
- // CC-27(0.71.1):读根候选透传 —— 窄读器铸新两键形(dir 非空串 ∧ clearsThisAsk === true),坏形/缺席 ⇒ 键不置。
1456
660
  ...(() => { const rc = readReadRootCandidate(frame.readRootCandidate); return rc !== undefined ? { readRootCandidate: rc } : {}; })(),
1457
- // #341/[5214]③(0.43.0):bidi 披露位透传。条件 stamp **只认严格 true**,与 governanceForced /
1458
- // requiresRealApproval 同一条纪律 —— 缺席同时覆盖「真的没有」与「扫不了」,折成显式 false 会把
1459
- // 「没扫到」渲成「已确认干净」,而这一位存在的全部理由就是「人眼读到的顺序 ≠ 真跑的字节顺序」。
1460
- // 🔴 披露不清洗:args 字节在本层一个都不改(清洗会让卡上显示的与真跑的不是同一个东西)。
1461
661
  ...(frame.inputHasBidi === true ? { inputHasBidi: true } : {}),
1462
- // ── S-15 第五单(0.59.0):报价缺席因由透传 ────────────────────────────────────────────────
1463
- // **非空串才 stamp**,词表**不校**(开集;server 已按闭集拒过词表外的值,包再校一遍只会在
1464
- // core 加员当天把合法值判没 —— `autoMode.reason` 的透传纪律逐字同规)。缺席/坏形 ⇒ 键不 stamp。
1465
- // 🔴 与 `ruleOffers` 引擎侧互斥,但本层**不据此互删**:两者同时在场是上游坏形,原样上卡让端
1466
- // 看得见,包替上游把矛盾湮灭掉只会让排障说不清是谁窄没的。
1467
662
  ...(typeof frame.ruleOffersAbsence === 'string' && frame.ruleOffersAbsence !== ''
1468
663
  ? { ruleOffersAbsence: frame.ruleOffersAbsence }
1469
664
  : {}),
1470
- // ── S-114(0.59.0):auto 分类器限额回落卡透传 ──────────────────────────────────────────────
1471
- // 经 `readDenialLimitFallback` 四成员窄读(缺一整只丢;判据本体在该函数顶注)。
1472
- // 🔴 stamp 的是**窄读产物**而不是原对象:端拿到的四座恒是有限非负数 + 非空 limit 串,
1473
- // 不必各自再写一遍同样的判断(三端各写一遍 = 三份会漂的判官)。
1474
665
  ...(denialLimitFallback !== undefined ? { denialLimitFallback } : {}),
1475
- // ── 0.67.0(core 7.14.0 #688 C3):「规则店读不出来是哪一头」的事实透传 ────────────────────
1476
- // 经 `readRuleStoreUnreadable` 单点窄读(判据本体在该函数顶注:**闭二词**、表外/坏形降缺席)。
1477
- // 与 durable 腿**同一把读器、同一个卡位**(键路同形 ⇒ 端一把读器吃两条腿)。
1478
- // 🔴 缺席 ⇒ 键不 stamp,**绝不折成任何肯定话**:缺席同时覆盖「店和这次调用都读通了」与
1479
- // 「本部署没接规则店」两形,端禁读成「你的规则都好着呢」。
1480
- // ⚠️ 它**换掉**的是上一代同位的 `classifierUnavailable`(core 7.14.0 整族退役;事实位随拒绝面的
1481
- // `GateDisposition.denied.cause` 走)—— 那一格按 clean-cut 删码不留别名。
1482
666
  ...((k) => (k !== undefined ? { ruleStoreUnreadable: k } : {}))(readRuleStoreUnreadable(frame.ruleStoreUnreadable)),
1483
- // ── S-125③/#564(0.59.0):ask 出身透传 ────────────────────────────────────────────────────
1484
- // 非空串才 stamp,词表**不校**(同 ruleOffersAbsence)。🔴 缺席 ⇒ 键不 stamp,**绝不折成
1485
- // `"policy"`**:那是一个正面事实(出自部署 ToolPolicy),把「老引擎没报」折进去 = 替引擎编话。
1486
667
  ...(typeof frame.origin === 'string' && frame.origin !== '' ? { origin: frame.origin } : {}),
1487
668
  });
1488
- // 0.72.14:入参不可得的卡上收到「编辑后批准」⇒ **什么都不发**。丢掉改写再发 allow = 批准了人没看到、也不是他改成的那份
1489
- // 原始入参;把改写转发出去 = 拿卡上那个空对象替换工具的真实入参。两条都不是人按下的那个决定 ⇒ 这只 ask 保持悬挂,
1490
- // 结局如实标 `editRefused`,由端重新出卡(异源对抗复审 [high])。
1491
669
  if (lane?.argsUnavailable === true && card.kind === 'allow' && card.updatedInput !== undefined) {
1492
670
  hostLog('error', `liveToolApprovalWire: ${frame.approvalId} card returned an edited approval on an args-unavailable ask — nothing sent (the ask stays pending)`);
1493
671
  surfaceEditRefusedOnBlindAsk();
1494
672
  return { decision: 'unresolved', editRefused: true };
1495
673
  }
1496
- // 0.74.1(CC-67):宿主撤卡(无决断收口)⇒ **什么都不发**。这张卡已经没有决断口(换会话 / 换引擎 / 跟踪器报 gone),
1497
- // 替用户 POST 一次 deny 会把悬挂的子代 ask 误拒。结局如实标 `retracted`,ask 在引擎上照旧挂着。
1498
674
  if (card.kind === RETRACTED_CARD_DECISION_KIND) {
1499
675
  hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} card retracted without a decision${card.reason ? ` (${card.reason})` : ''} — nothing sent`);
1500
676
  return { decision: 'unresolved', retracted: true };
@@ -1503,16 +679,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1503
679
  if (card.kind === 'failed') {
1504
680
  hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${frame.approvalId}`);
1505
681
  }
1506
- // #229 兑现(0.29.0 respond-note 批;此前 0.28.0 记账「候 server」,server 7.15.0 已到货):
1507
- // respond 端点收 **`note`**(不是 reason —— 与 durable `AskDecisionBody.note` 同词同源同一列
1508
- // `decision_note`),任何 decision 都可带。deny reason 经它上 wire,发送前置两道门:
1509
- // · 能力位(`lane.approvalDecisionNoteCapable === true`)—— SDK 6.16 成文「位缺席就别发」:
1510
- // 老 server 静默忽略未知键照回 200,「没听懂」与「记上了」不可分,发了只造「已留档」错觉;
1511
- // · 上限(≤{@link MAX_RESPOND_NOTE_CHARS}=2048)—— server 超限**响亮 400 且连决断一起拒**,
1512
- // 而备注是决断的补充面,绝不许它把决断本身打失败。超限 ⇒ **整条不发** + 留痕
1513
- // (诚实缺席优先于静默截断 —— 「一条被悄悄砍半的审计理由比没有理由更坏」,server 同款立场)。
1514
- // 🔴 留痕纪律不变:只写**元数据**(在场 + 长度 + 没发的原因),绝不把正文拷进日志 ——
1515
- // 正文是宿主自己卡口返回的自由文本,回喂宿主 sink 零诊断增量,还是外溢/注入面。
1516
682
  let note;
1517
683
  if (card.kind === 'deny' && typeof card.reason === 'string' && card.reason.trim() !== '') {
1518
684
  if (lane?.approvalDecisionNoteCapable !== true) {
@@ -1530,38 +696,11 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1530
696
  }
1531
697
  }
1532
698
  try {
1533
- // abort 后的 deny 仍要送达(引擎侧同款 cancel-by-deny)——不带已 aborted 的 signal。
1534
- // #225 件1:卡决断携带的 persistRule 只在「确是帧候选之一的原文」时上 wire —— server 本就
1535
- // 按表拒(rule_not_offered),这道核对把坏卡口实现挡在包边界并留痕(丢键不丢决断:人的
1536
- // allow/deny 已经落定,一条对不上的规则文本不该反过来污染整次回决)。deny 恒不带(server
1537
- // 宽收后忽略,发了只留「像是记住了」的错觉)。
1538
- //
1539
- // ── #225 编辑臂(0.42.0,[5071] wire 形):自由文本时**表核让位**,但能力位缺席就别发 ────────
1540
- // 🔴 让位的判据锚在**出身声明** `card.persistRuleEdited === true`,不锚「文本恰好不在表里」——
1541
- // 后者会让一个坏卡口实现(拼错了候选原文)自动升级成「自由文本臂」,把这道核对彻底架空。
1542
- // 🔴 让位的**理由**:表核的全部意义是「绝不把没被 offer 过的文本送进规则店」,而编辑臂的定义
1543
- // 就是人自己打的文本 —— 对它做表核,等于把这条 wire 上刚开出来的能力在包边界原样关掉
1544
- // ([5111] 探针实测的闭集三处之②:自由文本恒丢键)。真判官在引擎侧同一函数体
1545
- // (core `confirmRuleApproval`),不在这里。
1546
- // 🔴 能力位闸(`lane.respondFreeFormRulesCapable`)与 note 那条**同一条纪律**:位缺席就别发。
1547
- // 编辑臂在老 server 上会诚实降级 `rule_not_offered`(不是静默 200),但那仍然是一次
1548
- // 「人以为存上了、其实拿回一个拒绝」的往返 —— 能提前知道这台不供,就别把人的编辑送出去。
1549
- //
1550
- // ── #334 批臂(0.43.0,design/377 wire 形):选择键=**原始 wire 下标**,表核锚「该下标真是
1551
- // 一条 batch offer」──────────────────────────────────────────────────────────────────
1552
- // 🔴 **三臂互斥先判**(server `PERSIST_RULE_BATCH_EXCLUSIVE_ERROR`:同场响亮 400 且**连决断一起拒**)。
1553
- // 包边界丢整条持久臂而不是让 400 打掉决断 —— 与 note 超限那条同一条纪律:附带愿望的失败
1554
- // 绝不许升级成主动作的失败。也绝不替人挑一个臂(两臂说的是两次不同的授权)。
1555
699
  let persistRule;
1556
700
  let persistRuleEdited;
1557
701
  let persistRuleBatchOfferIndex;
1558
- /** 真发出去的那条 batch offer(供下方 ack 的**基数**相关性核对;缺席 = 没发批臂)。 */
1559
702
  let batchArmTarget;
1560
- /** 人选了持久臂、而**能力位没确认**所以整条没发(两臂共用)。通知发在 respond 成功之后。 */
1561
703
  let ruleArmDroppedForCaps = false;
1562
- /** 人选了持久臂、而那次选择**没过包内表核/互斥核**(表外文本 / 坏下标 / 两臂同场)。
1563
- * 与上一位**刻意分两个标记**:对用户的下一步建议不同(见两条文案常量的顶注)。同样发在
1564
- * respond 成功之后 —— 文案里有「审批本身过了」。 */
1565
704
  let ruleArmDroppedForCheck = false;
1566
705
  const wantsTextArm = card.kind === 'allow' && typeof card.persistRule === 'string' && card.persistRule !== '';
1567
706
  const wantsBatchArm = card.kind === 'allow' && card.persistRuleBatchOfferIndex !== undefined;
@@ -1569,24 +708,16 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1569
708
  hostLog('error', `liveToolApprovalWire: DROPPING the whole persistRule arm for ${frame.approvalId} — the card returned BOTH a rule ` +
1570
709
  'text and a batchOfferIndex; the server rejects that combination with a 400 that would take the decision down ' +
1571
710
  'with it, and picking one arm here would be deciding on the user\'s behalf (the decision itself still goes through)');
1572
- // 🔴 十轮 [medium]:人的明确选择被丢了,`hostLog` 那条是给开发者的诊断,用户还需要一句答复。
1573
711
  ruleArmDroppedForCheck = true;
1574
712
  }
1575
713
  else if (card.kind === 'allow' &&
1576
714
  card.persistRuleBatchOfferIndex !== undefined &&
1577
715
  decision !== 'deny') {
1578
- // 🔴 表核:下标必须在**本帧**归一后的 offers 里真指向一条 `kind:'batch'`。
1579
- // 越界 / 指向 single / 负数 / 非整数一律丢键留痕 —— server 对坏下标响亮 400
1580
- // (`persistRule.batchOfferIndex must be a non-negative integer when present`),
1581
- // 而一个被 `|0` 折过的下标指向的是**另一条** offer。
1582
716
  const idx = card.persistRuleBatchOfferIndex;
1583
717
  const target = Number.isSafeInteger(idx) && idx >= 0
1584
718
  ? ruleOffers?.find(o => o.offerIndex === idx)
1585
719
  : undefined;
1586
720
  if (target !== undefined && target.kind === 'batch') {
1587
- // 🔴 能力位闸(异源对抗复审 [high]):位缺席就别发 —— 见 `respondBatchRuleOffersCapable`
1588
- // 顶注。「帧上有 batch offer」只证明出帧端认识新形,不证明收 respond 的那台认识它;
1589
- // 旧解析器对本键响亮 400 且**连决断一起拒**。
1590
721
  if (lane?.respondBatchRuleOffersCapable === true) {
1591
722
  persistRuleBatchOfferIndex = idx;
1592
723
  batchArmTarget = target;
@@ -1595,10 +726,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1595
726
  hostLog('debug', `liveToolApprovalWire: DROPPING persistRuleBatchOfferIndex(${idx}) for ${frame.approvalId} — the ` +
1596
727
  'respondBatchRuleOffers capability is not confirmed for this engine, so the batch arm is not sent at all ' +
1597
728
  '(the decision itself still goes through unchanged; the offer stays on the card for local/display use)');
1598
- // 🔴 **人按下的意图被丢了就必须上屏**(异源对抗复审三轮 [high]):只记 debug =
1599
- // 「用户明确选了『不再询问』,系统静默丢掉,下次照旧弹卡」。见 surfaceRuleArmNotSent 顶注。
1600
- // ⚠️ 但**不在这里发** —— 文案里有「审批本身过了」,而此刻 respond 还没送出去(四轮 [medium]:
1601
- // respond 随后抛错 ⇒ 决断没落定,用户却已读到「过了」)。只置标记,发在成功之后。
1602
729
  ruleArmDroppedForCaps = true;
1603
730
  }
1604
731
  }
@@ -1606,7 +733,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1606
733
  hostLog('error', `liveToolApprovalWire: DROPPING persistRuleBatchOfferIndex(${String(idx)}) for ${frame.approvalId} — that wire ` +
1607
734
  'index is not a batch offer on this frame (out of range, a single offer, or not a non-negative integer); ' +
1608
735
  'never redeeming an index the engine did not offer as a batch');
1609
- ruleArmDroppedForCheck = true; // 见上:表核丢键同样要给用户一句答复
736
+ ruleArmDroppedForCheck = true;
1610
737
  }
1611
738
  }
1612
739
  else if (wantsTextArm && card.kind === 'allow' && typeof card.persistRule === 'string' && decision !== 'deny') {
@@ -1620,8 +747,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1620
747
  hostLog('debug', `liveToolApprovalWire: DROPPING edited persistRule for ${frame.approvalId} (len=${card.persistRule.length}) — the ` +
1621
748
  'respondFreeFormRules capability is not confirmed for this engine, so the free-form arm is not sent at all ' +
1622
749
  '(the decision itself still goes through unchanged)');
1623
- // 🔴 同形存量一并清(0.43.0):这一臂此前也只写 debug —— 与批臂同一个病,共用同一条通知,
1624
- // 也共用同一条时序契约(发在 respond 成功之后,理由见批臂那处)。
1625
750
  ruleArmDroppedForCaps = true;
1626
751
  }
1627
752
  }
@@ -1630,7 +755,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1630
755
  }
1631
756
  else {
1632
757
  hostLog('error', `liveToolApprovalWire: DROPPING persistRule for ${frame.approvalId} — the card returned a rule that is not among the frame's SINGLE offers (a batch member's text is not a selectable candidate either: a conjunction batch is redeemed by index, all-or-nothing) — never sending un-offered text to the rule store`);
1633
- ruleArmDroppedForCheck = true; // 同形存量一并清(0.26.0 起这条只写 error 日志)
758
+ ruleArmDroppedForCheck = true;
1634
759
  }
1635
760
  }
1636
761
  const raw = await respond(frame.approvalId, decision, {
@@ -1641,10 +766,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1641
766
  ...(persistRuleBatchOfferIndex !== undefined ? { persistRuleBatchOfferIndex } : {}),
1642
767
  ...(note !== undefined ? { note } : {}),
1643
768
  });
1644
- // 🔴 相关性门(对抗复审二轮):ack 必须是**这一次**审批的回执 —— id 与决断词都要对上。
1645
- // 对不上的 ack(注入面串了别人的响应 / 坏 mock / raw-fetch 包装器复用了连接)如果照样被
1646
- // 消费,一次普通审批就能弹出「你的编辑没生效、工具正在用原始入参跑」这种强安全声明。
1647
- // 不匹配 ⇒ 整个 ack 丢弃(降成「未知」)+ 响亮留痕,绝不据此触发任何用户告警。
1648
769
  const parsed = readToolApprovalRespondAck(raw);
1649
770
  let ack = parsed;
1650
771
  if (parsed !== undefined && (parsed.approvalId !== frame.approvalId || parsed.decision !== decision)) {
@@ -1653,47 +774,10 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1653
774
  'treating as "no ack" (unknown) — never surfacing a safety notice off an unrelated receipt');
1654
775
  ack = undefined;
1655
776
  }
1656
- // 🔴 **臂相关性门**(异源对抗复审 [high] 采纳,0.43.0):两位规范文本回显必须与**这一次真发出去
1657
- // 的持久臂**对得上 —— 没发持久臂却拿到「你的规则已存为 X」,或发的是文本臂却拿到一份批清单,
1658
- // 都是这份回执在说一次**没有发生过**的授权。端拿它渲的是强断言(「已存」+ 具体存了什么),
1659
- // 据一个对不上的回显去渲就是替引擎编一次授权。
1660
- // 🔴 **只丢这两位、不牵连整份 ack**(与上面那道 id/decision 相关性门刻意不同姿势):那道门管的是
1661
- // 「整份回执根本不是我的」,这道门管的是「回执是我的,但其中一格与我发出的臂矛盾」——
1662
- // 整份丢会把 `rememberApplied`/`updatedInputForwarded` 这两个更重的安全告知位一起连坐掉。
1663
777
  if (ack !== undefined &&
1664
778
  (ack.persistedRule !== undefined || ack.persistedRules !== undefined || ack.persistedRuleAnchors !== undefined)) {
1665
- // 🔴 单数回显**只属编辑臂**(异源对抗复审二轮 [medium] 采纳,server `editedArmEcho` 逐字:
1666
- // `persistRule.kind !== 'text' || !persistRule.edited || …` ⇒ 返 `{}`)——候选臂成功时
1667
- // server **不铸**这一位,所以候选臂上收到它同样是「一次没发生过的授权」。
1668
779
  const sentEditedArm = persistRule !== undefined && persistRuleEdited === true;
1669
- // 🔴 复数回显要过**基数**相关性:server 契约逐字「全体成员落地(persisted/deduped 都计)+
1670
- // `rules` = 规范文本,**展示序**(= batch offer 的成员序,壳逐条回显)」⇒ 一份合规回执的
1671
- // 条数恒等于所选 batch 的成员数。多报 = 声称存了用户在这只批里**没看见**的规则(授权范围
1672
- // 被放大);少报 = 这份清单不是它自称的「全体」。两向都不许当成「实际存了这些」渲上屏。
1673
- // ⚠️ 只做**基数**相关性,**不复判文本**:规范化是引擎的活(`Bash(adb *)` → `Bash(adb:*)`),
1674
- // 在这里比对文本就是装第二个判官。
1675
780
  const expectedBatchCount = batchArmTarget?.rules.length;
1676
- // 🔴 **P-39 归属锚门**(server ≥7.48.0,0.44.0):锚在场时,相关性从「基数」升级到「身份」——
1677
- // `offerIndex` 必须等于**这一次真发出去**的 `persistRuleBatchOfferIndex`,`memberIndex` 必须
1678
- // 落在所选 offer 的成员表范围内。这正是 P-39 登记里基数门**抓不到**的那一形:
1679
- // 「条数对,但这份回执说的是另一只 offer 的规则」。
1680
- // ⚠️ 锚**缺席**(≤7.47 引擎)⇒ 本门整条让位,回落既有基数门 —— additive 位不许把老引擎判红。
1681
- // 🔴 **absent / valid / invalid 必须是三态,不是两态**(异源对抗复审 [high] 采纳,本批):
1682
- // 读器把「在场但坏形」也降成 `undefined`,若这里只看 `ack.persistedRuleAnchors === undefined`
1683
- // 就把它与「老引擎真没这个键」混成一格 ⇒ 让位基数门。于是**「另一只 offer + 规则条数恰好相同
1684
- // + 故意发一份坏锚」这条回执可以精确绕过本门刚加的身份校验** —— 攻击面正是本门要封的那一形。
1685
- // ⇒ 判据取**wire 上键在不在**(raw),不取窄化产物在不在:
1686
- // · 键真缺席 ⇒ 让位基数门(老引擎兼容);
1687
- // · 键在场 ∧ 合形 ⇒ 走身份门;
1688
- // · 键在场 ∧ 坏形 ⇒ **不许让位**,连同 `persistedRules` 一起丢(一份自称带归属却给不出
1689
- // 合法归属的回执,不配驱动任何「已存了什么」的强断言)。
1690
- // (结构视图读:SDK 的 respond 返回型上没有这个 additive 键,与同文件 `pending.ruleOffers`
1691
- // / `riskDescriptor.probeCause` 同款姿势 —— 不引入新的宽 cast。)
1692
- // 🔴 判据是**键在不在**(`hasOwnProperty`),**不是**「值是不是 `undefined`」(异源对抗复审
1693
- // 二轮 [high] 采纳):注入面是**宿主给的 JS 函数**,不是 `JSON.parse` 的产物 —— 一个自铸
1694
- // `{persistedRuleAnchors: undefined}` 的回执在 JS 里**键是在场的**,用值判会把它读成
1695
- // 「老引擎真没这个键」而让位基数门,恰好绕过身份门。同仓 `engineCapState` 用 `in` 而不用
1696
- // `!== undefined` 的成文理由与此同源(那处头注:「本表也接宿主注入的对象」)。
1697
781
  const anchorKeyOnWire = typeof raw === 'object' &&
1698
782
  raw !== null &&
1699
783
  Object.prototype.hasOwnProperty.call(raw, 'persistedRuleAnchors');
@@ -1734,36 +818,18 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1734
818
  };
1735
819
  }
1736
820
  }
1737
- // ── 🔴 **同一次 respond 的通知顺序契约**(异源对抗复审十一轮 [high]:同一次 allow 可以同时
1738
- // 触发多条通知,而 `HitlHostSurface.showNotice` 的语义是**直接顶替 current、不排队** ⇒
1739
- // **最后发的那条才是用户真正看得到的那条**)。所以三条按**严重度升序**发,最重的压轴:
1740
- // ① 规则没存(本次「不再询问」丢了)—— 下次还会问,不影响这次跑的是什么;
1741
- // ② allow_session 没记住 —— 同族,范围更大一点;
1742
- // ③ 编辑没转发(**工具正在用原始入参跑**)—— 批的那份 ≠ 跑的那份,唯一一条 [high]。
1743
- // ⚠️ 顺序写反(把①放最后)= 用一条「规则没保存」把「你批的和正在跑的不是同一个东西」顶掉。
1744
- // ⚠️ 时序契约(四轮 [medium])一并在此兑现:三条**都在 `await respond(...)` 成功之后**才发 ——
1745
- // ①的文案里有「审批本身过了」,而 respond 抛错那条路上那句是假的(决断没落定,引擎自决)。
1746
821
  if (ruleArmDroppedForCaps)
1747
822
  surfaceRuleArmNotSent();
1748
823
  if (ruleArmDroppedForCheck)
1749
824
  surfaceRuleArmRejected();
1750
- // 🔴 FIX①:三选卡第 2 项在用户心里签的是「本会话这个工具不再问我」。server 说没记住时,
1751
- // 界面必须收敛那个承诺 —— 这里只发一条诚实通知(渲不渲「全放行」徽标由宿主按 ack 决定,
1752
- // ack 也随本函数返回值一起交出去)。ack 缺席(旧 server / void 注入面)= 未知,不发。
1753
825
  if (decision === 'allow_session' && ack?.rememberApplied === false) {
1754
826
  hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} allow_session ack rememberApplied=false — grant not stored, surfacing honest notice`);
1755
827
  surfaceRememberNotApplied();
1756
828
  }
1757
- // 🔴 [high] 同族但更重的一位:用户**编辑过**入参并批准,而 server 说编辑没转发 ⇒ 引擎按
1758
- // 原始入参跑。批的那份 ≠ 跑的那份,对 Write/Edit/Bash 就是破坏性操作的入口。
1759
- // allow 已送达,事后无法 fail-closed ⇒ 唯一诚实的处置是立刻响亮告知(不是 debug 日志)。
1760
829
  if (card.kind === 'allow' && card.updatedInput !== undefined && ack?.updatedInputForwarded === false) {
1761
830
  hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} edited args were NOT forwarded (ack.updatedInputForwarded=false) — the tool runs on the ORIGINAL input`);
1762
831
  surfaceEditNotForwarded();
1763
832
  }
1764
- // #229:note 发了而 ack 说没落行(noteRecorded=false:纯活卡无行可落 / 店抖但裁决照常生效 /
1765
- // 并发同决议先落行)或位缺席(异常形:能力位为真却没回执)—— 审计面事实,decision 本身不受
1766
- // 影响,debug 留痕即可(不惊动用户:决断没丢,丢的只是理由的持久档)。
1767
833
  if (note !== undefined && ack !== undefined && ack.noteRecorded !== true) {
1768
834
  hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} decision note was sent but not persisted ` +
1769
835
  `(ack.noteRecorded=${String(ack.noteRecorded)}) — decision stood; the audit note did not land on the ask row`);
@@ -1771,12 +837,6 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
1771
837
  return ack !== undefined ? { decision, ack } : { decision };
1772
838
  }
1773
839
  catch (e) {
1774
- // #225 件5(0.42.0):**原文交还调用方**,不再只留一行 debug 就把它吞掉。
1775
- // 🔴 `decision:'unresolved'` 逐字保留(respond 没落定这件事没变);新增的只是 `respondRefusal`
1776
- // 这一格诊断面 —— 宿主拿它去渲「你的规则被拒了,引擎说:…」,而不是把一次响亮 400 呈成
1777
- // 「什么也没发生」([5111] 探针实测的闭集三处之③)。
1778
- // 🔴 留痕仍只写**元数据**(状态码 + 码 + 长度),正文不进日志:它是 UNTRUSTED 的应答体文本,
1779
- // 回喂宿主 sink 零诊断增量、还是外溢面(与 note 那条同一条纪律)。
1780
840
  const respondRefusal = readToolApprovalRespondRefusal(e);
1781
841
  hostLog('debug', `liveToolApprovalWire: respond(${decision}) failed for ${frame.approvalId} ` +
1782
842
  `(status=${respondRefusal?.status ?? 'none'} errorCode=${logSafeErrorCode(respondRefusal?.errorCode)} ` +