@intutic/cli 1.10.0 → 1.10.1

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/README.md +1 -0
  2. package/dist/cli.js +868 -30
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/attenuate.d.ts +39 -0
  5. package/dist/commands/attenuate.d.ts.map +1 -0
  6. package/dist/commands/attenuate.js +98 -0
  7. package/dist/commands/attenuate.js.map +1 -0
  8. package/dist/commands/budget.d.ts +80 -0
  9. package/dist/commands/budget.d.ts.map +1 -1
  10. package/dist/commands/budget.js +119 -3
  11. package/dist/commands/budget.js.map +1 -1
  12. package/dist/commands/connect.d.ts +10 -0
  13. package/dist/commands/connect.d.ts.map +1 -1
  14. package/dist/commands/connect.js +358 -48
  15. package/dist/commands/connect.js.map +1 -1
  16. package/dist/commands/credentials.d.ts +31 -0
  17. package/dist/commands/credentials.d.ts.map +1 -0
  18. package/dist/commands/credentials.js +134 -0
  19. package/dist/commands/credentials.js.map +1 -0
  20. package/dist/commands/decision.d.ts +37 -0
  21. package/dist/commands/decision.d.ts.map +1 -0
  22. package/dist/commands/decision.js +56 -0
  23. package/dist/commands/decision.js.map +1 -0
  24. package/dist/commands/doctor.d.ts +38 -1
  25. package/dist/commands/doctor.d.ts.map +1 -1
  26. package/dist/commands/doctor.js +228 -18
  27. package/dist/commands/doctor.js.map +1 -1
  28. package/dist/commands/enforce.d.ts +33 -0
  29. package/dist/commands/enforce.d.ts.map +1 -0
  30. package/dist/commands/enforce.js +150 -0
  31. package/dist/commands/enforce.js.map +1 -0
  32. package/dist/commands/enterprise.d.ts +29 -0
  33. package/dist/commands/enterprise.d.ts.map +1 -0
  34. package/dist/commands/enterprise.js +141 -0
  35. package/dist/commands/enterprise.js.map +1 -0
  36. package/dist/commands/env.d.ts +7 -0
  37. package/dist/commands/env.d.ts.map +1 -0
  38. package/dist/commands/env.js +55 -0
  39. package/dist/commands/env.js.map +1 -0
  40. package/dist/commands/exec.d.ts +38 -2
  41. package/dist/commands/exec.d.ts.map +1 -1
  42. package/dist/commands/exec.js +247 -10
  43. package/dist/commands/exec.js.map +1 -1
  44. package/dist/commands/findings.d.ts +84 -0
  45. package/dist/commands/findings.d.ts.map +1 -0
  46. package/dist/commands/findings.js +329 -0
  47. package/dist/commands/findings.js.map +1 -0
  48. package/dist/commands/gateway.d.ts +62 -0
  49. package/dist/commands/gateway.d.ts.map +1 -0
  50. package/dist/commands/gateway.js +298 -0
  51. package/dist/commands/gateway.js.map +1 -0
  52. package/dist/commands/guardrails.d.ts +81 -0
  53. package/dist/commands/guardrails.d.ts.map +1 -0
  54. package/dist/commands/guardrails.js +735 -0
  55. package/dist/commands/guardrails.js.map +1 -0
  56. package/dist/commands/init.d.ts +8 -0
  57. package/dist/commands/init.d.ts.map +1 -1
  58. package/dist/commands/init.js +14 -2
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/commands/install-daemon.d.ts +70 -3
  61. package/dist/commands/install-daemon.d.ts.map +1 -1
  62. package/dist/commands/install-daemon.js +482 -47
  63. package/dist/commands/install-daemon.js.map +1 -1
  64. package/dist/commands/integrity.d.ts +205 -0
  65. package/dist/commands/integrity.d.ts.map +1 -0
  66. package/dist/commands/integrity.js +620 -0
  67. package/dist/commands/integrity.js.map +1 -0
  68. package/dist/commands/judge.d.ts +31 -0
  69. package/dist/commands/judge.d.ts.map +1 -0
  70. package/dist/commands/judge.js +118 -0
  71. package/dist/commands/judge.js.map +1 -0
  72. package/dist/commands/org.d.ts +31 -0
  73. package/dist/commands/org.d.ts.map +1 -0
  74. package/dist/commands/org.js +159 -0
  75. package/dist/commands/org.js.map +1 -0
  76. package/dist/commands/policy.d.ts +138 -4
  77. package/dist/commands/policy.d.ts.map +1 -1
  78. package/dist/commands/policy.js +538 -97
  79. package/dist/commands/policy.js.map +1 -1
  80. package/dist/commands/predict-cost.d.ts +45 -0
  81. package/dist/commands/predict-cost.d.ts.map +1 -0
  82. package/dist/commands/predict-cost.js +123 -0
  83. package/dist/commands/predict-cost.js.map +1 -0
  84. package/dist/commands/rollback.d.ts +5 -0
  85. package/dist/commands/rollback.d.ts.map +1 -0
  86. package/dist/commands/rollback.js +142 -0
  87. package/dist/commands/rollback.js.map +1 -0
  88. package/dist/commands/routing.d.ts +62 -0
  89. package/dist/commands/routing.d.ts.map +1 -0
  90. package/dist/commands/routing.js +121 -0
  91. package/dist/commands/routing.js.map +1 -0
  92. package/dist/commands/setup.d.ts +62 -0
  93. package/dist/commands/setup.d.ts.map +1 -0
  94. package/dist/commands/setup.js +238 -0
  95. package/dist/commands/setup.js.map +1 -0
  96. package/dist/commands/skill.d.ts +268 -1
  97. package/dist/commands/skill.d.ts.map +1 -1
  98. package/dist/commands/skill.js +744 -39
  99. package/dist/commands/skill.js.map +1 -1
  100. package/dist/commands/sops.d.ts +95 -0
  101. package/dist/commands/sops.d.ts.map +1 -1
  102. package/dist/commands/sops.js +334 -34
  103. package/dist/commands/sops.js.map +1 -1
  104. package/dist/commands/start.d.ts.map +1 -1
  105. package/dist/commands/start.js +1 -18
  106. package/dist/commands/start.js.map +1 -1
  107. package/dist/commands/status.d.ts.map +1 -1
  108. package/dist/commands/status.js +44 -1
  109. package/dist/commands/status.js.map +1 -1
  110. package/dist/commands/team.d.ts +38 -0
  111. package/dist/commands/team.d.ts.map +1 -0
  112. package/dist/commands/team.js +140 -0
  113. package/dist/commands/team.js.map +1 -0
  114. package/dist/commands/traces.d.ts +8 -1
  115. package/dist/commands/traces.d.ts.map +1 -1
  116. package/dist/commands/traces.js +114 -15
  117. package/dist/commands/traces.js.map +1 -1
  118. package/dist/commands/whoami.d.ts.map +1 -1
  119. package/dist/commands/whoami.js +2 -1
  120. package/dist/commands/whoami.js.map +1 -1
  121. package/dist/config/keychain.d.ts +10 -2
  122. package/dist/config/keychain.d.ts.map +1 -1
  123. package/dist/config/keychain.js +83 -14
  124. package/dist/config/keychain.js.map +1 -1
  125. package/dist/config/paths.d.ts +27 -0
  126. package/dist/config/paths.d.ts.map +1 -1
  127. package/dist/config/paths.js +37 -6
  128. package/dist/config/paths.js.map +1 -1
  129. package/dist/config/store.d.ts.map +1 -1
  130. package/dist/config/store.js +19 -3
  131. package/dist/config/store.js.map +1 -1
  132. package/dist/harness/ag2.d.ts +22 -0
  133. package/dist/harness/ag2.d.ts.map +1 -0
  134. package/dist/harness/ag2.js +34 -0
  135. package/dist/harness/ag2.js.map +1 -0
  136. package/dist/harness/agentcore.d.ts +55 -0
  137. package/dist/harness/agentcore.d.ts.map +1 -0
  138. package/dist/harness/agentcore.js +119 -0
  139. package/dist/harness/agentcore.js.map +1 -0
  140. package/dist/harness/agenticOrchestrator.d.ts +52 -0
  141. package/dist/harness/agenticOrchestrator.d.ts.map +1 -0
  142. package/dist/harness/agenticOrchestrator.js +115 -0
  143. package/dist/harness/agenticOrchestrator.js.map +1 -0
  144. package/dist/harness/aiSdkHarness.d.ts +30 -0
  145. package/dist/harness/aiSdkHarness.d.ts.map +1 -0
  146. package/dist/harness/aiSdkHarness.js +51 -0
  147. package/dist/harness/aiSdkHarness.js.map +1 -0
  148. package/dist/harness/aiSdkWorkflow.d.ts +26 -0
  149. package/dist/harness/aiSdkWorkflow.d.ts.map +1 -0
  150. package/dist/harness/aiSdkWorkflow.js +39 -0
  151. package/dist/harness/aiSdkWorkflow.js.map +1 -0
  152. package/dist/harness/autogen.d.ts +24 -0
  153. package/dist/harness/autogen.d.ts.map +1 -0
  154. package/dist/harness/autogen.js +36 -0
  155. package/dist/harness/autogen.js.map +1 -0
  156. package/dist/harness/base.js.map +1 -1
  157. package/dist/harness/continue.d.ts.map +1 -1
  158. package/dist/harness/continue.js +2 -1
  159. package/dist/harness/continue.js.map +1 -1
  160. package/dist/harness/crewai.d.ts +15 -0
  161. package/dist/harness/crewai.d.ts.map +1 -0
  162. package/dist/harness/crewai.js +26 -0
  163. package/dist/harness/crewai.js.map +1 -0
  164. package/dist/harness/detector.d.ts +1 -1
  165. package/dist/harness/detector.d.ts.map +1 -1
  166. package/dist/harness/detector.js +47 -1
  167. package/dist/harness/detector.js.map +1 -1
  168. package/dist/harness/dsh.d.ts +19 -0
  169. package/dist/harness/dsh.d.ts.map +1 -0
  170. package/dist/harness/dsh.js +93 -0
  171. package/dist/harness/dsh.js.map +1 -0
  172. package/dist/harness/eve.d.ts +40 -0
  173. package/dist/harness/eve.d.ts.map +1 -0
  174. package/dist/harness/eve.js +67 -0
  175. package/dist/harness/eve.js.map +1 -0
  176. package/dist/harness/googleAdk.d.ts +18 -0
  177. package/dist/harness/googleAdk.d.ts.map +1 -0
  178. package/dist/harness/googleAdk.js +29 -0
  179. package/dist/harness/googleAdk.js.map +1 -0
  180. package/dist/harness/goose.d.ts.map +1 -1
  181. package/dist/harness/goose.js +5 -1
  182. package/dist/harness/goose.js.map +1 -1
  183. package/dist/harness/grok.d.ts +16 -0
  184. package/dist/harness/grok.d.ts.map +1 -0
  185. package/dist/harness/grok.js +78 -0
  186. package/dist/harness/grok.js.map +1 -0
  187. package/dist/harness/jsSdkGatedAdapter.d.ts +102 -0
  188. package/dist/harness/jsSdkGatedAdapter.d.ts.map +1 -0
  189. package/dist/harness/jsSdkGatedAdapter.js +139 -0
  190. package/dist/harness/jsSdkGatedAdapter.js.map +1 -0
  191. package/dist/harness/langchain.d.ts +22 -0
  192. package/dist/harness/langchain.d.ts.map +1 -0
  193. package/dist/harness/langchain.js +33 -0
  194. package/dist/harness/langchain.js.map +1 -0
  195. package/dist/harness/langgraph.d.ts +22 -0
  196. package/dist/harness/langgraph.d.ts.map +1 -0
  197. package/dist/harness/langgraph.js +80 -0
  198. package/dist/harness/langgraph.js.map +1 -0
  199. package/dist/harness/mastra.d.ts +21 -0
  200. package/dist/harness/mastra.d.ts.map +1 -0
  201. package/dist/harness/mastra.js +33 -0
  202. package/dist/harness/mastra.js.map +1 -0
  203. package/dist/harness/muse.d.ts +22 -0
  204. package/dist/harness/muse.d.ts.map +1 -0
  205. package/dist/harness/muse.js +92 -0
  206. package/dist/harness/muse.js.map +1 -0
  207. package/dist/harness/n8n.d.ts.map +1 -1
  208. package/dist/harness/n8n.js +36 -11
  209. package/dist/harness/n8n.js.map +1 -1
  210. package/dist/harness/openaiAgents.d.ts +40 -0
  211. package/dist/harness/openaiAgents.d.ts.map +1 -0
  212. package/dist/harness/openaiAgents.js +107 -0
  213. package/dist/harness/openaiAgents.js.map +1 -0
  214. package/dist/harness/opencode.d.ts +24 -0
  215. package/dist/harness/opencode.d.ts.map +1 -0
  216. package/dist/harness/opencode.js +97 -0
  217. package/dist/harness/opencode.js.map +1 -0
  218. package/dist/harness/pydanticAi.d.ts +19 -0
  219. package/dist/harness/pydanticAi.d.ts.map +1 -0
  220. package/dist/harness/pydanticAi.js +30 -0
  221. package/dist/harness/pydanticAi.js.map +1 -0
  222. package/dist/harness/sdkGatedAdapter.d.ts +55 -0
  223. package/dist/harness/sdkGatedAdapter.d.ts.map +1 -0
  224. package/dist/harness/sdkGatedAdapter.js +85 -0
  225. package/dist/harness/sdkGatedAdapter.js.map +1 -0
  226. package/dist/harness/smolagents.d.ts +22 -0
  227. package/dist/harness/smolagents.d.ts.map +1 -0
  228. package/dist/harness/smolagents.js +33 -0
  229. package/dist/harness/smolagents.js.map +1 -0
  230. package/dist/harness/strands.d.ts +22 -0
  231. package/dist/harness/strands.d.ts.map +1 -0
  232. package/dist/harness/strands.js +35 -0
  233. package/dist/harness/strands.js.map +1 -0
  234. package/dist/harness/trueforge.d.ts +25 -0
  235. package/dist/harness/trueforge.d.ts.map +1 -0
  236. package/dist/harness/trueforge.js +40 -0
  237. package/dist/harness/trueforge.js.map +1 -0
  238. package/dist/harness/types.d.ts.map +1 -1
  239. package/dist/harness/types.js +70 -0
  240. package/dist/harness/types.js.map +1 -1
  241. package/dist/harness/vercelAiSdk.d.ts +31 -0
  242. package/dist/harness/vercelAiSdk.d.ts.map +1 -0
  243. package/dist/harness/vercelAiSdk.js +44 -0
  244. package/dist/harness/vercelAiSdk.js.map +1 -0
  245. package/dist/harness/xirp.d.ts +43 -0
  246. package/dist/harness/xirp.d.ts.map +1 -0
  247. package/dist/harness/xirp.js +95 -0
  248. package/dist/harness/xirp.js.map +1 -0
  249. package/dist/lib/api.d.ts +68 -0
  250. package/dist/lib/api.d.ts.map +1 -1
  251. package/dist/lib/api.js +108 -67
  252. package/dist/lib/api.js.map +1 -1
  253. package/dist/lib/authMessages.d.ts +19 -0
  254. package/dist/lib/authMessages.d.ts.map +1 -0
  255. package/dist/lib/authMessages.js +20 -0
  256. package/dist/lib/authMessages.js.map +1 -0
  257. package/dist/lib/binaryChecksum.d.ts +12 -0
  258. package/dist/lib/binaryChecksum.d.ts.map +1 -0
  259. package/dist/lib/binaryChecksum.js +45 -0
  260. package/dist/lib/binaryChecksum.js.map +1 -0
  261. package/dist/lib/binaryOnPath.d.ts +24 -0
  262. package/dist/lib/binaryOnPath.d.ts.map +1 -0
  263. package/dist/lib/binaryOnPath.js +31 -0
  264. package/dist/lib/binaryOnPath.js.map +1 -0
  265. package/dist/lib/caTrust.d.ts +69 -0
  266. package/dist/lib/caTrust.d.ts.map +1 -0
  267. package/dist/lib/caTrust.js +169 -0
  268. package/dist/lib/caTrust.js.map +1 -0
  269. package/dist/lib/ciscoScanner.d.ts +178 -0
  270. package/dist/lib/ciscoScanner.d.ts.map +1 -0
  271. package/dist/lib/ciscoScanner.js +292 -0
  272. package/dist/lib/ciscoScanner.js.map +1 -0
  273. package/dist/lib/decisionsLogRefresh.d.ts +28 -0
  274. package/dist/lib/decisionsLogRefresh.d.ts.map +1 -0
  275. package/dist/lib/decisionsLogRefresh.js +54 -0
  276. package/dist/lib/decisionsLogRefresh.js.map +1 -0
  277. package/dist/lib/deviceReport.d.ts +26 -0
  278. package/dist/lib/deviceReport.d.ts.map +1 -0
  279. package/dist/lib/deviceReport.js +55 -0
  280. package/dist/lib/deviceReport.js.map +1 -0
  281. package/dist/lib/elevation.d.ts +58 -0
  282. package/dist/lib/elevation.d.ts.map +1 -0
  283. package/dist/lib/elevation.js +83 -0
  284. package/dist/lib/elevation.js.map +1 -0
  285. package/dist/lib/enforcementState.d.ts +69 -0
  286. package/dist/lib/enforcementState.d.ts.map +1 -0
  287. package/dist/lib/enforcementState.js +113 -0
  288. package/dist/lib/enforcementState.js.map +1 -0
  289. package/dist/lib/ensureValkey.d.ts.map +1 -1
  290. package/dist/lib/ensureValkey.js +2 -2
  291. package/dist/lib/ensureValkey.js.map +1 -1
  292. package/dist/lib/envInjector.d.ts.map +1 -1
  293. package/dist/lib/envInjector.js +22 -4
  294. package/dist/lib/envInjector.js.map +1 -1
  295. package/dist/lib/gitHooks.d.ts +7 -2
  296. package/dist/lib/gitHooks.d.ts.map +1 -1
  297. package/dist/lib/gitHooks.js +180 -13
  298. package/dist/lib/gitHooks.js.map +1 -1
  299. package/dist/lib/gracefulShutdown.d.ts +37 -0
  300. package/dist/lib/gracefulShutdown.d.ts.map +1 -0
  301. package/dist/lib/gracefulShutdown.js +57 -0
  302. package/dist/lib/gracefulShutdown.js.map +1 -0
  303. package/dist/lib/graphIdentity.d.ts +80 -0
  304. package/dist/lib/graphIdentity.d.ts.map +1 -0
  305. package/dist/lib/graphIdentity.js +120 -0
  306. package/dist/lib/graphIdentity.js.map +1 -0
  307. package/dist/lib/localTraces.d.ts +61 -0
  308. package/dist/lib/localTraces.d.ts.map +1 -0
  309. package/dist/lib/localTraces.js +173 -0
  310. package/dist/lib/localTraces.js.map +1 -0
  311. package/dist/lib/mdmManifest.d.ts +73 -0
  312. package/dist/lib/mdmManifest.d.ts.map +1 -0
  313. package/dist/lib/mdmManifest.js +190 -0
  314. package/dist/lib/mdmManifest.js.map +1 -0
  315. package/dist/lib/onboarding.d.ts.map +1 -1
  316. package/dist/lib/onboarding.js +60 -1
  317. package/dist/lib/onboarding.js.map +1 -1
  318. package/dist/lib/otelPropagation.d.ts +26 -0
  319. package/dist/lib/otelPropagation.d.ts.map +1 -0
  320. package/dist/lib/otelPropagation.js +40 -0
  321. package/dist/lib/otelPropagation.js.map +1 -0
  322. package/dist/lib/policySnapshot.d.ts +92 -0
  323. package/dist/lib/policySnapshot.d.ts.map +1 -0
  324. package/dist/lib/policySnapshot.js +173 -0
  325. package/dist/lib/policySnapshot.js.map +1 -0
  326. package/dist/lib/process.d.ts.map +1 -1
  327. package/dist/lib/process.js +60 -0
  328. package/dist/lib/process.js.map +1 -1
  329. package/dist/lib/providerProbe.d.ts +28 -0
  330. package/dist/lib/providerProbe.d.ts.map +1 -0
  331. package/dist/lib/providerProbe.js +57 -0
  332. package/dist/lib/providerProbe.js.map +1 -0
  333. package/dist/lib/proxyBinary.d.ts +19 -0
  334. package/dist/lib/proxyBinary.d.ts.map +1 -0
  335. package/dist/lib/proxyBinary.js +30 -0
  336. package/dist/lib/proxyBinary.js.map +1 -0
  337. package/dist/lib/sandbox/firecracker.d.ts +89 -0
  338. package/dist/lib/sandbox/firecracker.d.ts.map +1 -0
  339. package/dist/lib/sandbox/firecracker.js +135 -0
  340. package/dist/lib/sandbox/firecracker.js.map +1 -0
  341. package/dist/lib/sandbox/index.d.ts +34 -0
  342. package/dist/lib/sandbox/index.d.ts.map +1 -0
  343. package/dist/lib/sandbox/index.js +54 -0
  344. package/dist/lib/sandbox/index.js.map +1 -0
  345. package/dist/lib/sandbox/oci.d.ts +38 -0
  346. package/dist/lib/sandbox/oci.d.ts.map +1 -0
  347. package/dist/lib/sandbox/oci.js +108 -0
  348. package/dist/lib/sandbox/oci.js.map +1 -0
  349. package/dist/lib/sandbox/types.d.ts +48 -0
  350. package/dist/lib/sandbox/types.d.ts.map +1 -0
  351. package/dist/lib/sandbox/types.js +12 -0
  352. package/dist/lib/sandbox/types.js.map +1 -0
  353. package/dist/lib/sopFrontMatter.d.ts +50 -0
  354. package/dist/lib/sopFrontMatter.d.ts.map +1 -0
  355. package/dist/lib/sopFrontMatter.js +129 -0
  356. package/dist/lib/sopFrontMatter.js.map +1 -0
  357. package/package.json +11 -6
@@ -4,19 +4,46 @@ import * as os from 'node:os';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { log } from '../lib/logger.js';
7
+ import { NOT_AUTHENTICATED } from '../lib/authMessages.js';
8
+ import { WASM_HOST_IMPORTS, unsupportedWasmImports, explainWasmImport, } from '@intutic/shared-types';
7
9
  import { loadCredentials, loadConfig } from '../config/store.js';
8
10
  import { resolveControlPlaneUrl } from '../config/paths.js';
9
11
  import { createApiClient } from '../lib/api.js';
10
- async function getClient(dev) {
12
+ import { resolveSnapshotRulesPath, SNAPSHOT_RULES_FILE } from '../lib/policySnapshot.js';
13
+ /**
14
+ * Message of a caught value.
15
+ *
16
+ * `catch` binds `unknown` — a thrown non-Error (or an Error subclass with no
17
+ * message) is not hypothetical here, since `abort` below and the WASM runtime
18
+ * both throw across the host boundary.
19
+ */
20
+ function errMessage(err) {
21
+ return err instanceof Error ? err.message : String(err);
22
+ }
23
+ /**
24
+ * The control plane these commands talk to, and the credentials for it.
25
+ *
26
+ * Lifted out of `getClient` because `policy snapshot` needs the workspace id and
27
+ * the raw key rather than an API client — and resolving them a second way is how
28
+ * one command ends up pointed at production while its neighbour is on --dev.
29
+ */
30
+ async function resolveTarget(dev) {
11
31
  const creds = await loadCredentials();
12
32
  if (!creds) {
13
- log.error('Not authenticated. This command needs an Intutic control plane, which open core does not include. To run the proxy without one: `intutic start`.');
33
+ log.error(NOT_AUTHENTICATED);
14
34
  process.exit(1);
15
35
  }
16
36
  const config = loadConfig();
17
37
  const devMode = dev || config?.devMode || process.env.INTUTIC_DEV === '1';
18
- const controlPlaneUrl = resolveControlPlaneUrl(devMode);
19
- return createApiClient(controlPlaneUrl, creds.apiKey);
38
+ return {
39
+ controlPlaneUrl: resolveControlPlaneUrl(devMode),
40
+ apiKey: creds.apiKey,
41
+ workspaceId: creds.workspaceId,
42
+ };
43
+ }
44
+ async function getClient(dev) {
45
+ const { controlPlaneUrl, apiKey } = await resolveTarget(dev);
46
+ return createApiClient(controlPlaneUrl, apiKey);
20
47
  }
21
48
  export async function runPolicyEnable(policyId, opts) {
22
49
  log.header(`Intutic — Enable Policy: ${policyId}`);
@@ -35,7 +62,7 @@ export async function runPolicyEnable(policyId, opts) {
35
62
  }
36
63
  }
37
64
  catch (err) {
38
- log.error(`Failed to enable policy: ${err.message}`);
65
+ log.error(`Failed to enable policy: ${errMessage(err)}`);
39
66
  process.exit(1);
40
67
  }
41
68
  }
@@ -56,7 +83,7 @@ export async function runPolicyDisable(policyId, opts) {
56
83
  }
57
84
  }
58
85
  catch (err) {
59
- log.error(`Failed to disable policy: ${err.message}`);
86
+ log.error(`Failed to disable policy: ${errMessage(err)}`);
60
87
  process.exit(1);
61
88
  }
62
89
  }
@@ -82,7 +109,7 @@ export async function runPolicyRollback(policyId, opts) {
82
109
  }
83
110
  }
84
111
  catch (err) {
85
- log.error(`Failed to rollback policy: ${err.message}`);
112
+ log.error(`Failed to rollback policy: ${errMessage(err)}`);
86
113
  process.exit(1);
87
114
  }
88
115
  }
@@ -102,10 +129,55 @@ export async function runPolicyExport(opts) {
102
129
  }
103
130
  }
104
131
  catch (err) {
105
- log.error(`Failed to export policies: ${err.message}`);
132
+ log.error(`Failed to export policies: ${errMessage(err)}`);
106
133
  process.exit(1);
107
134
  }
108
135
  }
136
+ /**
137
+ * `intutic policy snapshot` — compile this workspace's policy to disk.
138
+ *
139
+ * The snapshot is the dynamic tier: every harness gate reads
140
+ * `~/.intutic/hooks/policy-snapshot.rules` and enforces the compiled floor and
141
+ * nothing else when it is missing. Until this command existed the only thing
142
+ * that ever wrote it was `intutic connect`, so arming the gates meant running a
143
+ * daemon that also rewrites five harnesses' hook configs, starts a drift
144
+ * watcher and provisions Valkey. This does the one thing.
145
+ */
146
+ export async function runPolicySnapshot(opts) {
147
+ log.header('Intutic — Refresh Policy Snapshot');
148
+ const { controlPlaneUrl, apiKey, workspaceId } = await resolveTarget(opts.dev);
149
+ // The gates consult INTUTIC_SNAPSHOT_RULES before the default path, so writing
150
+ // to the default while they read elsewhere would produce a snapshot nothing
151
+ // enforces — the precise silent failure this command exists to end.
152
+ const rulesPath = resolveSnapshotRulesPath();
153
+ const snapshotDir = path.dirname(rulesPath);
154
+ if (path.basename(rulesPath) !== SNAPSHOT_RULES_FILE) {
155
+ log.warn(`INTUTIC_SNAPSHOT_RULES names "${path.basename(rulesPath)}", but the writer always produces ` +
156
+ `"${SNAPSHOT_RULES_FILE}" — the gates will read a file this command does not write.`);
157
+ }
158
+ // Imported here rather than at module scope: @intutic/sync-daemon pulls in ws,
159
+ // ioredis and chokidar, and every other `intutic policy` subcommand would pay
160
+ // that startup cost without ever touching a snapshot.
161
+ const { refreshPolicySnapshot } = await import('@intutic/sync-daemon');
162
+ const result = await refreshPolicySnapshot({ controlPlaneUrl, apiKey, workspaceId, snapshotDir });
163
+ // `refreshPolicySnapshot` never throws — policy refresh must not be able to
164
+ // take down the daemon's sync loop — so null is the only failure signal it
165
+ // has. Reporting that as success is how a machine comes to enforce nothing
166
+ // while the command meant to arm it said it worked.
167
+ if (!result) {
168
+ log.error('Snapshot NOT written — GET /api/v1/policy/resolve returned nothing usable.');
169
+ log.field('Control plane', controlPlaneUrl);
170
+ log.field('Workspace', workspaceId);
171
+ log.info('Any previous snapshot is untouched and still enforced.');
172
+ log.info('Check the control plane is reachable and the key is still valid: `intutic whoami`.');
173
+ process.exit(1);
174
+ }
175
+ log.success(`Policy snapshot written — ${result.ruleCount} rule(s) now enforced by every gate.`);
176
+ log.field('Digest', result.digest);
177
+ log.field('Workspace', workspaceId);
178
+ log.field('Path', path.join(snapshotDir, SNAPSHOT_RULES_FILE));
179
+ log.info('Confirm the gates accept it with `intutic doctor`.');
180
+ }
109
181
  /**
110
182
  * Minimal RequestContext used to validate a rule instantiates and evaluates
111
183
  * before install. Mirrors packages/wasm-sdk/assembly/mock_context_allow.json
@@ -123,125 +195,276 @@ export const DEFAULT_ALLOW_MOCK = JSON.stringify({
123
195
  risk_tier: 'Low',
124
196
  dlp_findings: []
125
197
  });
198
+ /**
199
+ * Every host function the proxy registers, declared once on this side.
200
+ *
201
+ * `tools/scripts/check-wasm-host-imports.js` asserts this equals
202
+ * `HOST_IMPORTS` in `packages/proxy/src/wasm/host.rs`. The two had already
203
+ * drifted — this side offered `seed`, the proxy never has — and the drift was
204
+ * invisible because it only surfaced as a per-request warning in a process
205
+ * nobody was reading.
206
+ */
207
+ export const HOST_IMPORT_NAMES = WASM_HOST_IMPORTS;
208
+ /**
209
+ * A callable export, or null when the module does not export that name (or
210
+ * exports it as a global/table/memory instead of a function).
211
+ *
212
+ * The variadic signature is the widest thing every guest function here fits;
213
+ * `resolveGuestAbi` immediately narrows each one to its real arity by way of
214
+ * the `WasmGuestAbi` members it assigns them to.
215
+ */
216
+ function exportedFunction(exports, name) {
217
+ const value = exports[name];
218
+ return typeof value === 'function' ? value : null;
219
+ }
220
+ /** Resolve the guest ABI, throwing a message that names the missing export. */
221
+ function resolveGuestAbi(instance) {
222
+ const { exports } = instance;
223
+ const memory = exports.memory;
224
+ if (!(memory instanceof WebAssembly.Memory)) {
225
+ // Previously read as `exports.memory as WebAssembly.Memory` and dereferenced,
226
+ // so a module without a memory export failed with "Cannot read properties of
227
+ // undefined (reading 'buffer')" instead of saying what was wrong.
228
+ throw new Error("WASM module does not export 'memory'. Compile with AssemblyScript's " +
229
+ 'default settings (`--exportRuntime`) so the host can read what the rule returns.');
230
+ }
231
+ // Accept exactly the allocator exports the proxy runner accepts
232
+ // (packages/proxy/src/wasm/runner.rs: `allocate`, then `__new`) — anything
233
+ // looser would validate rules the proxy then silently fails open on.
234
+ let allocate;
235
+ const allocateExport = exportedFunction(exports, 'allocate');
236
+ if (allocateExport) {
237
+ allocate = allocateExport;
238
+ }
239
+ else {
240
+ const newExport = exportedFunction(exports, '__new');
241
+ if (!newExport) {
242
+ throw new Error("WASM module is missing 'allocate' or '__new' memory helpers.");
243
+ }
244
+ allocate = (size) => newExport(size, 0);
245
+ }
246
+ const evaluate = exportedFunction(exports, 'evaluate');
247
+ if (!evaluate) {
248
+ throw new Error("WASM module is missing 'evaluate' function export.");
249
+ }
250
+ return { memory, allocate, evaluate };
251
+ }
252
+ /**
253
+ * Read `len` UTF-8 bytes at `ptr` out of guest memory, or null if that range is
254
+ * not wholly inside it.
255
+ */
256
+ function readGuestUtf8(memory, ptr, len) {
257
+ const size = memory.buffer.byteLength;
258
+ if (!Number.isInteger(ptr) || !Number.isInteger(len) || ptr < 0 || len < 0 || ptr + len > size) {
259
+ return null;
260
+ }
261
+ return Buffer.from(memory.buffer, ptr, len).toString('utf-8');
262
+ }
263
+ /**
264
+ * Read an AssemblyScript string — UTF-16LE, with a 4-byte little-endian byte
265
+ * length in the header immediately before the pointer — out of guest memory.
266
+ * Returns null if the pointer or the length it declares is out of bounds.
267
+ *
268
+ * That bounds check is the point of the helper. The two copies of this loop it
269
+ * replaces trusted the header, which is a value the guest writes: a rule
270
+ * aborting with a bogus pointer could name a 4 GB string and send the CLI into
271
+ * a multi-billion-iteration loop building it one char at a time. The proxy host
272
+ * checks the same range and logs the raw pointer when it does not fit
273
+ * (`packages/proxy/src/wasm/host.rs`).
274
+ */
275
+ function readGuestString(memory, ptr) {
276
+ const size = memory.buffer.byteLength;
277
+ if (!Number.isInteger(ptr) || ptr < 4 || ptr > size)
278
+ return null;
279
+ const byteLength = new DataView(memory.buffer).getUint32(ptr - 4, true);
280
+ if (byteLength % 2 !== 0 || ptr + byteLength > size)
281
+ return null;
282
+ return Buffer.from(memory.buffer, ptr, byteLength).toString('utf16le');
283
+ }
126
284
  /**
127
285
  * Instantiate a compiled WASM rule with the standard host imports and run
128
286
  * `evaluate` against a JSON-serialized RequestContext. Returns the raw
129
- * verdict code (0=ALLOW, 1=BLOCK, 2=REDACT). Throws on any instantiation or
287
+ * verdict code (0=ALLOW, 1=BLOCK, 3=REASK; 2 is a deprecated block). Throws on any instantiation or
130
288
  * ABI failure — callers decide how to surface it.
131
289
  */
132
- export async function instantiateAndEvaluate(wasmBuffer, mockStr, opts = {}) {
290
+ /**
291
+ * Re-exported so existing callers and tests keep one import site.
292
+ *
293
+ * The implementations live in `@intutic/shared-types` because three surfaces
294
+ * need them — this CLI, the control plane's upload endpoint, and the SDK test
295
+ * harness — and each having its own copy is precisely how `seed` came to be
296
+ * offered here and never registered by the proxy.
297
+ */
298
+ export const unsupportedImports = unsupportedWasmImports;
299
+ export const explainUnsupportedImport = explainWasmImport;
300
+ /**
301
+ * Instantiates a compiled rule with the proxy's host imports and evaluates it
302
+ * against one mock context, returning the raw verdict code.
303
+ *
304
+ * The import set here is the proxy's, not a convenient superset: a rule that
305
+ * links in this sandbox must link in the proxy, or install validation passes
306
+ * something the proxy then silently bypasses.
307
+ */
308
+ export async function instantiateAndEvaluate(wasmBuffer, mockStr) {
133
309
  let instanceRef = null;
310
+ /**
311
+ * Guest memory, or null before `WebAssembly.instantiate` has resolved.
312
+ *
313
+ * The null is load-bearing, not defensive padding: AssemblyScript's start
314
+ * function runs during instantiation and can call `abort` or `trace` from
315
+ * there, so a host import can be invoked before there is an instance to read
316
+ * memory from.
317
+ */
318
+ const guestMemory = () => {
319
+ const memory = instanceRef?.exports.memory;
320
+ return memory instanceof WebAssembly.Memory ? memory : null;
321
+ };
134
322
  const imports = {
135
323
  env: {
136
324
  // Mirrors the proxy's host import set (packages/proxy/src/wasm/host.rs)
137
325
  // — a rule that links in the proxy must link here too, or install
138
326
  // validation would reject rules the sandbox runs fine (and vice versa).
139
327
  log_info(message, len) {
140
- if (instanceRef && message) {
141
- const memory = instanceRef.exports.memory;
142
- const bytes = new Uint8Array(memory.buffer, message, len);
143
- console.log(`[WASM Log] ${Buffer.from(bytes).toString('utf-8')}`);
144
- }
328
+ const memory = guestMemory();
329
+ if (!memory || !message)
330
+ return;
331
+ const text = readGuestUtf8(memory, message, len);
332
+ console.log(text === null
333
+ ? `[WASM Log] <out of bounds: ptr=${message} len=${len}>`
334
+ : `[WASM Log] ${text}`);
145
335
  },
336
+ // `fileName` is part of AssemblyScript's fixed abort ABI and deliberately
337
+ // unused: it points at the source path, which is only populated under
338
+ // --debug and is a bare pointer otherwise. Not renamed to `_fileName`
339
+ // because it is a real parameter of a foreign ABI and the name documents
340
+ // the slot; `args: 'after-used'` means an unused parameter before two used
341
+ // ones is not reported anyway.
146
342
  abort(message, fileName, line, column) {
147
- let errorMsg = 'AssemblyScript abort';
148
- if (instanceRef && message) {
149
- const memory = instanceRef.exports.memory;
150
- const size = new Uint32Array(memory.buffer, message - 4, 1)[0];
151
- const memView16 = new Uint16Array(memory.buffer);
152
- const chars = [];
153
- for (let i = 0; i < size / 2; i++) {
154
- chars.push(String.fromCharCode(memView16[(message / 2) + i]));
155
- }
156
- errorMsg = chars.join('');
157
- }
158
- throw new Error(`WASM Abort: ${errorMsg} (at line ${line}, col ${column})`);
343
+ const memory = guestMemory();
344
+ const text = memory && message ? readGuestString(memory, message) : null;
345
+ throw new Error(`WASM Abort: ${text ?? 'AssemblyScript abort'} (at line ${line}, col ${column})`);
159
346
  },
160
- trace(message, n) {
161
- if (instanceRef && message) {
162
- const memory = instanceRef.exports.memory;
163
- const size = new Uint32Array(memory.buffer, message - 4, 1)[0];
164
- const memView16 = new Uint16Array(memory.buffer);
165
- const chars = [];
166
- for (let i = 0; i < size / 2; i++) {
167
- chars.push(String.fromCharCode(memView16[(message / 2) + i]));
168
- }
169
- console.log(`[WASM Trace] ${chars.join('')}`);
347
+ /**
348
+ * AssemblyScript's `trace(message, n, a0..a4)`: `n` says how many of the
349
+ * five f64 slots carry a value.
350
+ *
351
+ * The five slots were absent from this signature and `n` was accepted and
352
+ * dropped, so a rule calling `trace("tokens left", 1, remaining)` printed
353
+ * "tokens left" here and nothing else — the number the author was tracing
354
+ * never reached the output of the command whose whole job is to show them
355
+ * what their rule does. The proxy at least logs `n`
356
+ * (`packages/proxy/src/wasm/host.rs`).
357
+ */
358
+ trace(message, n, a0, a1, a2, a3, a4) {
359
+ const memory = guestMemory();
360
+ const text = memory && message ? readGuestString(memory, message) : null;
361
+ // A module may declare a narrower `env.trace`, in which case JS passes
362
+ // undefined for the slots it did not declare; drop anything non-finite.
363
+ const count = Number.isInteger(n) ? Math.min(Math.max(n, 0), 5) : 0;
364
+ const values = [a0, a1, a2, a3, a4].slice(0, count).filter((v) => Number.isFinite(v));
365
+ const suffix = values.length > 0 ? ` ${values.join(' ')}` : '';
366
+ if (text === null) {
367
+ console.log(`[WASM Trace Pointer] ${message} (n=${n})${suffix}`);
170
368
  }
171
369
  else {
172
- console.log(`[WASM Trace Pointer] ${message}`);
370
+ console.log(`[WASM Trace] ${text}${suffix}`);
173
371
  }
174
372
  },
175
- seed() {
176
- return Math.random();
177
- }
373
+ /**
374
+ * `env.read_referenced_file(pathPtr, pathLen, outPtr, outCap) -> i32`
375
+ *
376
+ * Always refuses here, and that is the honest answer rather than a gap:
377
+ * the proxy builds its readable-file table from the tool calls of a live
378
+ * request, and `intutic policy test` has a mock context, not a request.
379
+ * Pretending otherwise — serving the mock's paths off the developer's own
380
+ * disk — would make this harness disagree with production in the
381
+ * permissive direction, which is the failure the `runOnnxInference` mock
382
+ * below was removed for.
383
+ *
384
+ * -2 is ERR_REFUSED from `packages/proxy/src/wasm/referenced_files.rs`.
385
+ * A rule exercised here therefore takes its no-manifest-available branch,
386
+ * which is the branch worth checking anyway: it is what runs in any
387
+ * deployment that has not configured a manifest root.
388
+ */
389
+ read_referenced_file(_pathPtr, _pathLen, _outPtr, _outCap) {
390
+ return -2;
391
+ },
392
+ // `seed()` used to be here, and the comment above — asserting this set
393
+ // mirrors the proxy's — was false two lines later. `host.rs` has never
394
+ // registered it. AssemblyScript emits `env.seed` for anything reaching
395
+ // `Math.random()`, so a rule using randomness validated here, installed,
396
+ // and then failed to link in the proxy on every request, where fail-open
397
+ // turned it into a silent allow. Removing it cannot make enforcement
398
+ // worse: such a rule was already fully bypassed, and now it is refused
399
+ // loudly at install instead of quietly at runtime.
400
+ //
401
+ // Not added to the proxy instead, deliberately: a governance verdict that
402
+ // samples is not a verdict. The same request would get different answers.
178
403
  },
179
- onnx_rules: {
180
- runOnnxInference(modelNamePtr, inputDataPtr) {
181
- if (instanceRef && opts.forceAnomaly) {
182
- const memory = instanceRef.exports.memory;
183
- // TypedArray layout in AssemblyScript: buffer at offset 0, dataStart at offset 4
184
- const dataStart = new Uint32Array(memory.buffer, inputDataPtr + 4, 1)[0];
185
- // Mutate the backing buffer floats to trigger MSE reconstruction error
186
- const floats = new Float32Array(memory.buffer, dataStart, 180);
187
- for (let i = 0; i < floats.length; i++) {
188
- floats[i] = 99.0; // force large difference from one-hot 0.0/1.0
189
- }
190
- }
191
- return inputDataPtr;
192
- }
193
- }
404
+ // The `onnx_rules.runOnnxInference` mock lived here and is gone with the host
405
+ // import it imitated.
406
+ //
407
+ // It was worse than the production stub it stood for. The proxy's version returned
408
+ // the input pointer unchanged, so the rule it backed could never fire; this one,
409
+ // under --force-anomaly, rewrote the input buffer to 99.0 so the reconstruction
410
+ // error was enormous and the rule DID fire. A rule author could watch their
411
+ // sequence-anomaly rule block a request in `policy test`, ship it, and have it
412
+ // never block anything in production. A harness that disagrees with production in
413
+ // the permissive direction is worse than no harness.
194
414
  };
195
- const { instance } = (await WebAssembly.instantiate(wasmBuffer, imports));
415
+ // `Uint8Array` means `Uint8Array<ArrayBufferLike>`, and `BufferSource` demands
416
+ // `ArrayBufferView<ArrayBuffer>` — a view over a SharedArrayBuffer is not one,
417
+ // so the parameter type callers actually use (`Buffer` from `fs.readFile`) does
418
+ // not fit `WebAssembly.instantiate`. The old `as any` on the result hid that
419
+ // mismatch by silently selecting the `Module` overload instead, which returns a
420
+ // bare `Instance` with no `.instance` property: the destructuring below was
421
+ // reading a field the declared type does not have. Copy into a plainly-backed
422
+ // array rather than narrowing the parameter, which would reject `Buffer`. One
423
+ // copy of a rule binary, once per validation.
424
+ // Compile first and inspect the import section, so an unprovided import is
425
+ // named rather than surfacing as V8's "Import #3 module=env function=seed
426
+ // error: function import requires a callable". Same outcome either way — the
427
+ // instantiate below would throw — but a rule author needs to know it was
428
+ // `Math.random()`.
429
+ const compiled = await WebAssembly.compile(new Uint8Array(wasmBuffer));
430
+ const unsupported = unsupportedImports(compiled);
431
+ if (unsupported.length > 0) {
432
+ throw new Error(`rule imports ${unsupported.length} function(s) the proxy sandbox does not provide:\n` +
433
+ unsupported.map((n) => ` ${explainUnsupportedImport(n)}`).join('\n') +
434
+ `\n Available: ${HOST_IMPORT_NAMES.map((n) => `env.${n}`).join(', ')}.`);
435
+ }
436
+ const instance = await WebAssembly.instantiate(compiled, imports);
196
437
  instanceRef = instance;
438
+ const abi = resolveGuestAbi(instance);
197
439
  const jsonBytes = Buffer.from(mockStr);
198
- // Accept exactly the allocator exports the proxy runner accepts
199
- // (packages/proxy/src/wasm/runner.rs: `allocate`, then `__new`) — anything
200
- // looser would validate rules the proxy then silently fails open on.
201
- let offset = 0;
202
- if (typeof instance.exports.allocate === 'function') {
203
- offset = instance.exports.allocate(jsonBytes.length);
204
- }
205
- else if (typeof instance.exports.__new === 'function') {
206
- offset = instance.exports.__new(jsonBytes.length, 0);
207
- }
208
- else {
209
- throw new Error("WASM module is missing 'allocate' or '__new' memory helpers.");
210
- }
211
- const memory = instance.exports.memory;
212
- const memView = new Uint8Array(memory.buffer, offset, jsonBytes.length);
440
+ const offset = abi.allocate(jsonBytes.length);
441
+ // Re-read `abi.memory.buffer` here rather than caching a view: `allocate` can
442
+ // grow the memory, which detaches every ArrayBuffer taken before the call.
443
+ const memView = new Uint8Array(abi.memory.buffer, offset, jsonBytes.length);
213
444
  memView.set(jsonBytes);
214
- const evaluate = instance.exports.evaluate;
215
- if (typeof evaluate !== 'function') {
216
- throw new Error("WASM module is missing 'evaluate' function export.");
217
- }
218
- return evaluate(offset, jsonBytes.length);
445
+ return abi.evaluate(offset, jsonBytes.length);
219
446
  }
220
447
  export async function runPolicyTest(opts) {
221
448
  log.header('Intutic — Test Local WASM Policy Rule');
222
449
  let wasmBuffer;
223
450
  let mockStr;
224
- let forceAnomaly = false;
225
451
  try {
226
452
  wasmBuffer = await fs.readFile(opts.wasm);
227
453
  }
228
454
  catch (err) {
229
- log.error(`Failed to read WASM file at "${opts.wasm}": ${err.message}`);
455
+ log.error(`Failed to read WASM file at "${opts.wasm}": ${errMessage(err)}`);
230
456
  process.exit(1);
231
457
  }
232
458
  try {
233
459
  mockStr = await fs.readFile(opts.mock, 'utf-8');
234
- const parsed = JSON.parse(mockStr); // syntax check
235
- if (parsed && parsed.mock_anomaly === true) {
236
- forceAnomaly = true;
237
- }
460
+ JSON.parse(mockStr); // syntax check
238
461
  }
239
462
  catch (err) {
240
- log.error(`Failed to read or parse mock context JSON at "${opts.mock}": ${err.message}`);
463
+ log.error(`Failed to read or parse mock context JSON at "${opts.mock}": ${errMessage(err)}`);
241
464
  process.exit(1);
242
465
  }
243
466
  try {
244
- const verdict = await instantiateAndEvaluate(wasmBuffer, mockStr, { forceAnomaly });
467
+ const verdict = await instantiateAndEvaluate(wasmBuffer, mockStr);
245
468
  log.info(`Dry-run evaluation executed successfully.`);
246
469
  log.field('WASM Verdict Code', String(verdict));
247
470
  if (verdict === 0) {
@@ -250,15 +473,23 @@ export async function runPolicyTest(opts) {
250
473
  else if (verdict === 1) {
251
474
  log.warn('Result: BLOCK / KILL');
252
475
  }
476
+ else if (verdict === 3) {
477
+ log.warn('Result: REASK — the agent is told why and may retry');
478
+ }
253
479
  else if (verdict === 2) {
254
- log.warn('Result: REDACT / BLOCK');
480
+ log.warn('Result: BLOCK (deprecated code 2)');
481
+ log.info(' 2 was specified as REDACT and never could be: a rule is not given');
482
+ log.info(' the request body, so there is nothing to redact. The proxy treats it');
483
+ log.info(' as a block. Return 1 to block, or 3 to reask.');
255
484
  }
256
485
  else {
257
- log.error(`Result: Unknown verdict code ${verdict}`);
486
+ log.error(`Result: unmapped verdict code ${verdict}`);
487
+ log.info(' Valid codes are 0 (allow), 1 (block) and 3 (reask). The proxy allows');
488
+ log.info(' anything else, so a rule returning this enforces nothing.');
258
489
  }
259
490
  }
260
491
  catch (err) {
261
- log.error(`Execution error during WASM policy test: ${err.message}`);
492
+ log.error(`Execution error during WASM policy test: ${errMessage(err)}`);
262
493
  process.exit(1);
263
494
  }
264
495
  }
@@ -312,10 +543,123 @@ export function buildCompileArgs(opts) {
312
543
  }
313
544
  return args;
314
545
  }
546
+ /** Where a fetched candidate source is written: two directories below the project root, which is what its `../../assembly/index` import needs. */
547
+ export function candidateSourcePath(candidateId) {
548
+ return path.join('generated', 'candidates', `${candidateId}.ts`);
549
+ }
550
+ export function candidateOutPath(candidateId) {
551
+ return path.join('build', `${candidateId}.wasm`);
552
+ }
553
+ export function sourceSha256Of(source) {
554
+ return createHash('sha256').update(source, 'utf8').digest('hex');
555
+ }
556
+ /**
557
+ * Fetch a candidate's source of record and check it against the hash the
558
+ * server states. A body that does not hash to its own `sourceSha256` is not
559
+ * compiled: `/bundle` would refuse the result anyway, and a mismatch here
560
+ * means something between the control plane and this process changed bytes.
561
+ */
562
+ export async function fetchCandidateSource(client, candidateId) {
563
+ const { status, body } = await client.getWithStatus(`/api/v1/rule-candidates/${encodeURIComponent(candidateId)}/source`);
564
+ if (status === 404) {
565
+ log.error(`Rule candidate "${candidateId}" was not found in this workspace.`);
566
+ process.exit(1);
567
+ }
568
+ if (status === 409) {
569
+ log.error(`Refused: ${body.error ?? 'conflict'}${body.detail ? ` — ${body.detail}` : ''}`);
570
+ process.exit(1);
571
+ }
572
+ if (status !== 200 || typeof body.source !== 'string' || typeof body.sourceSha256 !== 'string') {
573
+ log.error(`Could not fetch the candidate's source (HTTP ${status}): ${body.error ?? 'unexpected response'}`);
574
+ process.exit(1);
575
+ }
576
+ const local = sourceSha256Of(body.source);
577
+ if (local !== body.sourceSha256.toLowerCase()) {
578
+ log.error(`The served source hashes to ${local}, not the ${body.sourceSha256} the server states; refusing to compile it.`);
579
+ process.exit(1);
580
+ }
581
+ return {
582
+ candidateId: body.candidateId ?? candidateId,
583
+ guardrailId: body.guardrailId ?? null,
584
+ status: body.status ?? 'unknown',
585
+ source: body.source,
586
+ sourceSha256: body.sourceSha256.toLowerCase(),
587
+ };
588
+ }
589
+ /**
590
+ * Upload a compiled bundle to its candidate together with the hash of the
591
+ * source it was compiled from, and print the gate results. The server
592
+ * recomputes the hash from the candidate row; a bundle built from anything
593
+ * else is refused before a gate runs.
594
+ */
595
+ export async function uploadCandidateBundle(client, candidateId, wasmPath, sourceSha256) {
596
+ let bytes;
597
+ try {
598
+ bytes = await fs.readFile(wasmPath);
599
+ }
600
+ catch (err) {
601
+ log.error(`Failed to read the compiled bundle at "${wasmPath}": ${errMessage(err)}`);
602
+ process.exit(1);
603
+ }
604
+ const form = new FormData();
605
+ form.append('file', new Blob([new Uint8Array(bytes)], { type: 'application/wasm' }), path.basename(wasmPath));
606
+ form.append('source_sha256', sourceSha256);
607
+ const { status, body } = await client.postForm(`/api/v1/rule-candidates/${encodeURIComponent(candidateId)}/bundle`, form);
608
+ if (status === 404) {
609
+ log.error(`Rule candidate "${candidateId}" was not found in this workspace.`);
610
+ process.exit(1);
611
+ }
612
+ if (status === 400 || status === 409) {
613
+ log.error(`Refused: ${body.error ?? `HTTP ${status}`}${body.detail ? ` — ${body.detail}` : ''}`);
614
+ process.exit(1);
615
+ }
616
+ if (status !== 200 && status !== 201) {
617
+ log.error(`Upload failed (HTTP ${status}): ${body.error ?? 'unexpected response'}`);
618
+ process.exit(1);
619
+ }
620
+ for (const g of body.gates ?? [])
621
+ log.info(` ${g.passed ? '✓' : '✗'} ${g.gate}: ${g.detail}`);
622
+ if (body.accepted) {
623
+ log.success(`Accepted: rule ${body.ruleId ?? '?'} is in ${body.mode ?? 'SHADOW'} for candidate ${candidateId}.`);
624
+ log.info('It reports without acting until a member promotes it on counted shadow evidence.');
625
+ return;
626
+ }
627
+ log.error('The bundle failed verification; the candidate is rejected with the gate named above.');
628
+ process.exit(1);
629
+ }
315
630
  export async function runPolicyCompile(opts) {
316
631
  log.header('Intutic — Compile WASM Policy Rule');
317
- const args = buildCompileArgs(opts);
318
- const out = opts.out ?? 'build/rule.wasm';
632
+ let src = opts.src;
633
+ let out = opts.out ?? 'build/rule.wasm';
634
+ let candidate = null;
635
+ let client = null;
636
+ if (opts.candidate) {
637
+ if (opts.src) {
638
+ log.error('--candidate compiles the source of record; it cannot be combined with --src.');
639
+ process.exit(1);
640
+ }
641
+ try {
642
+ await fs.access(path.join('assembly', 'index.ts'));
643
+ }
644
+ catch {
645
+ log.error('Run this from a rule project with assembly/index.ts (the SDK layout): the generated source imports the SDK from two directories up.');
646
+ process.exit(1);
647
+ }
648
+ client = await getClient(opts.dev);
649
+ candidate = await fetchCandidateSource(client, opts.candidate);
650
+ src = candidateSourcePath(opts.candidate);
651
+ if (!opts.out)
652
+ out = candidateOutPath(opts.candidate);
653
+ await fs.mkdir(path.dirname(src), { recursive: true });
654
+ await fs.writeFile(src, candidate.source, 'utf8');
655
+ log.info(`Source of record for ${candidate.candidateId} (${candidate.status}) → ${src}`);
656
+ log.info(`source_sha256 ${candidate.sourceSha256}`);
657
+ }
658
+ else if (opts.upload) {
659
+ log.error('--upload needs --candidate: only a candidate has a source of record to upload against.');
660
+ process.exit(1);
661
+ }
662
+ const args = buildCompileArgs({ src, out, debug: opts.debug });
319
663
  try {
320
664
  await fs.mkdir(path.dirname(out), { recursive: true });
321
665
  }
@@ -333,8 +677,16 @@ export async function runPolicyCompile(opts) {
333
677
  log.info(' pnpm add -D assemblyscript assemblyscript-json');
334
678
  process.exit(1);
335
679
  }
336
- log.success(`Compiled ${opts.src ?? 'assembly/index.ts'} → ${out}`);
337
- log.info('Next: dry-run with `intutic policy test --wasm <out> --mock <mock.json>`');
680
+ log.success(`Compiled ${src ?? 'assembly/index.ts'} → ${out}`);
681
+ if (!candidate || !opts.candidate) {
682
+ log.info('Next: dry-run with `intutic policy test --wasm <out> --mock <mock.json>`');
683
+ return;
684
+ }
685
+ if (!opts.upload) {
686
+ log.info(`Next: intutic policy compile --candidate ${opts.candidate} --upload (or upload ${out} with source_sha256 ${candidate.sourceSha256})`);
687
+ return;
688
+ }
689
+ await uploadCandidateBundle(client, opts.candidate, out, candidate.sourceSha256);
338
690
  }
339
691
  export async function runPolicyInstall(opts) {
340
692
  log.header('Intutic — Install Local WASM Policy Rule');
@@ -343,16 +695,34 @@ export async function runPolicyInstall(opts) {
343
695
  wasmBuffer = await fs.readFile(opts.wasm);
344
696
  }
345
697
  catch (err) {
346
- log.error(`Failed to read WASM file at "${opts.wasm}": ${err.message}`);
698
+ log.error(`Failed to read WASM file at "${opts.wasm}": ${errMessage(err)}`);
347
699
  process.exit(1);
348
700
  }
349
701
  // Refuse to install a rule that cannot instantiate or evaluate — a broken
350
702
  // rule enforces nothing (the proxy sandbox fails open).
351
703
  try {
352
- await instantiateAndEvaluate(wasmBuffer, DEFAULT_ALLOW_MOCK);
704
+ const validationVerdict = await instantiateAndEvaluate(wasmBuffer, DEFAULT_ALLOW_MOCK);
705
+ // Refuse a code the proxy does not map. Everything outside {0,1,2,3} is
706
+ // allowed at runtime with a warning, so a rule inventing a rung installs
707
+ // clean and then enforces nothing — the same silent shape as a rule that
708
+ // cannot link. 2 is accepted here because already-installed rules use it,
709
+ // but it is deprecated and reported as such.
710
+ if (validationVerdict === 2) {
711
+ // Accepted, because refusing it would break reinstalling a rule that
712
+ // already shipped — but never silently. The guest never receives the
713
+ // request body, so redaction was never expressible, and the proxy maps 2
714
+ // to a block. An author who believes they are redacting is blocking.
715
+ log.warn('Rule returned verdict code 2 (REDACT), which is deprecated. The proxy treats it ' +
716
+ 'as a block. Return 1 to block, or 3 to reask.');
717
+ }
718
+ if (![0, 1, 2, 3].includes(validationVerdict)) {
719
+ log.error(`Rule returned verdict code ${validationVerdict}, which the proxy does not map — ` +
720
+ 'it would be allowed on every request. Valid codes: 0 allow, 1 block, 3 reask.');
721
+ process.exit(1);
722
+ }
353
723
  }
354
724
  catch (err) {
355
- log.error(`Rule failed validation and was NOT installed: ${err.message}`);
725
+ log.error(`Rule failed validation and was NOT installed: ${errMessage(err)}`);
356
726
  process.exit(1);
357
727
  }
358
728
  const priority = parseInt(opts.priority ?? '100', 10);
@@ -369,7 +739,7 @@ export async function runPolicyInstall(opts) {
369
739
  await fs.writeFile(dest, wasmBuffer);
370
740
  }
371
741
  catch (err) {
372
- log.error(`Failed to install rule to "${dest}": ${err.message}`);
742
+ log.error(`Failed to install rule to "${dest}": ${errMessage(err)}`);
373
743
  process.exit(1);
374
744
  }
375
745
  const sha256 = createHash('sha256').update(wasmBuffer).digest('hex');
@@ -380,6 +750,77 @@ export async function runPolicyInstall(opts) {
380
750
  log.info('The proxy picks up local rule changes within ~5 s on the next request.');
381
751
  log.info('If your proxy config.yaml sets intutic_settings.wasm_local_dir, make sure it matches this path (or set INTUTIC_WASM_DIR for both).');
382
752
  }
753
+ const VERDICT_NAMES = {
754
+ '0': 'allow',
755
+ '1': 'block',
756
+ '2': 'block (deprecated code 2)',
757
+ '3': 'reask',
758
+ };
759
+ /**
760
+ * `intutic policy replay <ruleId>` — "prove before enforce": run an installed
761
+ * or not-yet-active rule against a sample of this workspace's own recent
762
+ * traffic and report what it would have done, without touching enforcement.
763
+ *
764
+ * Uses `postWithStatus` rather than `post`: the 422 the route returns for too
765
+ * little sampled traffic is an answer this command exists to report, not a
766
+ * request failure to throw past.
767
+ */
768
+ export async function runPolicyReplay(ruleId, opts) {
769
+ log.header(`Intutic — Replay Policy Rule: ${ruleId}`);
770
+ const client = await getClient(opts.dev);
771
+ const body = {};
772
+ if (opts.limit !== undefined) {
773
+ const limit = parseInt(opts.limit, 10);
774
+ if (isNaN(limit) || limit <= 0) {
775
+ log.error(`Invalid --limit "${opts.limit}". Must be a positive integer.`);
776
+ process.exit(1);
777
+ }
778
+ body.limit = limit;
779
+ }
780
+ if (opts.since !== undefined) {
781
+ body.since = opts.since;
782
+ }
783
+ try {
784
+ const { status, body: result } = await client.postWithStatus(`/api/v1/wasm-rules/${ruleId}/replay`, body);
785
+ if (status === 404) {
786
+ log.error(`Rule "${ruleId}" not found in this workspace.`);
787
+ process.exit(1);
788
+ }
789
+ if (status === 422) {
790
+ const err = result;
791
+ if (err.error === 'not_enough_traffic') {
792
+ log.warn('Not enough sampled traffic to estimate a would-act rate.');
793
+ log.field('Sampled contexts', String(err.contextCount ?? 0));
794
+ log.field('Required', String(err.minRequired ?? 0));
795
+ log.info('Contexts are sampled at WASM_CONTEXT_SNAPSHOT_RATE (default 5%) as traffic flows ' +
796
+ 'through the proxy — this grows with time and traffic, not with --limit.');
797
+ }
798
+ else {
799
+ log.error(`Replay could not evaluate the rule: ${err.message ?? err.error}`);
800
+ }
801
+ process.exit(1);
802
+ }
803
+ if (status !== 200) {
804
+ log.error(`Replay failed (${status}): ${JSON.stringify(result)}`);
805
+ process.exit(1);
806
+ }
807
+ const summary = result;
808
+ log.success(`Replayed against ${summary.contextCount} sampled context(s).`);
809
+ log.field('Since', summary.since ?? '(all sampled history)');
810
+ log.field('Would act (block/reask/other)', `${summary.wouldActCount} (${(summary.wouldActRate * 100).toFixed(1)}%)`);
811
+ for (const [code, count] of Object.entries(summary.verdictCounts)) {
812
+ log.field(` verdict ${code} (${VERDICT_NAMES[code] ?? 'unmapped'})`, String(count));
813
+ }
814
+ if (summary.sampleMatches.length > 0) {
815
+ log.info(`First ${summary.sampleMatches.length} non-allow context(s):`);
816
+ console.log(JSON.stringify(summary.sampleMatches, null, 2));
817
+ }
818
+ }
819
+ catch (err) {
820
+ log.error(`Failed to replay rule: ${errMessage(err)}`);
821
+ process.exit(1);
822
+ }
823
+ }
383
824
  export async function runPolicyListLocal() {
384
825
  log.header('Intutic — Local WASM Policy Rules');
385
826
  const dir = resolveLocalWasmDir();
@@ -405,7 +846,7 @@ export async function runPolicyListLocal() {
405
846
  log.field(name, `priority=${priority} size=${stat.size}B mtime=${stat.mtime.toISOString()} sha256=${sha256.slice(0, 16)}…`);
406
847
  }
407
848
  catch (err) {
408
- log.warn(`${file}: unreadable (${err.message})`);
849
+ log.warn(`${file}: unreadable (${errMessage(err)})`);
409
850
  }
410
851
  }
411
852
  }