@sema-agent/core 7.52.0 → 7.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/CHANGELOG.md +470 -0
  2. package/LICENSE +3 -1
  3. package/README.md +3 -1
  4. package/dist/agents/cross-session-envelope.d.ts +8 -0
  5. package/dist/agents/cross-session-envelope.js +6 -0
  6. package/dist/agents/cross-session-judge.d.ts +21 -1
  7. package/dist/agents/cross-session-judge.js +8 -0
  8. package/dist/agents/cross-session-ref.d.ts +5 -0
  9. package/dist/agents/cross-session-ref.js +1 -0
  10. package/dist/agents/org-delivery-gate.d.ts +122 -0
  11. package/dist/agents/org-delivery-gate.js +69 -0
  12. package/dist/agents/peer-held-queue.d.ts +9 -4
  13. package/dist/agents/peer-held-queue.js +6 -5
  14. package/dist/agents/peer-session-drain.d.ts +35 -6
  15. package/dist/agents/peer-session-drain.js +271 -182
  16. package/dist/agents/read-inbox-tool.d.ts +7 -0
  17. package/dist/agents/read-inbox-tool.js +5 -1
  18. package/dist/agents/send-message-tool.js +3 -4
  19. package/dist/agents/subagent.d.ts +6 -5
  20. package/dist/agents/subagent.js +117 -83
  21. package/dist/agents/tool-filter.d.ts +25 -4
  22. package/dist/agents/tool-filter.js +13 -3
  23. package/dist/brain/anthropic.d.ts +6 -1
  24. package/dist/brain/anthropic.js +4 -4
  25. package/dist/brain/model-presets.js +2 -1
  26. package/dist/brain/route-adjudicator.d.ts +6 -0
  27. package/dist/brain/routing.d.ts +24 -1
  28. package/dist/brain/routing.js +54 -19
  29. package/dist/brain/timeout.js +5 -4
  30. package/dist/core/a2a.d.ts +5 -2
  31. package/dist/core/a2a.js +33 -14
  32. package/dist/core/allow-entry.d.ts +3 -1
  33. package/dist/core/allow-entry.js +3 -2
  34. package/dist/core/approval-object.d.ts +87 -0
  35. package/dist/core/approval-object.js +85 -0
  36. package/dist/core/arg-summary.d.ts +3 -2
  37. package/dist/core/arg-summary.js +17 -11
  38. package/dist/core/ask-origin.d.ts +43 -27
  39. package/dist/core/ask-origin.js +20 -11
  40. package/dist/core/ask-question.d.ts +0 -1
  41. package/dist/core/ask-question.js +4 -67
  42. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  43. package/dist/core/auto-compaction.d.ts +106 -50
  44. package/dist/core/auto-compaction.js +69 -53
  45. package/dist/core/auto-mode-prompt.d.ts +4 -2
  46. package/dist/core/auto-mode-prompt.js +4 -3
  47. package/dist/core/canonical-json.d.ts +69 -0
  48. package/dist/core/canonical-json.js +130 -8
  49. package/dist/core/checkpoint-requirements.d.ts +13 -7
  50. package/dist/core/checkpoint-requirements.js +4 -1
  51. package/dist/core/checkpoint-store.d.ts +60 -36
  52. package/dist/core/checkpoint-store.js +7 -6
  53. package/dist/core/child-clamp-seats.d.ts +45 -6
  54. package/dist/core/child-clamp-seats.js +56 -4
  55. package/dist/core/compliance.js +3 -1
  56. package/dist/core/consolidate-scope.d.ts +1 -1
  57. package/dist/core/cross-boundary-taint.d.ts +213 -0
  58. package/dist/core/cross-boundary-taint.js +196 -0
  59. package/dist/core/decision-receipt.d.ts +44 -1
  60. package/dist/core/decision-receipt.js +16 -0
  61. package/dist/core/effective-path-target.d.ts +28 -6
  62. package/dist/core/effective-path-target.js +28 -2
  63. package/dist/core/engine-notice.d.ts +47 -4
  64. package/dist/core/engine-notice.js +17 -1
  65. package/dist/core/engine-storage-backend.d.ts +73 -0
  66. package/dist/core/engine-storage-backend.js +1 -0
  67. package/dist/core/file-history-store.d.ts +6 -2
  68. package/dist/core/file-history-store.js +1 -0
  69. package/dist/core/gate-fold.js +13 -4
  70. package/dist/core/gate-lanes.js +41 -34
  71. package/dist/core/gate-outcome.d.ts +18 -9
  72. package/dist/core/governance-codes.d.ts +9 -2
  73. package/dist/core/governance-codes.js +9 -0
  74. package/dist/core/hands-band-screen.d.ts +7 -0
  75. package/dist/core/hands-band-screen.js +32 -0
  76. package/dist/core/hooks.d.ts +21 -19
  77. package/dist/core/hooks.js +6 -5
  78. package/dist/core/locked-config.d.ts +110 -31
  79. package/dist/core/locked-config.js +155 -42
  80. package/dist/core/lsp.d.ts +1 -1
  81. package/dist/core/mcp-probe.js +1 -1
  82. package/dist/core/mcp-server-spec.d.ts +152 -17
  83. package/dist/core/mcp-server-spec.js +223 -40
  84. package/dist/core/mcp.d.ts +21 -14
  85. package/dist/core/mcp.js +59 -54
  86. package/dist/core/memory-engine/engine.d.ts +14 -19
  87. package/dist/core/memory-engine/engine.js +9 -6
  88. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  89. package/dist/core/memory-engine/export-bundle.js +1 -1
  90. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  91. package/dist/core/memory-engine/file-backend.js +1 -12
  92. package/dist/core/memory.d.ts +3 -3
  93. package/dist/core/mode-policy.d.ts +16 -26
  94. package/dist/core/mode-policy.js +5 -18
  95. package/dist/core/mods/kernel/dispatch.js +13 -15
  96. package/dist/core/mods/kernel/events.d.ts +9 -6
  97. package/dist/core/mods/kernel/events.js +2 -2
  98. package/dist/core/mods/kernel/receive.d.ts +41 -11
  99. package/dist/core/mods/kernel/receive.js +124 -54
  100. package/dist/core/mods/kernel/site-rules.d.ts +80 -15
  101. package/dist/core/mods/kernel/site-rules.js +232 -40
  102. package/dist/core/mods/stations.d.ts +0 -6
  103. package/dist/core/mods/stations.js +0 -7
  104. package/dist/core/mounted-mcp.d.ts +57 -0
  105. package/dist/core/mounted-mcp.js +71 -2
  106. package/dist/core/own-property-receipt.d.ts +13 -0
  107. package/dist/core/own-property-receipt.js +3 -1
  108. package/dist/core/parent-spec-seat.d.ts +12 -15
  109. package/dist/core/parent-spec-seat.js +14 -6
  110. package/dist/core/pause-registry.d.ts +35 -1
  111. package/dist/core/pause-registry.js +10 -0
  112. package/dist/core/permission-rule-consent.d.ts +18 -5
  113. package/dist/core/permission-rule-consent.js +13 -8
  114. package/dist/core/permission-rule-model.d.ts +87 -15
  115. package/dist/core/permission-rule-model.js +87 -8
  116. package/dist/core/permission-rule-org.d.ts +200 -33
  117. package/dist/core/permission-rule-org.js +217 -91
  118. package/dist/core/permission-rule-session.js +2 -2
  119. package/dist/core/permission-rule-store.d.ts +8 -0
  120. package/dist/core/permission-rule-store.js +1 -0
  121. package/dist/core/permission-rule-sync.js +3 -2
  122. package/dist/core/permission-rules.d.ts +7 -1
  123. package/dist/core/permission-rules.js +37 -8
  124. package/dist/core/read-only-shell.d.ts +1 -1
  125. package/dist/core/read-only-shell.js +30 -9
  126. package/dist/core/remote-env.d.ts +41 -1
  127. package/dist/core/remote-env.js +18 -0
  128. package/dist/core/resume-startup-cause.d.ts +18 -0
  129. package/dist/core/resume-startup-cause.js +12 -0
  130. package/dist/core/retention.js +7 -2
  131. package/dist/core/retired-keys.d.ts +8 -2
  132. package/dist/core/retired-keys.js +12 -1
  133. package/dist/core/roles.js +60 -10
  134. package/dist/core/rule-lane-reads.js +1 -1
  135. package/dist/core/runner/approval-feedback.d.ts +46 -0
  136. package/dist/core/runner/approval-feedback.js +41 -0
  137. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  138. package/dist/core/runner/compaction-call-options.js +2 -2
  139. package/dist/core/runner/contracts.d.ts +34 -41
  140. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  141. package/dist/core/runner/cross-boundary-lane.js +190 -0
  142. package/dist/core/runner/decide-continuation.d.ts +4 -4
  143. package/dist/core/runner/decide-continuation.js +3 -1
  144. package/dist/core/runner/gate-exit.d.ts +32 -12
  145. package/dist/core/runner/gate-exit.js +77 -34
  146. package/dist/core/runner/inherited-ask-grants.js +2 -2
  147. package/dist/core/runner/mcp-redial.d.ts +52 -10
  148. package/dist/core/runner/mcp-redial.js +38 -9
  149. package/dist/core/runner/permission-rule-lanes.d.ts +5 -2
  150. package/dist/core/runner/permission-rule-lanes.js +22 -10
  151. package/dist/core/runner/person-path-rows.d.ts +5 -3
  152. package/dist/core/runner/person-path-rows.js +2 -2
  153. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -4
  154. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  155. package/dist/core/runner/prepare-ask-lane.js +3 -3
  156. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  157. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  158. package/dist/core/runner/prepare-config-doors.d.ts +58 -15
  159. package/dist/core/runner/prepare-config-doors.js +167 -146
  160. package/dist/core/runner/prepare-context-lane.js +6 -3
  161. package/dist/core/runner/prepare-defer-classify.js +4 -7
  162. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  163. package/dist/core/runner/prepare-delegation-surface.js +8 -7
  164. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  165. package/dist/core/runner/prepare-execution-env.js +2 -1
  166. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  167. package/dist/core/runner/prepare-gate-stations.js +4 -3
  168. package/dist/core/runner/prepare-hands-readface.d.ts +3 -3
  169. package/dist/core/runner/prepare-hands-readface.js +3 -2
  170. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  171. package/dist/core/runner/prepare-inherited-gate.js +3 -2
  172. package/dist/core/runner/prepare-listings.d.ts +1 -1
  173. package/dist/core/runner/prepare-listings.js +4 -2
  174. package/dist/core/runner/prepare-memory-engine-session.d.ts +0 -6
  175. package/dist/core/runner/prepare-memory-engine-session.js +19 -6
  176. package/dist/core/runner/prepare-memory.d.ts +3 -2
  177. package/dist/core/runner/prepare-memory.js +6 -2
  178. package/dist/core/runner/prepare-offload-wrappers.d.ts +4 -4
  179. package/dist/core/runner/prepare-offload-wrappers.js +2 -1
  180. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  181. package/dist/core/runner/prepare-park-ask.js +9 -3
  182. package/dist/core/runner/prepare-policy-chain.d.ts +1 -1
  183. package/dist/core/runner/prepare-policy-chain.js +14 -19
  184. package/dist/core/runner/prepare-project-context.d.ts +1 -1
  185. package/dist/core/runner/prepare-project-context.js +6 -4
  186. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  187. package/dist/core/runner/prepare-prompt-assembly.js +20 -16
  188. package/dist/core/runner/prepare-prompt-inputs.d.ts +12 -6
  189. package/dist/core/runner/prepare-prompt-inputs.js +6 -3
  190. package/dist/core/runner/prepare-protocol-tools.d.ts +7 -5
  191. package/dist/core/runner/prepare-protocol-tools.js +19 -12
  192. package/dist/core/runner/prepare-question-face.d.ts +4 -2
  193. package/dist/core/runner/prepare-question-face.js +4 -3
  194. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  195. package/dist/core/runner/prepare-safety-scan.js +34 -27
  196. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  197. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  198. package/dist/core/runner/prepare-task.js +22 -19
  199. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  200. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  201. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -5
  202. package/dist/core/runner/prepare-wiring-manifest.js +9 -6
  203. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  204. package/dist/core/runner/resume-admission.d.ts +4 -1
  205. package/dist/core/runner/resume-admission.js +16 -14
  206. package/dist/core/runner/resume-apply.js +4 -4
  207. package/dist/core/runner/resume-internals-and-config.d.ts +3 -3
  208. package/dist/core/runner/resume-internals-and-config.js +3 -8
  209. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  210. package/dist/core/runner/resume-policy-outcome.js +18 -15
  211. package/dist/core/runner/resume-preflight.js +2 -2
  212. package/dist/core/runner/resume-review-outcome.js +3 -3
  213. package/dist/core/runner/retired-runner-deps.d.ts +5 -6
  214. package/dist/core/runner/retired-runner-deps.js +1 -4
  215. package/dist/core/runner/run-leg.js +97 -43
  216. package/dist/core/runner/run-recovery-lanes.js +5 -3
  217. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  218. package/dist/core/runner/run-terminal-adoption.js +21 -9
  219. package/dist/core/runner/run-turn-boundary.js +9 -3
  220. package/dist/core/runner/runtask.js +1 -1
  221. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  222. package/dist/core/runner/session-file-state-replay.js +3 -2
  223. package/dist/core/runner/skills-seat.d.ts +15 -0
  224. package/dist/core/runner/skills-seat.js +57 -0
  225. package/dist/core/runner/stream-steer-verb.js +2 -2
  226. package/dist/core/runner/sub-agent-deny-lists.d.ts +13 -11
  227. package/dist/core/runner/sub-agent-deny-lists.js +7 -5
  228. package/dist/core/runner/synthetic-tools.d.ts +5 -2
  229. package/dist/core/runner/synthetic-tools.js +2 -12
  230. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  231. package/dist/core/runner/tool-face-overlay.js +24 -15
  232. package/dist/core/runner-deps.d.ts +37 -22
  233. package/dist/core/session-policy-store.d.ts +19 -0
  234. package/dist/core/session-policy-store.js +22 -2
  235. package/dist/core/session-store.d.ts +35 -16
  236. package/dist/core/session-store.js +20 -12
  237. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  238. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  239. package/dist/core/shell-wrapper-table.d.ts +5 -3
  240. package/dist/core/shell-wrapper-table.js +1 -1
  241. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  242. package/dist/core/store-contracts/mailbox-store-contract.js +13 -0
  243. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  244. package/dist/core/store-contracts/session-store-contract.js +639 -0
  245. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  246. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  247. package/dist/core/task-event.d.ts +44 -6
  248. package/dist/core/task-notification.js +2 -2
  249. package/dist/core/task-registry-agent.js +1 -0
  250. package/dist/core/task-registry-shared.d.ts +5 -0
  251. package/dist/core/task-registry.d.ts +7 -0
  252. package/dist/core/task-registry.js +17 -0
  253. package/dist/core/task-result.d.ts +1 -1
  254. package/dist/core/task-spec.d.ts +7 -7
  255. package/dist/core/tighten-task-spec.d.ts +6 -4
  256. package/dist/core/tighten-task-spec.js +2 -2
  257. package/dist/core/tool-catalog-entries.d.ts +1 -1
  258. package/dist/core/tool-catalog-entries.js +52 -52
  259. package/dist/core/tool-face-reader.d.ts +49 -0
  260. package/dist/core/tool-face-reader.js +94 -0
  261. package/dist/core/tool-face.d.ts +17 -8
  262. package/dist/core/tool-face.js +1 -0
  263. package/dist/core/tool-policy.d.ts +67 -23
  264. package/dist/core/tool-policy.js +72 -60
  265. package/dist/core/tool-roster.d.ts +46 -28
  266. package/dist/core/tool-roster.js +91 -14
  267. package/dist/core/tool-spec-members.js +2 -2
  268. package/dist/core/tool-spec.d.ts +20 -4
  269. package/dist/core/tools.js +2 -0
  270. package/dist/core/trace.d.ts +24 -2
  271. package/dist/core/types.d.ts +3 -3
  272. package/dist/core/types.js +1 -1
  273. package/dist/core/untrusted-text.d.ts +23 -0
  274. package/dist/core/untrusted-text.js +10 -10
  275. package/dist/core/user-attachment.d.ts +56 -2
  276. package/dist/core/user-attachment.js +139 -56
  277. package/dist/core/wiring-manifest.d.ts +4 -2
  278. package/dist/engine/compaction/compaction.d.ts +2 -2
  279. package/dist/engine/compaction/utils.d.ts +2 -2
  280. package/dist/engine/harness/agent-harness.js +2 -1
  281. package/dist/engine/harness/types.d.ts +10 -4
  282. package/dist/engine/llm/types.d.ts +7 -2
  283. package/dist/engine/llm/types.js +2 -1
  284. package/dist/engine/loop/agent-loop.d.ts +12 -1
  285. package/dist/engine/loop/agent-loop.js +17 -5
  286. package/dist/engine/loop/types.d.ts +7 -1
  287. package/dist/engine/session/repo-utils.d.ts +5 -0
  288. package/dist/engine/session/repo-utils.js +9 -0
  289. package/dist/engine/session/storage-base.js +2 -2
  290. package/dist/index.d.ts +37 -17
  291. package/dist/index.js +31 -14
  292. package/dist/internal/harness.d.ts +1 -0
  293. package/dist/internal/harness.js +1 -0
  294. package/dist/internal/llm.d.ts +1 -1
  295. package/dist/internal/llm.js +1 -1
  296. package/dist/orchestration/goal.d.ts +24 -1
  297. package/dist/orchestration/goal.js +3 -2
  298. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  299. package/dist/orchestration/run-workflow-tool.js +1 -0
  300. package/dist/orchestration/workflow-governance.d.ts +1 -1
  301. package/dist/orchestration/workflow.d.ts +3 -0
  302. package/dist/orchestration/workflow.js +2 -1
  303. package/dist/prompt-assembly/artifact-store.d.ts +44 -9
  304. package/dist/prompt-assembly/artifact-store.js +38 -12
  305. package/dist/prompt-assembly/artifact.d.ts +12 -11
  306. package/dist/prompt-assembly/artifact.js +52 -21
  307. package/dist/prompt-assembly/assemble.d.ts +2 -0
  308. package/dist/prompt-assembly/assemble.js +2 -1
  309. package/dist/prompt-assembly/catalog/index.js +2 -1
  310. package/dist/prompt-assembly/catalog/memory.d.ts +13 -0
  311. package/dist/prompt-assembly/catalog/memory.js +6 -0
  312. package/dist/prompt-assembly/catalog/schema.d.ts +19 -2
  313. package/dist/prompt-assembly/catalog/schema.js +3 -0
  314. package/dist/prompt-assembly/composer.d.ts +2 -0
  315. package/dist/prompt-assembly/composer.js +1 -1
  316. package/dist/prompt-assembly/overlay.d.ts +21 -11
  317. package/dist/prompt-assembly/overlay.js +57 -19
  318. package/dist/prompt-assembly/select.d.ts +21 -6
  319. package/dist/prompt-assembly/select.js +41 -4
  320. package/dist/prompts/default.d.ts +1 -12
  321. package/dist/prompts/default.js +0 -3
  322. package/dist/shell/lex.d.ts +87 -8
  323. package/dist/shell/lex.js +283 -49
  324. package/dist/stores/file/index.d.ts +8 -6
  325. package/dist/stores/file/permission-rule-store.d.ts +1 -1
  326. package/dist/stores/file/permission-rule-store.js +2 -1
  327. package/dist/tools/artifact/artifact-tool.js +14 -3
  328. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  329. package/dist/tools/fs/bash-program-position.js +3 -10
  330. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  331. package/dist/tools/fs/fs-bash.d.ts +3 -3
  332. package/dist/tools/fs/fs-bash.js +72 -101
  333. package/dist/tools/fs/fs-read.d.ts +12 -0
  334. package/dist/tools/fs/fs-read.js +10 -9
  335. package/dist/tools/fs/fs-shared.d.ts +27 -31
  336. package/dist/tools/fs/fs-shared.js +12 -15
  337. package/dist/tools/fs/fs-write.js +1 -1
  338. package/dist/tools/fs/index.js +2 -2
  339. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  340. package/dist/tools/fs/read-deny-home.js +5 -4
  341. package/dist/tools/fs/read-deny.d.ts +16 -3
  342. package/dist/tools/fs/read-deny.js +12 -9
  343. package/dist/tools/fs/safety.d.ts +7 -6
  344. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  345. package/dist/tools/fs/shell-read-bindings.js +15 -16
  346. package/dist/tools/fs/shell-read-device.js +1 -1
  347. package/dist/tools/fs/shell-read-plan.js +36 -47
  348. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  349. package/dist/tools/fs/shell-read-word.js +2 -20
  350. package/dist/tools/fs/shell-reentry-text.js +1 -1
  351. package/dist/tools/web.d.ts +2 -2
  352. package/package.json +4 -1
  353. package/test/export-surface.snapshot.json +5651 -5457
  354. package/wire-changes/core@7.53.0.json +1968 -0
  355. package/wire-changes/core@7.54.0.json +2870 -0
  356. package/dist/core/shell-scan.d.ts +0 -60
  357. package/dist/core/shell-scan.js +0 -183
@@ -1,23 +1,47 @@
1
- /** One argv word as the lexer read it. */
1
+ /**
2
+ * The TIGHTENING reader's view of a command line — "which programs could this command line run, and can the text
3
+ * even be read?" — for the deny/ask direction of the permission-rule lane (`permission-rule-model.ts`
4
+ * `programRunReachOf`) and the read-only allow member (`read-only-shell.ts`). It reads the ONE shell lexer's result
5
+ * (`shell/lex.ts`) and adds only this reader's own domain: which pieces of a line are run candidates, the leading
6
+ * keyword of a control structure, and the additive peel. A segment that carries a command/process substitution
7
+ * anywhere (an argument, a redirection operand, an unquoted heredoc body) is one whose program runs are not fully
8
+ * known — the reader answers `unreadable` for every rule such a segment does not otherwise reach, never silence; the
9
+ * leading assignments and the wrapper programs of {@link SHELL_WRAPPER_TABLE} are peeled ADDITIVELY — a segment keeps
10
+ * its spelled argv as one run candidate and gains one per peeled layer, so `sudo env X=1 nohup rm -r x` is a run of
11
+ * `sudo …`, of `env …`, of `nohup …` and of `rm -r x`, and a deny on any of them reaches.
12
+ *
13
+ * What it answers is deliberately THREE-valued per segment: the argv words it could read, each marked literal or
14
+ * expansion-bearing, or an `unreadable` reason where the program runs cannot be known from the text (a command the
15
+ * lexer cannot delimit, a syntax error, a control structure whose commands need a syntax tree, a wrapper handed its
16
+ * command as one string or an option of unknown arity). The reader above turns "unreadable" into a fail-closed ask
17
+ * rather than into silence — reading it as "no match" is how a deny went blind on `rm -r x; echo "$HOME"`. Where the
18
+ * lexer cannot delimit the text, the segments it read before it stopped stay candidates (bash runs every complete line
19
+ * before the one it cannot parse) and the segment it stopped in is the unreadable one.
20
+ *
21
+ * The ALLOW direction's rule lane never reads this module: a standing yes covers the exact spelling a person read
22
+ * (`ruleAdmitsCommand`), and its whole-string refusal of every shell construct stays where it is.
23
+ */
24
+ import { type LexedWord } from "../shell/lex.js";
25
+ /** One argv word as the reader projects it. */
2
26
  export interface ShellWord {
3
27
  /** The word's value with quotes and escapes removed — meaningful only when `expands` is `false`. */
4
28
  readonly text: string;
5
29
  /** The unquoted source spelling (assignment and wrapper-flag tests read this, never `text`). */
6
30
  readonly raw: string;
7
31
  /** `false`: a literal word. `"one"`: carries an expansion but yields exactly ONE word (it was double-quoted).
8
- * `"many"`: an unquoted expansion, glob or brace pattern, or a quoted `"$@"` / array expansion — may
9
- * yield zero, one or several words. */
32
+ * `"many"`: an unquoted expansion, glob or brace pattern, an ANSI-C quote, or a quoted `"$@"` / array expansion —
33
+ * may yield zero, one or several words. */
10
34
  readonly expands: false | "one" | "many";
11
35
  }
12
- /** One redirection of a segment, as read: the operator and its operand word (a heredoc's operand is its
13
- * delimiter; the IO number or `{name}` before the operator is folded into the operator's meaning and not
14
- * kept). The tightening reader never reads these — a rule speaks about program runs; the read-only
15
- * reader does (an output redirection is a write). */
36
+ /** One redirection of a segment, as read: the operator and its operand word (a heredoc's operand is its delimiter;
37
+ * a descriptor duplication's is the descriptor; the IO number or `{name}` before the operator is folded into the
38
+ * operator's meaning and not kept). The tightening reader never reads these — a rule speaks about program runs; the
39
+ * read-only reader does (an output redirection is a write). */
16
40
  export interface ShellRedirection {
17
41
  readonly op: string;
18
42
  readonly target: ShellWord;
19
43
  }
20
- /** One program run the lexer found, after keyword stripping. */
44
+ /** One program run the reader found, after keyword stripping. */
21
45
  export interface ShellSegment {
22
46
  /** The run as SPELLED (a leading keyword removed): the first candidate a rule is compared against. */
23
47
  readonly argv: readonly ShellWord[];
@@ -32,7 +56,7 @@ export interface ShellSegment {
32
56
  /** Present when the segment's program runs are NOT fully known although its candidates could be read:
33
57
  * a peel that stopped before the wrapped program could be named (its options carry an expansion, a
34
58
  * command string, an option of unknown arity), or a command/process substitution anywhere in the
35
- * segment (it runs a program the lexer does not read). The candidates stand — a rule they reach is
59
+ * segment (it runs a program the reader does not read). The candidates stand — a rule they reach is
36
60
  * reached — and every other rule reads `unreadable`. */
37
61
  readonly peelUnreadable?: string;
38
62
  }
@@ -54,10 +78,10 @@ export interface ShellCommandShape {
54
78
  readonly strayRedirection: boolean;
55
79
  }
56
80
  export { SHELL_WRAPPER_TABLE, type ShellWrapperName } from "./shell-wrapper-table.js";
57
- /** Longer than this and the command is not read at all (one unreadable segment): every pass is linear,
58
- * but a substitution nested inside quoting re-enters the quote reader once per nesting level, and the
59
- * cap is what bounds that on model-supplied text. Upstream's own reader stops at the same size. */
81
+ /** Longer than this and the command is not read at all (one unreadable segment). Upstream's own reader stops at the
82
+ * same size. */
60
83
  export declare const MAX_SHELL_READ_CHARS = 10000;
84
+ export declare function lexedWordOf(w: ShellWord): LexedWord | undefined;
61
85
  /** Read `source` as the tightening reader must: every program run the text could perform, or why it cannot be read. */
62
86
  export declare function readShellCommand(source: string): ShellCommandShape;
63
87
  /** Every program run of `shape` can be NAMED from the text: no segment is unreadable, no peel stopped,
@@ -1,62 +1,74 @@
1
+ import { arithmeticTextIsPlain, compoundValueHidesRun, expansionHidesRun, heredocBodyHidesRun, lexShell, namesRedirectDescriptor, PARSE_FAILURE_SENTENCE, } from "../shell/lex.js";
1
2
  import { shellWordsOf } from "../shell/shell-words.js";
3
+ import { isLauncherAssignmentOperand } from "../tools/fs/bash-program-position.js";
4
+ import { extglobOpenerIn } from "../tools/fs/bash-readonly-classifier.js";
2
5
  import { evaluatesOperand, SHELL_WRAPPER_TABLE } from "./shell-wrapper-table.js";
3
- import { arithmeticHidesRun, carriesSubstitution, freshWord, matchOp, readDoubleQuoted, readExpansion, skipBalanced, skipHeredocBodies } from "./shell-scan.js";
4
6
  export { SHELL_WRAPPER_TABLE } from "./shell-wrapper-table.js";
5
7
  const WRAPPERS = new Map(Object.entries(SHELL_WRAPPER_TABLE));
6
8
  const PREFIX_KEYWORDS = shellWordsOf("stripped_prefix");
7
9
  const CLOSING_KEYWORDS = shellWordsOf("stripped_standalone");
8
10
  const OPAQUE_KEYWORDS = shellWordsOf("opaque");
9
- const ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?\+?=/;
11
+ const CONTROL_PREFIXES = shellWordsOf("control_prefix");
10
12
  const DURATION_RE = /^\d+(?:\.\d+)?[smhd]?$/;
11
- const REDIRECT_OPS = ["<<<", "<<-", "<<", "<>", "<&", "<", ">>", ">|", ">&", "&>>", "&>", ">"];
12
- const CONNECTOR_OPS = ["&&", "||", "|&", "|", ";;&", ";&", ";;", ";", "&"];
13
- const ALL_OPS = [...CONNECTOR_OPS, ...REDIRECT_OPS];
14
13
  export const MAX_SHELL_READ_CHARS = 10_000;
14
+ const LEXED_WORDS = new WeakMap();
15
+ export function lexedWordOf(w) {
16
+ return LEXED_WORDS.get(w);
17
+ }
18
+ function wordOf(w) {
19
+ let expands = false;
20
+ const widen = (to) => { if (expands !== "many")
21
+ expands = to; };
22
+ let text = "";
23
+ for (const p of w.pieces) {
24
+ const e = p.expansion;
25
+ if (p.quotedBy === "$'" || (e === undefined && p.literal === undefined)) {
26
+ widen("many");
27
+ continue;
28
+ }
29
+ if (e === undefined) {
30
+ text += p.literal ?? "";
31
+ continue;
32
+ }
33
+ if (e.kind === "tilde") {
34
+ if (p === w.pieces[0])
35
+ widen(e.yields);
36
+ text += e.body ?? "";
37
+ }
38
+ else if (e.kind === "brace") {
39
+ widen("many");
40
+ text += `{${e.body ?? ""}}`;
41
+ }
42
+ else {
43
+ widen(e.yields);
44
+ text += e.pattern?.folded ?? "";
45
+ }
46
+ }
47
+ const out = { text, raw: w.raw, expands };
48
+ LEXED_WORDS.set(out, w);
49
+ return out;
50
+ }
51
+ const literalIn = (w, set) => w !== undefined && w.expands === false && set.has(w.text);
52
+ const bareAssignment = (w) => lexedWordOf(w)?.assignment?.bare === true;
53
+ const hidesRun = (w) => w.expansions.some((p) => p.expansion !== undefined && expansionHidesRun(p.expansion)) || (w.assignment?.subscript !== undefined && !arithmeticTextIsPlain(w.assignment.subscript.raw)) || compoundValueHidesRun(w);
54
+ const CASE_ARM_UNREAD = "a `case` arm terminator outside a `case` this lane reads";
55
+ const EXTGLOB_UNREAD = "an extended pattern (`?(…)` `*(…)` `+(…)` `@(…)` `!(…)`) is one this reader does not expand";
15
56
  export function readShellCommand(source) {
16
57
  if (source.length > MAX_SHELL_READ_CHARS)
17
58
  return { segments: [{ argv: [], redirections: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false, backgrounded: false, strayRedirection: false };
59
+ const lexed = lexShell(source);
60
+ const failed = "parseFailure" in lexed ? lexed : undefined;
61
+ const read = "parseFailure" in lexed ? lexed.readBefore : lexed.segments;
18
62
  const segments = [];
19
63
  const connectors = [];
20
64
  let grouped = false;
21
65
  let backgrounded = false;
22
66
  let strayRedirection = false;
23
67
  let seg = { words: [], redirections: [] };
24
- let word = freshWord();
25
- let pendingRedirect;
26
- let heredocs = [];
27
- let badDelimiter = false;
28
68
  let afterBoundary = true;
29
69
  let lastConnector;
30
- let tail;
31
- const n = source.length;
32
- let i = 0;
33
- const endWord = () => {
34
- if (!word.started)
35
- return;
36
- if (word.braceExpands)
37
- word.expands = "many";
38
- if (word.substitution)
39
- seg.substitution = true;
40
- if (pendingRedirect !== undefined) {
41
- if (pendingRedirect === "<<" || pendingRedirect === "<<-") {
42
- if (word.expands !== false)
43
- badDelimiter = true;
44
- heredocs.push({ delimiter: word.text, quoted: word.raw !== word.text, dash: pendingRedirect === "<<-" });
45
- }
46
- seg.redirections.push({ op: pendingRedirect, target: publicWord(word) });
47
- pendingRedirect = undefined;
48
- }
49
- else {
50
- seg.words.push(word);
51
- }
52
- word = freshWord();
53
- };
70
+ let arithmetic;
54
71
  const finishSegment = (connector) => {
55
- endWord();
56
- if (pendingRedirect !== undefined) {
57
- seg.unreadable ??= `the redirection \`${pendingRedirect}\` has no operand — a syntax error`;
58
- pendingRedirect = undefined;
59
- }
60
72
  let built = buildSegment(seg);
61
73
  if (built === "keyworded")
62
74
  grouped = true;
@@ -86,214 +98,121 @@ export function readShellCommand(source) {
86
98
  afterBoundary = false;
87
99
  };
88
100
  const boundary = () => { finishSegment(undefined); grouped = true; afterBoundary = true; lastConnector = undefined; };
89
- while (i < n && tail === undefined) {
90
- const ch = source[i];
91
- if (ch === " " || ch === "\t") {
92
- endWord();
93
- i++;
94
- continue;
95
- }
96
- if (ch === "\n") {
97
- endWord();
98
- if (badDelimiter) {
99
- tail = "a here-document delimiter spelled with an expansion";
100
- break;
101
- }
102
- if (heredocs.length === 0) {
103
- finishSegment("\n");
104
- i++;
105
- continue;
106
- }
107
- const skipped = skipHeredocBodies(source, i + 1, heredocs);
108
- heredocs = [];
109
- if (skipped === undefined) {
110
- tail = "a here-document body never reaches its delimiter";
111
- break;
101
+ const word = (w) => { if (hidesRun(w))
102
+ seg.substitution = true; return wordOf(w); };
103
+ const readTokens = (s) => {
104
+ const tokens = s.tokens;
105
+ const shown = tokens.flatMap((t) => (t.kind === "word" ? t.expansions : t.kind === "redirect" ? t.target?.expansions ?? [] : [])).filter((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic").length;
106
+ if (s.substitutions.length > shown)
107
+ seg.substitution = true;
108
+ let definitionClose = false;
109
+ tokens.forEach((t, i) => {
110
+ if (arithmetic !== undefined) {
111
+ if (t.kind === "group") {
112
+ arithmetic.depth += t.op === "(" ? 1 : -1;
113
+ if (arithmetic.depth === 0)
114
+ arithmetic = undefined;
115
+ return;
116
+ }
117
+ const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
118
+ if (w === undefined || hidesRun(w) || !arithmeticTextIsPlain(w.raw) || t.kind === "heredoc")
119
+ seg.substitution = true;
120
+ if (t.kind === "redirect" && t.dup !== undefined && !arithmeticTextIsPlain(t.dup))
121
+ seg.substitution = true;
122
+ return;
112
123
  }
113
- if (skipped.substitution)
114
- seg.substitution = true;
115
- finishSegment("\n");
116
- i = skipped.end;
117
- continue;
118
- }
119
- if (ch === "#" && !word.started) {
120
- while (i < n && source[i] !== "\n")
121
- i++;
122
- continue;
123
- }
124
- if (ch === "\\") {
125
- if (i + 1 >= n) {
126
- tail = "a trailing backslash — the line never ends";
127
- break;
124
+ if (t.kind === "word") {
125
+ if (!namesRedirectDescriptor(t, tokens[i + 1]))
126
+ seg.words.push(word(t));
127
+ return;
128
128
  }
129
- if (source[i + 1] === "\n") {
130
- i += 2;
131
- continue;
129
+ if (t.kind === "redirect") {
130
+ const target = t.target !== undefined ? word(t.target) : { text: t.dup ?? "", raw: t.dup ?? "", expands: false };
131
+ seg.redirections.push({ op: t.op, target });
132
+ return;
132
133
  }
133
- word.started = true;
134
- word.text += source[i + 1];
135
- word.raw += source.slice(i, i + 2);
136
- i += 2;
137
- continue;
138
- }
139
- if (ch === "'") {
140
- const close = source.indexOf("'", i + 1);
141
- if (close < 0) {
142
- tail = "an unterminated single quote";
143
- break;
134
+ if (t.kind === "heredoc") {
135
+ seg.redirections.push({ op: "<<", target: { text: t.delimiter, raw: t.delimiter, expands: false } });
136
+ if (!t.quoted && heredocBodyHidesRun(t.body))
137
+ seg.substitution = true;
138
+ return;
144
139
  }
145
- word.started = true;
146
- word.text += source.slice(i + 1, close);
147
- word.raw += source.slice(i, close + 1);
148
- i = close + 1;
149
- continue;
150
- }
151
- let dq = i + 1;
152
- if (ch === "$")
153
- while (source[dq] === "\\" && source[dq + 1] === "\n")
154
- dq += 2;
155
- if (ch === '"' || (ch === "$" && source[dq] === '"')) {
156
- const open = ch === "$" ? dq + 1 : i + 1;
157
- const end = readDoubleQuoted(source, open, word);
158
- if (end === undefined) {
159
- tail = "an unterminated double quote";
160
- break;
140
+ const next = tokens[i + 1];
141
+ if (definitionClose) {
142
+ definitionClose = false;
143
+ return;
161
144
  }
162
- word.raw += source.slice(i, end);
163
- i = end;
164
- continue;
165
- }
166
- if (ch === "$" || ch === "`" || ((ch === "<" || ch === ">") && source[i + 1] === "(")) {
167
- const end = readExpansion(source, i, word);
168
- if (end === undefined) {
169
- tail = "an unterminated substitution";
170
- break;
145
+ if (t.op === "(" && next?.kind === "group" && next.op === "(" && seg.redirections.length === 0 && seg.words.every((w) => literalIn(w, CONTROL_PREFIXES) || literalIn(w, CLOSING_KEYWORDS) || (w.expands === false && w.text === "-p"))) {
146
+ arithmetic = { depth: 1 };
147
+ return;
171
148
  }
172
- if (end === i + 1) {
173
- word.started = true;
174
- word.text += ch;
175
- word.raw += ch;
176
- i = end;
177
- continue;
149
+ if (t.op === "(" && next?.kind === "group" && next.op === ")" && seg.words.length === 1 && seg.words[0].expands === false && seg.redirections.length === 0) {
150
+ seg.words.pop();
151
+ definitionClose = true;
178
152
  }
179
- word.started = true;
180
- word.expands = ch === "$" || ch === "`" ? "many" : word.expands || "one";
181
- word.raw += source.slice(i, end);
182
- i = end;
183
- continue;
184
- }
185
- if (ch === "~" && !word.started)
186
- word.expands = "one";
187
- const op = matchOp(source, i, ALL_OPS);
188
- if (op !== undefined) {
189
- if (CONNECTOR_OPS.includes(op)) {
190
- if (op === ";;" || op === ";&" || op === ";;&") {
191
- endWord();
192
- seg.unreadable ??= "a `case` arm terminator outside a `case` this lane reads";
193
- i += op.length;
194
- continue;
195
- }
196
- if (op === "&")
197
- backgrounded = true;
198
- finishSegment(op);
199
- i += op.length;
153
+ boundary();
154
+ });
155
+ };
156
+ for (let k = 0; k < read.length; k++) {
157
+ const s = read[k];
158
+ if (k > 0) {
159
+ const armEnd = caseArmEnd(read, k);
160
+ if (armEnd !== undefined) {
161
+ seg.unreadable ??= CASE_ARM_UNREAD;
162
+ k = armEnd;
163
+ readTokens(read[k]);
200
164
  continue;
201
165
  }
202
- if (word.started && word.raw === word.text && (/^\d+$/.test(word.text) || /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(word.text)))
203
- word = freshWord();
204
- else
205
- endWord();
206
- if (pendingRedirect !== undefined)
207
- seg.unreadable ??= `the redirection \`${pendingRedirect}\` has no operand — a syntax error`;
208
- pendingRedirect = op;
209
- i += op.length;
210
- continue;
211
- }
212
- if (ch === "(" && word.started && "@?*+!".includes(word.raw.slice(-1)) && word.raw !== "!") {
213
- const end = skipBalanced(source, i, "(", ")");
214
- if (end === undefined) {
215
- tail = "an unterminated extended pattern (`@(…)`)";
216
- break;
217
- }
218
- if (carriesSubstitution(source.slice(i + 1, end - 1)))
219
- word.substitution = true;
220
- word.expands = "many";
221
- word.text += source.slice(i, end);
222
- word.raw += source.slice(i, end);
223
- i = end;
224
- continue;
225
- }
226
- if (ch === "(" && source[i + 1] === "(" && (!word.started || word.raw === "!") && pendingRedirect === undefined && seg.words.every((w) => w.expands === false && (shellWordsOf("control_prefix").has(w.text) || CLOSING_KEYWORDS.has(w.text) || w.text === "-p"))) {
227
- endWord();
228
- const end = skipBalanced(source, i, "(", ")");
229
- if (end === undefined) {
230
- tail = "an unterminated arithmetic command";
231
- break;
166
+ const connector = s.pipesStderr === true ? "|&" : s.connector;
167
+ if (arithmetic !== undefined) {
168
+ if (connector === ";")
169
+ seg.substitution = true;
232
170
  }
233
- if (arithmeticHidesRun(source.slice(i + 2, end - 1)))
234
- seg.substitution = true;
235
- i = end;
236
- continue;
237
- }
238
- if (ch === "(" || ch === ")") {
239
- if (ch === "(" && word.started && !word.expands && seg.words.length === 0 && pendingRedirect === undefined) {
240
- let j = i + 1;
241
- while (j < n && (source[j] === " " || source[j] === "\t"))
242
- j++;
243
- if (source[j] === ")") {
244
- word = freshWord();
245
- boundary();
246
- i = j + 1;
247
- continue;
248
- }
171
+ else {
172
+ if (connector === "&")
173
+ backgrounded = true;
174
+ finishSegment(connector);
249
175
  }
250
- boundary();
251
- i++;
252
- continue;
253
176
  }
254
- if (ch === "*" || ch === "?" || (ch === "]" && word.bracketOpen))
255
- word.expands = "many";
256
- if (ch === "[")
257
- word.bracketOpen = true;
258
- if (ch === "{")
259
- word.braceOpen = true;
260
- if (word.braceOpen && (ch === "," || (ch === "." && source[i + 1] === ".")))
261
- word.braceExpands = true;
262
- if (ch === "}" && !word.braceExpands)
263
- word.braceOpen = false;
264
- word.started = true;
265
- word.text += ch;
266
- word.raw += ch;
267
- i++;
177
+ readTokens(s);
268
178
  }
269
- if (tail !== undefined) {
270
- endWord();
271
- seg.unreadable ??= tail;
272
- finishSegment(undefined);
179
+ if (failed !== undefined) {
180
+ seg.unreadable ??= PARSE_FAILURE_SENTENCE[failed.parseFailure.reason];
181
+ }
182
+ else if (arithmetic !== undefined) {
183
+ seg.unreadable ??= "an unterminated arithmetic command";
273
184
  }
274
185
  else {
275
- if (heredocs.length > 0)
276
- seg.unreadable ??= "a here-document with no body";
277
- const dangling = seg.words.length === 0 && !word.started && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
186
+ const dangling = seg.words.length === 0 && seg.redirections.length === 0 && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
278
187
  if (dangling)
279
188
  seg.unreadable ??= `the connector \`${lastConnector}\` has no command after it — a syntax error`;
280
- finishSegment(undefined);
281
189
  }
190
+ finishSegment(undefined);
191
+ if (failed === undefined && "segments" in lexed && extglobOpenerIn(lexed, source))
192
+ return { segments: segments.map((s) => (s.unreadable !== undefined ? s : { ...s, peelUnreadable: s.peelUnreadable ?? EXTGLOB_UNREAD })), connectors, grouped, backgrounded, strayRedirection };
282
193
  return { segments, connectors, grouped, backgrounded, strayRedirection };
283
194
  }
284
- const publicWord = ({ text, raw, expands }) => ({ text, raw, expands });
195
+ function caseArmEnd(read, k) {
196
+ const arm = (j) => read[j].connector === ";" && read[j].text === "" && read[j].tokens.length === 0;
197
+ const next = read[k + 1];
198
+ if (!arm(k) || next === undefined || (next.connector !== ";" && next.connector !== "&"))
199
+ return undefined;
200
+ const third = read[k + 2];
201
+ return next.connector === ";" && arm(k + 1) && third?.connector === "&" ? k + 2 : k + 1;
202
+ }
285
203
  const SUBSTITUTION_UNREAD = "a command substitution runs a program this reader does not read";
286
204
  const MAX_WRAPPER_LAYERS = 8;
287
205
  export function isFullyReadable(shape) {
288
206
  return shape.segments.every((s) => s.unreadable === undefined && s.peelUnreadable === undefined && [s.argv, ...s.peeled].every((argv) => argv[0]?.expands === false));
289
207
  }
208
+ const operandsOf = (run) => run.map((w) => ({ text: w.text, raw: w.raw, assignment: lexedWordOf(w)?.assignment }));
290
209
  function buildSegment(seg) {
291
210
  if (seg.unreadable !== undefined)
292
- return { argv: seg.words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: seg.unreadable };
211
+ return { argv: seg.words, redirections: seg.redirections, peeled: [], unreadable: seg.unreadable };
293
212
  const carried = () => (seg.substitution === true ? { argv: [], redirections: seg.redirections, peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
294
213
  let keyworded = false;
295
214
  let k0 = 0;
296
- while (k0 < seg.words.length && seg.words[k0].expands === false && (PREFIX_KEYWORDS.has(seg.words[k0].text) || CLOSING_KEYWORDS.has(seg.words[k0].text))) {
215
+ while (k0 < seg.words.length && (literalIn(seg.words[k0], PREFIX_KEYWORDS) || literalIn(seg.words[k0], CLOSING_KEYWORDS))) {
297
216
  k0++;
298
217
  keyworded = true;
299
218
  }
@@ -301,24 +220,24 @@ function buildSegment(seg) {
301
220
  const head = words[0];
302
221
  if (head === undefined)
303
222
  return carried();
304
- if (evaluatesOperand(words))
223
+ if (evaluatesOperand(operandsOf(words)))
305
224
  seg.substitution = true;
306
225
  if (head.expands === false && (head.text === "[[" || head.text === "["))
307
226
  return carried();
308
- if (head.expands === false && OPAQUE_KEYWORDS.has(head.text))
309
- return { argv: words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
310
- const argv = words.map(publicWord);
227
+ if (literalIn(head, OPAQUE_KEYWORDS))
228
+ return { argv: words, redirections: seg.redirections, peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
229
+ const argv = words;
311
230
  const peeled = [];
312
231
  let peelUnreadable;
313
232
  let k = 0;
314
- while (k < argv.length && ASSIGNMENT_RE.test(argv[k].raw))
233
+ while (k < argv.length && bareAssignment(argv[k]))
315
234
  k++;
316
235
  let run = argv.slice(k);
317
236
  if (k > 0 && run.length > 0)
318
237
  peeled.push(run);
319
238
  for (let layer = 0; run.length > 0; layer++) {
320
239
  let j = 0;
321
- while (j < run.length && ((run[j].text === "!" && run[j].expands === false) || ASSIGNMENT_RE.test(run[j].raw)))
240
+ while (j < run.length && ((run[j].text === "!" && run[j].expands === false) || bareAssignment(run[j])))
322
241
  j++;
323
242
  if (j > 0) {
324
243
  run = run.slice(j);
@@ -344,7 +263,7 @@ function buildSegment(seg) {
344
263
  run = next;
345
264
  peeled.push(run);
346
265
  }
347
- if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(c)))
266
+ if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(operandsOf(c))))
348
267
  peelUnreadable ??= SUBSTITUTION_UNREAD;
349
268
  return { argv, redirections: seg.redirections, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
350
269
  }
@@ -364,6 +283,7 @@ function unwrap(words) {
364
283
  const flags = new Set(row.flags);
365
284
  const attached = new Set(row.attached ?? []);
366
285
  const commandString = new Set(row.commandString);
286
+ const assignmentOperand = (w) => row.assignments === true && w.expands === false && isLauncherAssignmentOperand(w.text);
367
287
  let positionalTaken = row.positional === undefined;
368
288
  let durationTaken = row.duration !== true;
369
289
  let replaceToken;
@@ -405,7 +325,7 @@ function unwrap(words) {
405
325
  return r;
406
326
  i++;
407
327
  }
408
- while (row.assignments === true && i < words.length && words[i].expands === false && ASSIGNMENT_RE.test(words[i].text))
328
+ while (i < words.length && assignmentOperand(words[i]))
409
329
  i++;
410
330
  break;
411
331
  }
@@ -464,7 +384,7 @@ function unwrap(words) {
464
384
  i++;
465
385
  continue;
466
386
  }
467
- if (row.assignments === true && ASSIGNMENT_RE.test(t)) {
387
+ if (assignmentOperand(w)) {
468
388
  i++;
469
389
  continue;
470
390
  }
@@ -1,5 +1,6 @@
1
+ import type { AssignmentFact } from "../shell/lex.js";
1
2
  /**
2
- * The wrapper programs the tightening reader ({@link import("./shell-lexer.js").readShellCommand}) peels to
3
+ * The wrapper programs the tightening reader ({@link import("./shell-run-candidates.js").readShellCommand}) peels to
3
4
  * reach the program they run — upstream's three tables (its light strip on every arm, its full strip on
4
5
  * the deny/ask arms, and its `xargs` clause inlined in the matcher) as ONE closed table. Peeling is
5
6
  * ADDITIVE on the reading side: the segment keeps its spelled argv as one run candidate and adds one
@@ -395,13 +396,14 @@ export interface ShellWrapperRow {
395
396
  * subscript (`arr[a]`, quoted, bare, attached `-varr[a]`, or `arr[a]=1`) is evaluated as arithmetic —
396
397
  * recursively, like `$((a))` — when the builtin reads or writes it (`printf -v 'arr[a]' …`, `unset arr[a]`,
397
398
  * `declare arr[a]=1`, `read arr[a]`, `[[ -v arr[a] ]]`, `test -v arr[a]`), and so is the value of an
398
- * INTEGER-attribute assignment (`declare -i x=a`). The lexer marks such a run's hidden run at every peel
399
+ * INTEGER-attribute assignment (`declare -i x=a`). The tightening reader marks such a run's hidden run at every peel
399
400
  * candidate (`command printf -v …`). An operand that is WHOLLY an expansion is not judged here (`unset "$X"`
400
401
  * — one more indirection; the price of reading it would be a prompt on every `export FOO="$BAR"`), nor is
401
402
  * an arithmetic comparison's operand (`[[ $n -gt 0 ]]` evaluates `$n` — the same price). */
402
403
  export declare const VARIABLE_OPERAND_BUILTINS: ReadonlySet<string>;
403
- /** Does this run of a variable-operand builtin evaluate an operand arithmetically? Linear in the words. */
404
+ /** Does this run of a variable-operand builtin evaluate an operand arithmetically? `assignment`: the lexer's fact on the word. */
404
405
  export declare function evaluatesOperand(run: readonly {
405
406
  readonly text: string;
406
407
  readonly raw: string;
408
+ readonly assignment?: AssignmentFact | undefined;
407
409
  }[]): boolean;
@@ -67,7 +67,7 @@ export function evaluatesOperand(run) {
67
67
  if (!VARIABLE_OPERAND_BUILTINS.has(run[0]?.text ?? ""))
68
68
  return false;
69
69
  const integer = run.some((w) => /^-[A-Za-z]*i/.test(w.text));
70
- return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && /^[A-Za-z_][A-Za-z0-9_]*\+?=.*[A-Za-z_$]/.test(w.text)));
70
+ return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && w.assignment !== undefined && /[A-Za-z_$]/.test(w.assignment.value.folded)));
71
71
  }
72
72
  function namesArrayElement(word) {
73
73
  for (let open = word.indexOf("["); open > 0; open = word.indexOf("[", open + 1)) {
@@ -308,7 +308,7 @@ export async function checkpointStoreContract(make, runAssertion) {
308
308
  const stored = (await store.get(bitless.token)).pendingAction;
309
309
  assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
310
310
  });
311
- run("design/388 B18 × B6 identity + face group: a row's eight optional members (capabilityId/contractId/shapeDigest/implementationRevision/rosterDigest + family/pathTarget/names) survive the round-trip verbatim; an absent group stays absent", async () => {
311
+ run("the approval object's members: a row's identity + face group and its targets (capabilityId/contractId/shapeDigest/implementationRevision + family/pathTarget/names + targets) survive the round-trip verbatim; an absent group stays absent", async () => {
312
312
  const store = make();
313
313
  const carried = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-carried" });
314
314
  const group = {
@@ -316,10 +316,10 @@ export async function checkpointStoreContract(make, runAssertion) {
316
316
  contractId: "acme.write@2",
317
317
  shapeDigest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
318
318
  implementationRevision: "r7",
319
- rosterDigest: "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
320
319
  family: "file-write",
321
320
  pathTarget: { param: "destination", aliases: ["path"], access: "create", skillScopeEligible: true },
322
321
  names: ["AcmeWrite", "acme_write"],
322
+ targets: { protective: "/srv/work/out.txt", declared: "/srv/work/out.txt" },
323
323
  };
324
324
  Object.assign(carried.pendingAction, group);
325
325
  const bare = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-bare" });
@@ -1,6 +1,8 @@
1
1
  import { strict as assert } from "node:assert";
2
2
  import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
3
3
  import { beginContract } from "./contract-harness.js";
4
+ import { PRINCIPAL_INBOX_HANDLE } from "../../agents/cross-session-ref.js";
5
+ import { mintCrossPrincipalRecord } from "../../agents/org-delivery-gate.js";
4
6
  export const MAILBOX_CONTRACT_SCOPE = "default";
5
7
  const msg = (content, sentAt = 1000) => ({ content, sentAt });
6
8
  async function withStores(mk, fn) {
@@ -259,6 +261,17 @@ export async function mailboxBundledOnlyContract(mk, runAssertion) {
259
261
  await s.drop("tenant-b", "shared-handle");
260
262
  assert.equal(await s.peekCount("tenant-b", "shared-handle"), 0);
261
263
  });
264
+ run("第四条消费链(principal 收件箱):投递门铸的信落收件人 scope 的 principal.inbox,两只信任锚逐字节回读,发信方 scope 零残留", async (make) => {
265
+ const s = make();
266
+ const letter = mintCrossPrincipalRecord({ from: { scope: "principal-a", name: "alice" }, body: { content: "letter" }, receiptId: "gr-1", sentAt: 10_000, consumer: { kind: "terminals" } });
267
+ assert.equal(await s.append("principal-b", PRINCIPAL_INBOX_HANDLE, letter), 1);
268
+ assert.equal(await s.peekCount("principal-a", PRINCIPAL_INBOX_HANDLE), 0, "发信方 scope 的同名箱不得出现这封信");
269
+ const lease = await s.claimLease("principal-b", PRINCIPAL_INBOX_HANDLE, "drain:r1", 60_000, 10_000);
270
+ assert.deepEqual(lease.messages.map((m) => m.peerMeta), [{ kind: "peer_message", fromScope: "principal-a", gateReceiptId: "gr-1" }]);
271
+ assert.equal(lease.messages[0].from, "alice");
272
+ assert.deepEqual(await s.ack("principal-b", PRINCIPAL_INBOX_HANDLE, "drain:r1", lease.maxSeq), { acked: true });
273
+ assert.equal(await s.peekCount("principal-b", PRINCIPAL_INBOX_HANDLE), 0);
274
+ });
262
275
  await settle();
263
276
  }
264
277
  export async function mailboxCrossProcessContract(hooks) {