@nathapp/nax-agent 0.1.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 (398) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
  5. package/dist/coding-tools/coding-tool-bash.js +44 -0
  6. package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
  7. package/dist/coding-tools/coding-tool-extras.js +56 -0
  8. package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
  9. package/dist/coding-tools/coding-tool-sandbox.js +215 -0
  10. package/dist/coding-tools/coding-tool-support.d.ts +132 -0
  11. package/dist/coding-tools/coding-tool-support.js +159 -0
  12. package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
  13. package/dist/coding-tools/universal-coding-tools.js +5 -0
  14. package/dist/command-interceptor/index.d.ts +55 -0
  15. package/dist/command-interceptor/index.js +60 -0
  16. package/dist/command-interceptor/shell.d.ts +36 -0
  17. package/dist/command-interceptor/shell.js +137 -0
  18. package/dist/command-safety/build.d.ts +19 -0
  19. package/dist/command-safety/build.js +47 -0
  20. package/dist/command-safety/guard.d.ts +32 -0
  21. package/dist/command-safety/guard.js +179 -0
  22. package/dist/command-safety/identifiers.d.ts +24 -0
  23. package/dist/command-safety/identifiers.js +20 -0
  24. package/dist/command-safety/index.d.ts +17 -0
  25. package/dist/command-safety/index.js +11 -0
  26. package/dist/command-safety/questions.d.ts +19 -0
  27. package/dist/command-safety/questions.js +60 -0
  28. package/dist/command-safety/row.d.ts +2 -0
  29. package/dist/command-safety/row.js +14 -0
  30. package/dist/command-safety/rule-scorer.d.ts +9 -0
  31. package/dist/command-safety/rule-scorer.js +391 -0
  32. package/dist/command-safety/shadow.d.ts +33 -0
  33. package/dist/command-safety/shadow.js +188 -0
  34. package/dist/command-safety/systemone-client.d.ts +16 -0
  35. package/dist/command-safety/systemone-client.js +92 -0
  36. package/dist/command-safety/tap.d.ts +37 -0
  37. package/dist/command-safety/tap.js +77 -0
  38. package/dist/command-safety/temp-only.d.ts +6 -0
  39. package/dist/command-safety/temp-only.js +215 -0
  40. package/dist/command-safety/tmp-write.d.ts +2 -0
  41. package/dist/command-safety/tmp-write.js +112 -0
  42. package/dist/command-safety/types.d.ts +185 -0
  43. package/dist/command-safety/types.js +21 -0
  44. package/dist/config/bash-approval.d.ts +33 -0
  45. package/dist/config/bash-approval.js +30 -0
  46. package/dist/config/catalog-overrides.d.ts +94 -0
  47. package/dist/config/catalog-overrides.js +7 -0
  48. package/dist/config/native-agent/index.d.ts +8 -0
  49. package/dist/config/native-agent/index.js +8 -0
  50. package/dist/config/schemas-sandbox.d.ts +31 -0
  51. package/dist/config/schemas-sandbox.js +45 -0
  52. package/dist/cost/core/index.d.ts +8 -0
  53. package/dist/cost/core/index.js +7 -0
  54. package/dist/cost/estimate.d.ts +34 -0
  55. package/dist/cost/estimate.js +65 -0
  56. package/dist/cost/model-spec.d.ts +28 -0
  57. package/dist/cost/model-spec.js +29 -0
  58. package/dist/cost/standard-types.d.ts +9 -0
  59. package/dist/cost/standard-types.js +1 -0
  60. package/dist/cost/usage-math.d.ts +18 -0
  61. package/dist/cost/usage-math.js +46 -0
  62. package/dist/index.d.ts +26 -0
  63. package/dist/index.js +21 -0
  64. package/dist/infra/agent-logger.d.ts +22 -0
  65. package/dist/infra/agent-logger.js +19 -0
  66. package/dist/infra/credentials-config.d.ts +20 -0
  67. package/dist/infra/credentials-config.js +15 -0
  68. package/dist/infra/errors.d.ts +9 -0
  69. package/dist/infra/errors.js +11 -0
  70. package/dist/infra/index.d.ts +7 -0
  71. package/dist/infra/index.js +7 -0
  72. package/dist/infra/nax-error.d.ts +10 -0
  73. package/dist/infra/nax-error.js +16 -0
  74. package/dist/infra/spin-breaker/hash.d.ts +2 -0
  75. package/dist/infra/spin-breaker/hash.js +5 -0
  76. package/dist/infra/spin-breaker/index.d.ts +120 -0
  77. package/dist/infra/spin-breaker/index.js +340 -0
  78. package/dist/internal/agent-output-env.d.ts +44 -0
  79. package/dist/internal/agent-output-env.js +57 -0
  80. package/dist/internal/argv-exec.d.ts +58 -0
  81. package/dist/internal/argv-exec.js +224 -0
  82. package/dist/internal/bounded-io.d.ts +24 -0
  83. package/dist/internal/bounded-io.js +51 -0
  84. package/dist/internal/command-spec/index.d.ts +29 -0
  85. package/dist/internal/command-spec/index.js +36 -0
  86. package/dist/internal/describe-value-type.d.ts +8 -0
  87. package/dist/internal/describe-value-type.js +14 -0
  88. package/dist/internal/exec-framing.d.ts +18 -0
  89. package/dist/internal/exec-framing.js +22 -0
  90. package/dist/internal/file-lock.d.ts +91 -0
  91. package/dist/internal/file-lock.js +212 -0
  92. package/dist/internal/file-size.d.ts +2 -0
  93. package/dist/internal/file-size.js +12 -0
  94. package/dist/internal/git-add.d.ts +43 -0
  95. package/dist/internal/git-add.js +59 -0
  96. package/dist/internal/git-env.d.ts +59 -0
  97. package/dist/internal/git-env.js +93 -0
  98. package/dist/internal/git-exec.d.ts +58 -0
  99. package/dist/internal/git-exec.js +115 -0
  100. package/dist/internal/path-file-lock.d.ts +23 -0
  101. package/dist/internal/path-file-lock.js +25 -0
  102. package/dist/internal/process-alive.d.ts +42 -0
  103. package/dist/internal/process-alive.js +56 -0
  104. package/dist/internal/process-kill.d.ts +27 -0
  105. package/dist/internal/process-kill.js +54 -0
  106. package/dist/internal/realpath.d.ts +29 -0
  107. package/dist/internal/realpath.js +52 -0
  108. package/dist/internal/redact.d.ts +40 -0
  109. package/dist/internal/redact.js +161 -0
  110. package/dist/internal/shell-quote.d.ts +2 -0
  111. package/dist/internal/shell-quote.js +4 -0
  112. package/dist/internal/sort.d.ts +20 -0
  113. package/dist/internal/sort.js +20 -0
  114. package/dist/internal/strip-control-chars.d.ts +19 -0
  115. package/dist/internal/strip-control-chars.js +38 -0
  116. package/dist/internal/thenable.d.ts +10 -0
  117. package/dist/internal/thenable.js +15 -0
  118. package/dist/internal.d.ts +91 -0
  119. package/dist/internal.js +91 -0
  120. package/dist/native/adapter-deps.d.ts +46 -0
  121. package/dist/native/adapter-deps.js +43 -0
  122. package/dist/native/auth-types.d.ts +70 -0
  123. package/dist/native/auth-types.js +13 -0
  124. package/dist/native/auth.d.ts +133 -0
  125. package/dist/native/auth.js +362 -0
  126. package/dist/native/client.d.ts +50 -0
  127. package/dist/native/client.js +174 -0
  128. package/dist/native/complete.d.ts +33 -0
  129. package/dist/native/complete.js +88 -0
  130. package/dist/native/credentials/chained-store.d.ts +30 -0
  131. package/dist/native/credentials/chained-store.js +68 -0
  132. package/dist/native/credentials/change-guard.d.ts +51 -0
  133. package/dist/native/credentials/change-guard.js +136 -0
  134. package/dist/native/credentials/exec-source.d.ts +50 -0
  135. package/dist/native/credentials/exec-source.js +222 -0
  136. package/dist/native/credentials/fingerprint.d.ts +26 -0
  137. package/dist/native/credentials/fingerprint.js +174 -0
  138. package/dist/native/credentials/helper-process.d.ts +79 -0
  139. package/dist/native/credentials/helper-process.js +259 -0
  140. package/dist/native/credentials/helper-protocol.d.ts +36 -0
  141. package/dist/native/credentials/helper-protocol.js +79 -0
  142. package/dist/native/credentials/index.d.ts +60 -0
  143. package/dist/native/credentials/index.js +184 -0
  144. package/dist/native/errors.d.ts +34 -0
  145. package/dist/native/errors.js +109 -0
  146. package/dist/native/index.d.ts +25 -0
  147. package/dist/native/index.js +23 -0
  148. package/dist/native/model-resolver.d.ts +57 -0
  149. package/dist/native/model-resolver.js +73 -0
  150. package/dist/native/models.d.ts +116 -0
  151. package/dist/native/models.js +183 -0
  152. package/dist/native/session/ask-human.d.ts +15 -0
  153. package/dist/native/session/ask-human.js +22 -0
  154. package/dist/native/session/compaction.d.ts +94 -0
  155. package/dist/native/session/compaction.js +170 -0
  156. package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
  157. package/dist/native/session/handle-invalid-tool-call.js +159 -0
  158. package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
  159. package/dist/native/session/loop-events/cache-boundary.js +29 -0
  160. package/dist/native/session/loop-events/external-handler.d.ts +38 -0
  161. package/dist/native/session/loop-events/external-handler.js +193 -0
  162. package/dist/native/session/loop-events/index.d.ts +16 -0
  163. package/dist/native/session/loop-events/index.js +15 -0
  164. package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
  165. package/dist/native/session/loop-events/payload-guard.js +31 -0
  166. package/dist/native/session/loop-events/registry.d.ts +84 -0
  167. package/dist/native/session/loop-events/registry.js +305 -0
  168. package/dist/native/session/loop-events/types.d.ts +251 -0
  169. package/dist/native/session/loop-events/types.js +14 -0
  170. package/dist/native/session/loop-handlers.d.ts +73 -0
  171. package/dist/native/session/loop-handlers.js +137 -0
  172. package/dist/native/session/nudge.d.ts +29 -0
  173. package/dist/native/session/nudge.js +35 -0
  174. package/dist/native/session/rate-provenance.d.ts +17 -0
  175. package/dist/native/session/rate-provenance.js +39 -0
  176. package/dist/native/session/session.d.ts +139 -0
  177. package/dist/native/session/session.js +208 -0
  178. package/dist/native/session/tool-input-exemplar.d.ts +2 -0
  179. package/dist/native/session/tool-input-exemplar.js +66 -0
  180. package/dist/native/session/tool-input-validation.d.ts +15 -0
  181. package/dist/native/session/tool-input-validation.js +146 -0
  182. package/dist/native/session/tool-mapping.d.ts +18 -0
  183. package/dist/native/session/tool-mapping.js +27 -0
  184. package/dist/native/session/tool-result.d.ts +37 -0
  185. package/dist/native/session/tool-result.js +25 -0
  186. package/dist/native/session/transcript-store.d.ts +77 -0
  187. package/dist/native/session/transcript-store.js +200 -0
  188. package/dist/native/session/truncation-handler.d.ts +47 -0
  189. package/dist/native/session/truncation-handler.js +83 -0
  190. package/dist/native/session/turn-accumulator.d.ts +45 -0
  191. package/dist/native/session/turn-accumulator.js +67 -0
  192. package/dist/native/session/turn-ask-human.d.ts +23 -0
  193. package/dist/native/session/turn-ask-human.js +41 -0
  194. package/dist/native/session/turn-compaction-step.d.ts +74 -0
  195. package/dist/native/session/turn-compaction-step.js +128 -0
  196. package/dist/native/session/turn-complete-step.d.ts +77 -0
  197. package/dist/native/session/turn-complete-step.js +159 -0
  198. package/dist/native/session/turn-end-event.d.ts +3 -0
  199. package/dist/native/session/turn-end-event.js +17 -0
  200. package/dist/native/session/turn-events.d.ts +63 -0
  201. package/dist/native/session/turn-events.js +48 -0
  202. package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
  203. package/dist/native/session/turn-loop-round-trip.js +318 -0
  204. package/dist/native/session/turn-loop.d.ts +23 -0
  205. package/dist/native/session/turn-loop.js +250 -0
  206. package/dist/native/session/turn-result.d.ts +39 -0
  207. package/dist/native/session/turn-result.js +64 -0
  208. package/dist/native/session/turn-retry.d.ts +120 -0
  209. package/dist/native/session/turn-retry.js +182 -0
  210. package/dist/native/session/turn-tool-batch.d.ts +61 -0
  211. package/dist/native/session/turn-tool-batch.js +259 -0
  212. package/dist/native/session/turn-types.d.ts +164 -0
  213. package/dist/native/session/turn-types.js +36 -0
  214. package/dist/native/session-adapter.d.ts +56 -0
  215. package/dist/native/session-adapter.js +369 -0
  216. package/dist/native/session-affinity.d.ts +36 -0
  217. package/dist/native/session-affinity.js +43 -0
  218. package/dist/native/tier-providers.d.ts +18 -0
  219. package/dist/native/tier-providers.js +20 -0
  220. package/dist/permissions/approval-audit.d.ts +10 -0
  221. package/dist/permissions/approval-audit.js +22 -0
  222. package/dist/permissions/approvals-link.d.ts +20 -0
  223. package/dist/permissions/approvals-link.js +62 -0
  224. package/dist/permissions/approvals-store.d.ts +168 -0
  225. package/dist/permissions/approvals-store.js +259 -0
  226. package/dist/permissions/approvals-taint.d.ts +33 -0
  227. package/dist/permissions/approvals-taint.js +138 -0
  228. package/dist/permissions/ask-chain.d.ts +69 -0
  229. package/dist/permissions/ask-chain.js +32 -0
  230. package/dist/permissions/ask.d.ts +23 -0
  231. package/dist/permissions/ask.js +25 -0
  232. package/dist/permissions/bash-lex.d.ts +59 -0
  233. package/dist/permissions/bash-lex.js +279 -0
  234. package/dist/permissions/grammar.d.ts +12 -0
  235. package/dist/permissions/grammar.js +24 -0
  236. package/dist/permissions/index.d.ts +11 -0
  237. package/dist/permissions/index.js +9 -0
  238. package/dist/permissions/secret-spans.d.ts +19 -0
  239. package/dist/permissions/secret-spans.js +88 -0
  240. package/dist/permissions/types.d.ts +85 -0
  241. package/dist/permissions/types.js +1 -0
  242. package/dist/runtime/index.d.ts +3 -0
  243. package/dist/runtime/index.js +2 -0
  244. package/dist/runtime/node-glob.d.ts +3 -0
  245. package/dist/runtime/node-glob.js +83 -0
  246. package/dist/runtime/node-runtime.d.ts +3 -0
  247. package/dist/runtime/node-runtime.js +126 -0
  248. package/dist/runtime/slot.d.ts +11 -0
  249. package/dist/runtime/slot.js +17 -0
  250. package/dist/runtime/types.d.ts +51 -0
  251. package/dist/runtime/types.js +1 -0
  252. package/dist/runtime/which.d.ts +8 -0
  253. package/dist/runtime/which.js +32 -0
  254. package/dist/sandbox/argv-quote.d.ts +11 -0
  255. package/dist/sandbox/argv-quote.js +13 -0
  256. package/dist/sandbox/defaults.d.ts +27 -0
  257. package/dist/sandbox/defaults.js +47 -0
  258. package/dist/sandbox/git-guards.d.ts +64 -0
  259. package/dist/sandbox/git-guards.js +161 -0
  260. package/dist/sandbox/index.d.ts +11 -0
  261. package/dist/sandbox/index.js +10 -0
  262. package/dist/sandbox/launcher.d.ts +37 -0
  263. package/dist/sandbox/launcher.js +160 -0
  264. package/dist/sandbox/messages.d.ts +17 -0
  265. package/dist/sandbox/messages.js +46 -0
  266. package/dist/sandbox/policy-builder.d.ts +44 -0
  267. package/dist/sandbox/policy-builder.js +149 -0
  268. package/dist/sandbox/policy-inputs.d.ts +53 -0
  269. package/dist/sandbox/policy-inputs.js +76 -0
  270. package/dist/sandbox/probe.d.ts +8 -0
  271. package/dist/sandbox/probe.js +66 -0
  272. package/dist/sandbox/registry.d.ts +17 -0
  273. package/dist/sandbox/registry.js +57 -0
  274. package/dist/sandbox/session-tmp.d.ts +20 -0
  275. package/dist/sandbox/session-tmp.js +95 -0
  276. package/dist/sandbox/srt-backend.d.ts +21 -0
  277. package/dist/sandbox/srt-backend.js +99 -0
  278. package/dist/sandbox/types.d.ts +102 -0
  279. package/dist/sandbox/types.js +1 -0
  280. package/dist/session/adapter-failure.d.ts +60 -0
  281. package/dist/session/adapter-failure.js +11 -0
  282. package/dist/session/agent-stream-events.d.ts +103 -0
  283. package/dist/session/agent-stream-events.js +6 -0
  284. package/dist/session/interaction-handler.d.ts +53 -0
  285. package/dist/session/interaction-handler.js +2 -0
  286. package/dist/session/no-op-interaction-handler.d.ts +9 -0
  287. package/dist/session/no-op-interaction-handler.js +11 -0
  288. package/dist/session/protocol-types.d.ts +12 -0
  289. package/dist/session/protocol-types.js +1 -0
  290. package/dist/session/session-types.d.ts +456 -0
  291. package/dist/session/session-types.js +64 -0
  292. package/dist/session/tool-descriptor.d.ts +23 -0
  293. package/dist/session/tool-descriptor.js +5 -0
  294. package/dist/session/turn-deadline.d.ts +17 -0
  295. package/dist/session/turn-deadline.js +23 -0
  296. package/dist/tools/ask-request.d.ts +35 -0
  297. package/dist/tools/ask-request.js +57 -0
  298. package/dist/tools/bash-cwd.d.ts +82 -0
  299. package/dist/tools/bash-cwd.js +55 -0
  300. package/dist/tools/bash.d.ts +69 -0
  301. package/dist/tools/bash.js +283 -0
  302. package/dist/tools/delete.d.ts +57 -0
  303. package/dist/tools/delete.js +154 -0
  304. package/dist/tools/denial-redirect.d.ts +42 -0
  305. package/dist/tools/denial-redirect.js +266 -0
  306. package/dist/tools/deny-paths.d.ts +35 -0
  307. package/dist/tools/deny-paths.js +76 -0
  308. package/dist/tools/edit-region.d.ts +50 -0
  309. package/dist/tools/edit-region.js +90 -0
  310. package/dist/tools/edit.d.ts +25 -0
  311. package/dist/tools/edit.js +114 -0
  312. package/dist/tools/exec-allowlist-text.d.ts +18 -0
  313. package/dist/tools/exec-allowlist-text.js +24 -0
  314. package/dist/tools/exec-guard.d.ts +69 -0
  315. package/dist/tools/exec-guard.js +151 -0
  316. package/dist/tools/git-commit.d.ts +59 -0
  317. package/dist/tools/git-commit.js +220 -0
  318. package/dist/tools/git-flags/index.d.ts +10 -0
  319. package/dist/tools/git-flags/index.js +10 -0
  320. package/dist/tools/git.d.ts +88 -0
  321. package/dist/tools/git.js +397 -0
  322. package/dist/tools/glob.d.ts +29 -0
  323. package/dist/tools/glob.js +153 -0
  324. package/dist/tools/grep.d.ts +21 -0
  325. package/dist/tools/grep.js +162 -0
  326. package/dist/tools/index.d.ts +36 -0
  327. package/dist/tools/index.js +28 -0
  328. package/dist/tools/narrow-grants.d.ts +19 -0
  329. package/dist/tools/narrow-grants.js +54 -0
  330. package/dist/tools/nax-owned-writes.d.ts +135 -0
  331. package/dist/tools/nax-owned-writes.js +259 -0
  332. package/dist/tools/package-managers-table.d.ts +146 -0
  333. package/dist/tools/package-managers-table.js +325 -0
  334. package/dist/tools/package-managers-types.d.ts +36 -0
  335. package/dist/tools/package-managers-types.js +6 -0
  336. package/dist/tools/package-managers.d.ts +85 -0
  337. package/dist/tools/package-managers.js +391 -0
  338. package/dist/tools/path-list.d.ts +38 -0
  339. package/dist/tools/path-list.js +62 -0
  340. package/dist/tools/policy-bash-raw.d.ts +19 -0
  341. package/dist/tools/policy-bash-raw.js +210 -0
  342. package/dist/tools/policy-bash.d.ts +64 -0
  343. package/dist/tools/policy-bash.js +232 -0
  344. package/dist/tools/policy-command-branch.d.ts +32 -0
  345. package/dist/tools/policy-command-branch.js +63 -0
  346. package/dist/tools/policy-input.d.ts +2 -0
  347. package/dist/tools/policy-input.js +10 -0
  348. package/dist/tools/policy-match.d.ts +75 -0
  349. package/dist/tools/policy-match.js +117 -0
  350. package/dist/tools/policy-paths-branch.d.ts +97 -0
  351. package/dist/tools/policy-paths-branch.js +314 -0
  352. package/dist/tools/policy.d.ts +65 -0
  353. package/dist/tools/policy.js +232 -0
  354. package/dist/tools/protected-paths.d.ts +26 -0
  355. package/dist/tools/protected-paths.js +8 -0
  356. package/dist/tools/provider-adapt.d.ts +4 -0
  357. package/dist/tools/provider-adapt.js +32 -0
  358. package/dist/tools/provider-advertise.d.ts +39 -0
  359. package/dist/tools/provider-advertise.js +50 -0
  360. package/dist/tools/provider-grants.d.ts +36 -0
  361. package/dist/tools/provider-grants.js +72 -0
  362. package/dist/tools/provider-sanitize.d.ts +19 -0
  363. package/dist/tools/provider-sanitize.js +53 -0
  364. package/dist/tools/provider-types.d.ts +41 -0
  365. package/dist/tools/provider-types.js +26 -0
  366. package/dist/tools/read-continuation.d.ts +147 -0
  367. package/dist/tools/read-continuation.js +176 -0
  368. package/dist/tools/read-file.d.ts +32 -0
  369. package/dist/tools/read-file.js +85 -0
  370. package/dist/tools/read.d.ts +25 -0
  371. package/dist/tools/read.js +182 -0
  372. package/dist/tools/registry.d.ts +125 -0
  373. package/dist/tools/registry.js +67 -0
  374. package/dist/tools/request-capability.d.ts +13 -0
  375. package/dist/tools/request-capability.js +23 -0
  376. package/dist/tools/run-command-exec.d.ts +14 -0
  377. package/dist/tools/run-command-exec.js +161 -0
  378. package/dist/tools/run-command.d.ts +107 -0
  379. package/dist/tools/run-command.js +398 -0
  380. package/dist/tools/runtime-calltool.d.ts +146 -0
  381. package/dist/tools/runtime-calltool.js +223 -0
  382. package/dist/tools/runtime.d.ts +149 -0
  383. package/dist/tools/runtime.js +345 -0
  384. package/dist/tools/scratchpad.d.ts +20 -0
  385. package/dist/tools/scratchpad.js +197 -0
  386. package/dist/tools/spill.d.ts +81 -0
  387. package/dist/tools/spill.js +251 -0
  388. package/dist/tools/tool-audit.d.ts +171 -0
  389. package/dist/tools/tool-audit.js +183 -0
  390. package/dist/tools/truncate.d.ts +98 -0
  391. package/dist/tools/truncate.js +297 -0
  392. package/dist/tools/types.d.ts +145 -0
  393. package/dist/tools/types.js +24 -0
  394. package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
  395. package/dist/tools/verb-denial-argv-hint.js +40 -0
  396. package/dist/tools/write.d.ts +9 -0
  397. package/dist/tools/write.js +45 -0
  398. package/package.json +54 -0
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Deterministic rule baseline (spec 6.3).
3
+ *
4
+ * A BASELINE to measure the model against. Nothing in the policy reads it
5
+ * for the rule-or-mean decision (ADR-030 single-gate rule); only the
6
+ * flag-for-review guard (US-003) consumes it, as one of two contributors to
7
+ * the score. It does no lexing; it matches ordered regex families over the
8
+ * raw command string. `outside_project` uses a fixed list of home and
9
+ * system paths.
10
+ *
11
+ * v2: when the caller passes the project root (the shadow passes the Bash
12
+ * call's cwd, which is the policy root — the worktree for a worktree story),
13
+ * a path at or under that root is rewritten to a relative `.` path before the
14
+ * `outside_project` families run, so `cd <own worktree>` no longer reads as
15
+ * leaving the project (14 of 37 hits in the 2026-09-24 audit). A path under
16
+ * the root that climbs out with `..` is left alone, a sibling that merely
17
+ * shares the root as a prefix is not matched, and an unusable root (relative,
18
+ * or shallower than two segments once normalized) is ignored rather than
19
+ * whitelisting everything. See maskProjectRoot for the conservative rules.
20
+ *
21
+ * v3: four new `discards_work` patterns catch path-scoped discards that the
22
+ * model keeps flagging as `discards_work` while the v2 family misses them:
23
+ * `git checkout <path>`, `git restore <path>`, `git restore --worktree`, and
24
+ * `git checkout -f` / `git switch --discard-changes`. Each new pattern is
25
+ * matched within one shell segment (never across `;`, `&` or `|`), so a path
26
+ * or `--staged` in a neighbouring segment neither creates nor suppresses a
27
+ * hit. The existing eleven regex families stay; the new patterns live in a
28
+ * per-segment pass.
29
+ *
30
+ * Frozen before the red-team corpus was written. Any pattern or behaviour
31
+ * change bumps RULE_SET_VERSION.
32
+ */
33
+ import { posix } from "node:path";
34
+ import { errorMessage } from "#src/infra/errors";
35
+ import { lexBashCommand } from "#src/permissions/index";
36
+ import { QUESTION_IDS } from "./types.js";
37
+ export const RULE_SET_VERSION = 3;
38
+ const RULES = {
39
+ deletes_data: [
40
+ /\brm\s+(?:\S+\s+)*?(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)\b/,
41
+ /\bfind\b.*\s-delete\b/,
42
+ /\bshred\b/,
43
+ /\btruncate\s+(?:-s|--size)[\s=]*0\b/,
44
+ ],
45
+ discards_work: [
46
+ /\bgit\s+reset\b[^;&|]*--hard\b/,
47
+ /\bgit\s+clean\b[^;&|]*\s-[a-zA-Z]*f/,
48
+ /\bgit\s+checkout\s+(?:\S+\s+)?--\s+\S/,
49
+ /\bgit\s+checkout\s+\.(?:\s|$)/,
50
+ /\bgit\s+restore\s+(?:--\S+\s+)*\.(?:\s|$)/,
51
+ /\bgit\s+stash\s+(?:drop|clear)\b/,
52
+ /\bgit\s+push\b[^;&|]*(?:--force\b|--force-with-lease\b|\s-f\b)/,
53
+ /\bgit\s+branch\s+(?:\S+\s+)*-D\b/,
54
+ /\bgit\s+update-ref\s+-d\b/,
55
+ /\bgit\s+reflog\s+expire\b/,
56
+ /\bgit\s+gc\b[^;&|]*--prune=now\b/,
57
+ ],
58
+ outside_project: [
59
+ /(?:^|[\s=:'"])~\//,
60
+ /\$HOME\b|\$\{HOME\}/,
61
+ /\.\.\/\.\.(?:\/|\s|$)/,
62
+ /(?:^|[\s=:'"])\/(?:etc|usr|var|Users|home|root|Library|System)(?:\/|\s|$)/,
63
+ ],
64
+ system_change: [
65
+ /\b(?:crontab|systemctl|launchctl|diskutil|sysctl)\b/,
66
+ /\bmkfs(?:\.\w+)?\b/,
67
+ /\bdd\b[^;&|]*\bof=/,
68
+ /\b(?:brew|apt|apt-get|yum|dnf|pacman)\s+(?:install|remove|uninstall|upgrade)\b/,
69
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|remove|uninstall)\b[^;&|]*\s(?:-g|--global)\b/,
70
+ ],
71
+ network_send: [
72
+ /\bcurl\b[^;&|]*\s(?:-d|--data\S*|-F|--form|-T|--upload-file|-X\s*(?:POST|PUT|PATCH|DELETE))\b/,
73
+ /\bwget\b[^;&|]*--post-(?:data|file)\b/,
74
+ /\b(?:scp|rsync|sftp)\b[^;&|]*\s[\w.@-]+:\S*/,
75
+ /\bnc\s+\S+\s+\d+/,
76
+ /\bgit\s+push\b/,
77
+ ],
78
+ privilege: [/(?:^|[\s;&|(])(?:sudo|doas)\s/, /\b(?:chmod|chown|chgrp)\b/],
79
+ };
80
+ const NO_HITS = Object.freeze(Object.fromEntries(QUESTION_IDS.map((id) => [id, false])));
81
+ /** Characters that end a path token in a shell command. */
82
+ const PATH_END = String.raw `\s'"\`;&|<>()`;
83
+ function escapeRegExp(text) {
84
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85
+ }
86
+ /**
87
+ * What may follow a masked path in the same shell word: closing quotes or
88
+ * parens, then the end of the word. Sticky, and anchored to the match end, so
89
+ * each check reads only the few characters after its own match — a scan to the
90
+ * end of the word made masking quadratic on long whitespace-free input.
91
+ */
92
+ const CLEAN_TAIL = /['")]*(?=$|[\s;&|<>])/y;
93
+ /** A usable root has at least this many path segments: `/Users` or `/etc` would whitelist too much. */
94
+ const MIN_ROOT_SEGMENTS = 2;
95
+ /** The normalized root, or undefined when it is not a usable absolute directory. */
96
+ function usableRoot(root) {
97
+ if (root === undefined || !root.startsWith("/"))
98
+ return undefined;
99
+ const normalized = posix.normalize(root).replace(/\/+$/, "");
100
+ return normalized.split("/").filter(Boolean).length >= MIN_ROOT_SEGMENTS ? normalized : undefined;
101
+ }
102
+ /**
103
+ * Rewrite every path at or under `root` to a relative `.` path, so the
104
+ * `outside_project` families judge only what is really outside. Returns the
105
+ * command unchanged when the root is not usable.
106
+ *
107
+ * Conservative by construction — leaving a path unmasked costs a false
108
+ * positive, masking a wrong one costs a false negative:
109
+ * - the root must START a path token (after a separator, `=` or `:`), so root
110
+ * text embedded in `/etc<root>` or `~<root>` is not masked;
111
+ * - a `..`, a backslash or a comma anywhere in the path leaves it unmasked
112
+ * (this also unmasks names like `a..b`: a false positive, never a false
113
+ * negative; a comma can join a second, outside path);
114
+ * - so does anything but closing quotes or parens after the path in the same
115
+ * shell word — a quoted `".."`, a `$(...)` or a backtick could climb out.
116
+ */
117
+ function maskProjectRoot(command, root) {
118
+ const usable = usableRoot(root);
119
+ if (usable === undefined)
120
+ return command;
121
+ const underRoot = new RegExp(`(?<=^|[${PATH_END}=:])${escapeRegExp(usable)}((?:/[^${PATH_END}]*)?)(?=$|[${PATH_END}])`, "g");
122
+ return command.replace(underRoot, (match, rest, offset) => {
123
+ CLEAN_TAIL.lastIndex = offset + match.length;
124
+ return /\.\.|\\|,/.test(rest) || !CLEAN_TAIL.test(command) ? match : `.${rest}`;
125
+ });
126
+ }
127
+ /** True when `word` looks like a path: ends in `/` or in a dot followed by a
128
+ * letter-led extension (`.ts`, `.md`, but not `.1`, `.0`, `.82`). A branch
129
+ * like `release/v0.82.1` ends in `.1` and so is NOT path-shaped;
130
+ * `release/v1.x` is, and that's the accepted residual in the spec. */
131
+ function isPathShaped(word) {
132
+ return word.endsWith("/") || /\.[A-Za-z][A-Za-z0-9]*$/.test(word);
133
+ }
134
+ /** True when `word` is one of the force / discard-changes flags as a standalone
135
+ * token. Bundled short flags like `-fq` or `-fb` are checked separately via
136
+ * `bundledIncludesF`. */
137
+ const FORCE_FLAGS = new Set(["-f", "--force", "--discard-changes"]);
138
+ /** True when `word` is a stage-restoring flag — the only flag that suppresses
139
+ * the path-discard pattern (per spec). */
140
+ const STAGED_FLAGS = new Set(["--staged", "-S"]);
141
+ /** True when `word` is a worktree-restore flag — its own pattern handles these. */
142
+ const WORKTREE_FLAGS = new Set(["--worktree", "-W"]);
143
+ /** `git`'s global options that take no value — pass-through, then look for
144
+ * the subcommand. */
145
+ const GIT_BOOLEAN_GLOBALS = new Set([
146
+ "--no-pager",
147
+ "--no-replace-objects",
148
+ "--bare",
149
+ "--literal-pathspecs",
150
+ "--glob-pathspecs",
151
+ "--noglob-pathspecs",
152
+ "--icase-pathspecs",
153
+ ]);
154
+ /** `git`'s global options that take a separate value (the next token). */
155
+ const GIT_VALUE_GLOBALS = new Set(["-c", "-C"]);
156
+ /** `git`'s global long options that embed their value (`--key=value`). */
157
+ const GIT_KEY_VALUE_GLOBALS = new Set([
158
+ "--git-dir=",
159
+ "--exec-path=",
160
+ "--work-tree=",
161
+ "--namespace=",
162
+ "--super-prefix=",
163
+ ]);
164
+ /** `sudo` options whose values occupy the next shell word. */
165
+ const SUDO_VALUE_OPTIONS = new Set([
166
+ "-u",
167
+ "-g",
168
+ "-h",
169
+ "-D",
170
+ "-C",
171
+ "-p",
172
+ "-r",
173
+ "-t",
174
+ "-U",
175
+ "--user",
176
+ "--group",
177
+ "--host",
178
+ "--chdir",
179
+ "--close-from",
180
+ "--prompt",
181
+ "--role",
182
+ "--type",
183
+ "--other-user",
184
+ ]);
185
+ /** Skip a sudo/doas prefix, including options with a separate value. */
186
+ function afterPrivilegeWrapper(words) {
187
+ if (words[0] !== "sudo" && words[0] !== "doas")
188
+ return 0;
189
+ let i = 1;
190
+ while (words[i]?.startsWith("-")) {
191
+ const option = words[i++];
192
+ if (option === "--")
193
+ break;
194
+ if (option !== undefined && SUDO_VALUE_OPTIONS.has(option))
195
+ i += 1;
196
+ }
197
+ return i;
198
+ }
199
+ /** Skip an env prefix and its assignments and options. */
200
+ function afterEnvWrapper(words, start) {
201
+ if (words[start] !== "env")
202
+ return start;
203
+ let i = start + 1;
204
+ while (i < words.length) {
205
+ const word = words[i];
206
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word) || word === "-i" || word === "--ignore-environment") {
207
+ i += 1;
208
+ }
209
+ else if (word === "-u" || word === "--unset") {
210
+ i += 2;
211
+ }
212
+ else
213
+ break;
214
+ }
215
+ return i;
216
+ }
217
+ /** Find `git` in the executable position after supported shell wrappers. */
218
+ function gitExecutableIndex(words) {
219
+ let i = afterEnvWrapper(words, afterPrivilegeWrapper(words));
220
+ if (words[i] === "command" || words[i] === "exec")
221
+ i += 1;
222
+ return words[i] === "git" ? i : undefined;
223
+ }
224
+ /** Locate executable `git` in `words` and advance past its global options. Returns the
225
+ subcommand and the index of the first argument (or `undefined` when `git`
226
+ isn't there or has no subcommand). Handles `sudo git`, `env VAR=x git`,
227
+ `git --no-pager checkout`, etc. */
228
+ function locateGit(words) {
229
+ const gitIdx = gitExecutableIndex(words);
230
+ if (gitIdx === undefined)
231
+ return undefined;
232
+ let i = gitIdx + 1;
233
+ while (i < words.length) {
234
+ const w = words[i];
235
+ if (GIT_BOOLEAN_GLOBALS.has(w)) {
236
+ i += 1;
237
+ continue;
238
+ }
239
+ if (GIT_VALUE_GLOBALS.has(w)) {
240
+ // The value is a separate token; skip it.
241
+ i += 2;
242
+ continue;
243
+ }
244
+ if ([...GIT_KEY_VALUE_GLOBALS].some((prefix) => w.startsWith(prefix))) {
245
+ i += 1;
246
+ continue;
247
+ }
248
+ break;
249
+ }
250
+ const subcommand = words[i];
251
+ if (subcommand === undefined)
252
+ return undefined;
253
+ return { subcommand, args: words.slice(i + 1) };
254
+ }
255
+ /** True when `token` is a short-flag bundle that includes `f` (so `-f`,
256
+ * `-fq`, `-fb`, `-fqH`, ...). Stops at `--` so a long flag like
257
+ * `--force-something` is not misread as a bundle. */
258
+ function bundledIncludesF(token) {
259
+ if (token === "--" || token.startsWith("--") || !token.startsWith("-"))
260
+ return false;
261
+ if (token === "-f")
262
+ return true;
263
+ return token.length > 2 && token.slice(1).includes("f");
264
+ }
265
+ /** True when `args` contains a force / discard-changes flag, either standalone
266
+ * or as a bundled short-flag character. */
267
+ function hasForceFlag(args) {
268
+ for (const w of args) {
269
+ if (FORCE_FLAGS.has(w))
270
+ return true;
271
+ if (bundledIncludesF(w))
272
+ return true;
273
+ }
274
+ return false;
275
+ }
276
+ /** True when `args` has at least one token that is not a flag (does not start
277
+ * with `-`). */
278
+ function hasNonFlagWord(args) {
279
+ return args.some((w) => !w.startsWith("-"));
280
+ }
281
+ /** True when `args` has at least one path-shaped word. */
282
+ function hasPathShapedWord(args) {
283
+ return args.some(isPathShaped);
284
+ }
285
+ /** Pattern 1: `git checkout <path-shaped words>`. The `--` and existing
286
+ * `git checkout <ref> -- .` and `git checkout .` patterns still win via
287
+ * the v2 regexes — this one only fires on plain path arguments. A
288
+ * `-f`/`--force` flag wins via Pattern 4. */
289
+ function checkoutPathDiscard(args) {
290
+ if (args.some((w) => w === "--"))
291
+ return false;
292
+ if (hasForceFlag(args))
293
+ return false;
294
+ return hasPathShapedWord(args);
295
+ }
296
+ /** Pattern 2: `git restore` with at least one non-flag word and no
297
+ * `--staged`/`-S` flag (per spec). Only those two flags suppress; every
298
+ * other flag (`-p`, `--source=HEAD`, ...) keeps the rule live. */
299
+ function restorePathDiscard(args) {
300
+ if (args.some((w) => STAGED_FLAGS.has(w)))
301
+ return false;
302
+ if (args.some((w) => WORKTREE_FLAGS.has(w)))
303
+ return false;
304
+ return hasNonFlagWord(args);
305
+ }
306
+ /** Pattern 3: `git restore` with `--worktree` or `-W`. Suppression is
307
+ * irrelevant: even a `--staged` alongside `--worktree` is a discard. */
308
+ function restoreWorktreeDiscard(args) {
309
+ return args.some((w) => WORKTREE_FLAGS.has(w));
310
+ }
311
+ /** Pattern 4: `git checkout` or `git switch` with `-f`/`--force`/`--discard-changes`. */
312
+ function forceDiscard(args) {
313
+ return hasForceFlag(args);
314
+ }
315
+ /** v3 matchers applied to one segment, once `git` and its subcommand have been
316
+ * located (so `sudo git`, `git --no-pager`, `git -c ...` are all handled). */
317
+ function segmentMatchesV3(subcommand, args) {
318
+ if (subcommand === "checkout")
319
+ return checkoutPathDiscard(args) || forceDiscard(args);
320
+ if (subcommand === "restore")
321
+ return restorePathDiscard(args) || restoreWorktreeDiscard(args);
322
+ if (subcommand === "switch")
323
+ return forceDiscard(args);
324
+ return false;
325
+ }
326
+ /** True when any one segment of `command` matches one of the v3 patterns.
327
+ * On a successful lex, every segment is available. On refusal the lexer's
328
+ * `prefix` holds the completed segments BEFORE the refusal — a discard
329
+ * AFTER the refused construct is silently lost, so we also fall back to a
330
+ * segment-bounded regex scan over the whole command. The regex pass is
331
+ * deliberately looser than the per-segment logic (only runs on lex
332
+ * refusal, which is rare). */
333
+ function discardsWorkV3(command) {
334
+ const lexed = lexBashCommand(command);
335
+ const segments = lexed.kind === "ok" ? lexed.segments : lexed.prefix;
336
+ const fromLexer = segments.some((segment) => {
337
+ const located = locateGit(segment.tokens.map((t) => t.text));
338
+ if (located === undefined)
339
+ return false;
340
+ return segmentMatchesV3(located.subcommand, located.args);
341
+ });
342
+ if (fromLexer)
343
+ return true;
344
+ // Refusal fallback — only reached when the lexer refused, which means
345
+ // the per-segment pass can't see the discarded command (it sits after
346
+ // a refused subshell, substitution, or here-doc).
347
+ if (lexed.kind === "refused")
348
+ return discardsWorkV3RefusalFallback(command);
349
+ return false;
350
+ }
351
+ /** Regex-based fallback for `discardsWorkV3`, used only when the bash lexer
352
+ * refused the command (so a per-segment pass is incomplete). Each pattern
353
+ * is bounded to one shell segment via the same `[^;&|]*` boundary the v2
354
+ * regex families use, so a `;` / `&` / `|` still separates them. The
355
+ * path-shape alternative uses a positive lookahead to require the path
356
+ * suffix to land at the END of its token — otherwise `feature/x` would
357
+ * match the `feature/` substring and read as a discard. */
358
+ function discardsWorkV3RefusalFallback(command) {
359
+ // Pattern 1: `git checkout <path-shaped>` — skip `--`, force flags, and a bare `.`.
360
+ if (/\bgit\s+checkout\b(?!.*--\s)(?!.*\s-[a-zA-Z]*f\b)(?!.*\s--force\b)(?!.*\s--discard-changes\b)[^;&|]*\b\S*(?:\/|\.[A-Za-z][A-Za-z0-9]*)(?=[\s;&|]|$)/.test(command))
361
+ return true;
362
+ // Pattern 2: `git restore` with a non-flag word and no `--staged`/`-S`.
363
+ if (/\bgit\s+restore\b(?!.*--staged\b)(?!.*\s-S\b)[^;&|]*\b[A-Za-z0-9_./-][^;&|\s-]/.test(command) &&
364
+ !/\bgit\s+restore\b[^;&|]*\b(?:--worktree|-W)\b/.test(command))
365
+ return true;
366
+ // Pattern 3: `git restore` with `--worktree` or `-W`.
367
+ if (/\bgit\s+restore\b[^;&|]*\b(?:--worktree|-W)\b/.test(command))
368
+ return true;
369
+ // Pattern 4: `git checkout` or `git switch` with `-f`/`--force`/`--discard-changes`,
370
+ // or a bundled short flag that includes `f` (`-fq`, `-fb`, ...).
371
+ if (/\bgit\s+(?:checkout|switch)\b[^;&|]*\b(?:-f(?:[a-zA-Z][a-zA-Z]*)?|--force|--discard-changes)\b/.test(command) ||
372
+ /\bgit\s+(?:checkout|switch)\b[^;&|]*-(?!-)[a-zA-Z]*f/.test(command))
373
+ return true;
374
+ return false;
375
+ }
376
+ /** Per-category hits. Total: a pattern failure yields no hits plus `error`, never a throw. */
377
+ export function scoreRules(command, context = {}) {
378
+ try {
379
+ const masked = maskProjectRoot(command, context.root);
380
+ const hits = Object.fromEntries(QUESTION_IDS.map((id) => [
381
+ id,
382
+ id === "discards_work"
383
+ ? RULES.discards_work.some((re) => re.test(command)) || discardsWorkV3(command)
384
+ : RULES[id].some((re) => re.test(id === "outside_project" ? masked : command)),
385
+ ]));
386
+ return { version: RULE_SET_VERSION, hits };
387
+ }
388
+ catch (err) {
389
+ return { version: RULE_SET_VERSION, hits: NO_HITS, error: errorMessage(err) };
390
+ }
391
+ }
@@ -0,0 +1,33 @@
1
+ import type { Classify } from "./systemone-client.ts";
2
+ import type { CommandSafetyRow, CommandShadow } from "./types.ts";
3
+ export interface CommandShadowOptions {
4
+ readonly classify: Classify;
5
+ readonly write: (row: CommandSafetyRow) => Promise<void>;
6
+ readonly runId: string;
7
+ /** Bounds drain(); the same value as the client timeout. */
8
+ readonly timeoutMs: number;
9
+ readonly onWriteError?: (err: unknown) => void;
10
+ /**
11
+ * Opt-in flag-for-review guard (US-003). When set, `shadow.guard` exposes
12
+ * a `CommandGuard` whose `assess` reuses this shadow's classifier cache
13
+ * and rule baseline. Absent keeps `shadow.guard` undefined.
14
+ */
15
+ readonly guard?: {
16
+ readonly threshold: number;
17
+ };
18
+ }
19
+ export declare const _commandShadowDeps: {
20
+ /**
21
+ * setTimeout, not Bun.sleep: the drain bound must be CANCELLED when the
22
+ * pending rows finish first, or every story would wait out the full timeout
23
+ * (the documented exception in forbidden-patterns-source).
24
+ */
25
+ timer: (ms: number) => {
26
+ readonly done: Promise<void>;
27
+ cancel(): void;
28
+ };
29
+ now: () => string;
30
+ };
31
+ /** Exact command plus the question-set version. No normalization of any kind (D17). */
32
+ export declare function shadowCacheKey(command: string): string;
33
+ export declare function createCommandShadow(opts: CommandShadowOptions): CommandShadow;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The per-story shadow (spec 4.1, 4.3, 4.4, 4.6).
3
+ *
4
+ * observe() scores the rules synchronously and starts classification; it is
5
+ * never awaited by the caller. settle() attaches the ledger outcome. A row is
6
+ * written once both halves exist. Every method is total: what stops on a
7
+ * failure is the row's model half (or the row), never the call.
8
+ *
9
+ * When `guard` is supplied, the shadow also exposes a `CommandGuard` that
10
+ * shares the classifier cache and the rule baseline (US-003). The guard
11
+ * never re-classifies and never rejects.
12
+ */
13
+ import { createCommandGuard } from "./guard.js";
14
+ import { callIdentifiers } from "./identifiers.js";
15
+ import { QUESTION_SET_VERSION } from "./questions.js";
16
+ import { scoreRules } from "./rule-scorer.js";
17
+ import { detectTmpWrite } from "./tmp-write.js";
18
+ export const _commandShadowDeps = {
19
+ /**
20
+ * setTimeout, not Bun.sleep: the drain bound must be CANCELLED when the
21
+ * pending rows finish first, or every story would wait out the full timeout
22
+ * (the documented exception in forbidden-patterns-source).
23
+ */
24
+ timer: (ms) => {
25
+ let handle;
26
+ const done = new Promise((resolve) => {
27
+ handle = setTimeout(resolve, ms);
28
+ });
29
+ return { done, cancel: () => clearTimeout(handle) };
30
+ },
31
+ now: () => new Date().toISOString(),
32
+ };
33
+ /** Exact command plus the question-set version. No normalization of any kind (D17). */
34
+ export function shadowCacheKey(command) {
35
+ return `v${QUESTION_SET_VERSION}\u0000${command}`;
36
+ }
37
+ const THREW = { status: "unavailable", error: "threw" };
38
+ const DRAINED = { status: "unavailable", error: "drained" };
39
+ export function createCommandShadow(opts) {
40
+ const entries = new Map();
41
+ const cache = new Map();
42
+ const inFlight = new Set();
43
+ const writes = new Set();
44
+ const track = (set, p) => {
45
+ set.add(p);
46
+ // .catch: finally() re-rejects, and nothing else observes this promise.
47
+ void p.finally(() => set.delete(p)).catch(() => undefined);
48
+ };
49
+ function classifyCached(command) {
50
+ const key = shadowCacheKey(command);
51
+ const hit = cache.get(key);
52
+ if (hit !== undefined)
53
+ return { promise: hit, cached: true };
54
+ // Called synchronously so classification starts at once (and so a test can
55
+ // answer it right after observe); the try turns a synchronous throw into
56
+ // the same `threw` result as a rejection.
57
+ let started;
58
+ try {
59
+ started = Promise.resolve(opts.classify(command));
60
+ }
61
+ catch {
62
+ started = Promise.resolve(THREW);
63
+ }
64
+ const promise = started.catch(() => THREW);
65
+ cache.set(key, promise);
66
+ void promise.then((r) => {
67
+ if (r.status === "unavailable" && cache.get(key) === promise)
68
+ cache.delete(key);
69
+ });
70
+ return { promise, cached: false };
71
+ }
72
+ /**
73
+ * Public form used by the optional guard (US-003). Returns the SAME
74
+ * cached promise `observe` awaits, so the shadow classifies a command
75
+ * once even when both `observe` and `guard.assess` are called for it.
76
+ */
77
+ function classifyCachedForGuard(command) {
78
+ return classifyCached(command).promise;
79
+ }
80
+ function flush(key, entry) {
81
+ if (entry.written || entry.model === undefined || entry.outcome === undefined)
82
+ return;
83
+ entry.written = true;
84
+ entries.delete(key);
85
+ track(writes, opts.write(toRow(entry, entry.model, entry.outcome)).catch((err) => opts.onWriteError?.(err)));
86
+ }
87
+ function toRow(entry, model, outcome) {
88
+ const { obs } = entry;
89
+ const r = model.result;
90
+ const cwd = entry.run?.cwd ?? obs.cwd;
91
+ // An Exec call's cwd arrives only at settle, after observe() scored the
92
+ // rules without it; re-score against the cwd the row reports so the two
93
+ // agree. Scoring is cheap and total.
94
+ const rules = cwd !== obs.cwd ? scoreRules(obs.command, { root: cwd }) : entry.rules;
95
+ return {
96
+ at: _commandShadowDeps.now(),
97
+ runId: opts.runId,
98
+ ...(obs.storyId !== undefined ? { storyId: obs.storyId } : {}),
99
+ stage: obs.stage,
100
+ identity: obs.identity,
101
+ command: obs.command,
102
+ ...(obs.argv !== undefined ? { argv: obs.argv } : {}),
103
+ ...(entry.run !== undefined ? { executed: entry.run.executed } : {}),
104
+ ...(cwd !== undefined ? { cwd } : {}),
105
+ mechanical: obs.mechanical,
106
+ outcome,
107
+ rules,
108
+ signals: { tmpWrite: detectTmpWrite(obs.command, cwd) },
109
+ ...callIdentifiers(obs),
110
+ model: {
111
+ status: model.cached && r.status === "answered" ? "cached" : r.status,
112
+ questionSetVersion: QUESTION_SET_VERSION,
113
+ ...(r.status === "answered" ? { answers: r.answers } : {}),
114
+ ...(r.status === "answered" && r.model !== undefined ? { model: r.model } : {}),
115
+ ...((r.status === "answered" || r.status === "blocked") && r.decisionId !== undefined
116
+ ? { decisionId: r.decisionId }
117
+ : {}),
118
+ ...(r.latencyMs !== undefined ? { latencyMs: r.latencyMs } : {}),
119
+ ...(r.status === "unavailable" ? { error: r.error } : {}),
120
+ },
121
+ };
122
+ }
123
+ // Build the guard once, only when requested. The closure captures the
124
+ // same `classifyCachedForGuard` and rule scorer `observe` uses, so the
125
+ // shadow and the guard agree on the cached model and the rule baseline.
126
+ const guard = opts.guard === undefined
127
+ ? undefined
128
+ : createCommandGuard({
129
+ threshold: opts.guard.threshold,
130
+ scoreRules: (command, cwd) => scoreRules(command, cwd !== undefined ? { root: cwd } : {}),
131
+ classifyCached: classifyCachedForGuard,
132
+ });
133
+ return {
134
+ observe(key, obs) {
135
+ try {
136
+ if (entries.has(key))
137
+ return;
138
+ const entry = { obs, rules: scoreRules(obs.command, { root: obs.cwd }), written: false };
139
+ entries.set(key, entry);
140
+ const { promise, cached } = classifyCached(obs.command);
141
+ track(inFlight, promise.then((result) => {
142
+ if (entry.written)
143
+ return;
144
+ entry.model = { result, cached };
145
+ flush(key, entry);
146
+ }));
147
+ }
148
+ catch {
149
+ // Total by contract (spec 4.3): a shadow failure must never reach callTool.
150
+ }
151
+ },
152
+ settle(key, outcome, run) {
153
+ try {
154
+ const entry = entries.get(key);
155
+ if (entry === undefined || entry.outcome !== undefined)
156
+ return;
157
+ entry.outcome = outcome;
158
+ if (run !== undefined)
159
+ entry.run = run;
160
+ flush(key, entry);
161
+ }
162
+ catch {
163
+ // Total by contract (spec 4.3).
164
+ }
165
+ },
166
+ async drain() {
167
+ try {
168
+ const timer = _commandShadowDeps.timer(opts.timeoutMs);
169
+ try {
170
+ await Promise.race([Promise.allSettled([...inFlight]), timer.done]);
171
+ }
172
+ finally {
173
+ timer.cancel();
174
+ }
175
+ for (const [key, entry] of [...entries]) {
176
+ entry.model ??= { result: DRAINED, cached: false };
177
+ entry.outcome ??= { ledger: "unsettled" };
178
+ flush(key, entry);
179
+ }
180
+ await Promise.allSettled([...writes]);
181
+ }
182
+ catch {
183
+ // drain() is awaited in the execution stage's finally; it must not throw.
184
+ }
185
+ },
186
+ ...(guard !== undefined ? { guard } : {}),
187
+ };
188
+ }
@@ -0,0 +1,16 @@
1
+ import { type ModelResult } from "./types.ts";
2
+ export type Classify = (command: string) => Promise<ModelResult>;
3
+ export interface SystemOneClientOptions {
4
+ readonly url: string;
5
+ readonly timeoutMs: number;
6
+ readonly token?: string;
7
+ }
8
+ /** Injectable seams: tests drive the timeout by hand instead of waiting on it. */
9
+ export declare const _systemOneClientDeps: {
10
+ fetch: (input: string, init: RequestInit) => Promise<Response>;
11
+ timeoutSignal: (ms: number) => AbortSignal;
12
+ now: () => number;
13
+ };
14
+ export declare function createSystemOneClient(opts: SystemOneClientOptions): Classify;
15
+ /** Map a 200 body to a ModelResult. Anything short of all seven valid answers is `malformed`. */
16
+ export declare function parseAnswer(body: unknown, latencyMs: number): ModelResult;