@wenathlan/devthink 2.0.14 → 2.0.19

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 (444) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
@@ -1,4793 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { existsSync } from "node:fs";
3
- import {
4
- allowlistreport,
5
- approvalframes,
6
- batchreport,
7
- calllogreport,
8
- cancelframes,
9
- dryrunreport,
10
- eventnotification,
11
- heartbeatreport,
12
- httpstreamreport,
13
- idempotencyreplayframe,
14
- idempotencyreport,
15
- inflightreport,
16
- mockreport,
17
- pairingframes,
18
- progressnoticeframe,
19
- promptcallframe,
20
- promptreport,
21
- ratelimitreport,
22
- resourcedeltareport,
23
- samplingframes,
24
- streamchunkframe,
25
- structurederrorreport,
26
- subscriptionframes,
27
- tlsreport,
28
- tokenreport,
29
- toolcallframe,
30
- toolresultframe,
31
- ecosystemviews,
32
- } from "../protocol.js";
33
- import {
34
- authreport,
35
- callsreport,
36
- capturereport,
37
- cdpreport,
38
- consolediffreport,
39
- controlreport,
40
- datasetresponse,
41
- diffresponse,
42
- downloadreport,
43
- editorstate,
44
- errorreportresponse,
45
- eventresponse,
46
- exchangesreport,
47
- extractionreport,
48
- formreportresponse,
49
- heldkeysreport,
50
- layoutreport,
51
- mapresponse,
52
- mediareport,
53
- navstateresponse,
54
- netlogreport,
55
- observationresponse,
56
- emulationreport,
57
- outcomeresponse,
58
- parseproposal,
59
- profilereport,
60
- provenancereport,
61
- quarantinereport,
62
- requestbody,
63
- runhistoryquery,
64
- runhistoryreport,
65
- safetyresponse,
66
- selectorresponse,
67
- signalsreport,
68
- tabreportresponse,
69
- timelinereport,
70
- trailreport,
71
- transformgrammar,
72
- wizardreport,
73
- workflowfileversion,
74
- } from "../protocol.js";
75
- import { modeloutcome, modelproposal, environmentgrammar, environmentreport } from "../protocol.js";
76
- import { consentmodel, logchainreport, securityreport, surfacesnapshot, interfaceviews } from "../protocol.js";
77
- import { agenteventframe, boardstatesnapshot, handoffframe, reviewframe, swarmstatereport } from "../protocol.js";
78
- import {
79
- protocolversion,
80
- type a11ynode,
81
- type modeloutput,
82
- type plandraft,
83
- type agentplan,
84
- type apimapentry,
85
- type breakpointspec,
86
- type callrecord,
87
- type cdpcommand,
88
- type cdpeventrule,
89
- type cdpsession,
90
- type cpuprofile,
91
- type debuggergrant,
92
- type flowmetric,
93
- type growsample,
94
- type heaprecord,
95
- type memorytrend,
96
- type shiftentry,
97
- type sourcemapconsent,
98
- type sourcemapref,
99
- type tracerecord,
100
- type pausestate,
101
- type scriptoverride,
102
- type watchexpression,
103
- type channelrecord,
104
- type columnspec,
105
- type dataset,
106
- type eventsubscription,
107
- type exchangerecord,
108
- type downloadrecord,
109
- type extractsession,
110
- type imagebatch,
111
- type mediarecord,
112
- type mutationevent,
113
- type focusevent,
114
- type bannerreport,
115
- type errorreport,
116
- type formreport,
117
- type navstate,
118
- type netlogrecord,
119
- type observation,
120
- type provenancerecord,
121
- type quarantineentry,
122
- type redirectchain,
123
- type safetyverdict,
124
- type selectorcandidate,
125
- type shotpair,
126
- type shotrecord,
127
- type snapshotdiff,
128
- type stepoutcome,
129
- type tablayout,
130
- type tabreport,
131
- type transformrule,
132
- type typeaheadpick,
133
- type wizardstate,
134
- type swarmstate,
135
- } from "../types.js";
136
-
137
- describe("protocol", () => {
138
- it("turns a valid proposal into a pending local review", () => {
139
- const proposal = parseproposal(
140
- {
141
- version: protocolversion,
142
- plan: {
143
- objective: "Read the page",
144
- steps: [{ kind: "observe", summary: "Capture an approved page snapshot" }],
145
- },
146
- },
147
- "https://example.com",
148
- );
149
- expect(proposal.plan.state).toBe("pending");
150
- expect(proposal.plan.steps[0]?.risk).toBe("read");
151
- });
152
-
153
- it("rejects unsupported versions and cross-origin routes", () => {
154
- expect(() =>
155
- parseproposal({ version: "1.0.0", plan: { objective: "x", steps: [] } }, "https://example.com"),
156
- ).toThrow("protocol");
157
- expect(() =>
158
- parseproposal(
159
- {
160
- version: protocolversion,
161
- plan: {
162
- objective: "x",
163
- steps: [{ kind: "navigate", value: "https://elsewhere.example", summary: "Leave site" }],
164
- },
165
- },
166
- "https://example.com",
167
- ),
168
- ).toThrow("approved origin");
169
- });
170
-
171
- it("accepts the expanded action vocabulary with derived risk levels", () => {
172
- const proposal = parseproposal(
173
- {
174
- version: protocolversion,
175
- plan: {
176
- objective: "Review a richer flow",
177
- steps: [
178
- { kind: "scroll", target: "#menu", summary: "Scroll the reviewed menu into view." },
179
- { kind: "hover", target: "#menu", summary: "Hover the reviewed menu." },
180
- { kind: "select", target: "#country", value: "br", summary: "Select the reviewed country." },
181
- { kind: "extract", summary: "Extract bounded page links." },
182
- { kind: "wait", value: "500", summary: "Wait half a second between steps." },
183
- { kind: "presskey", value: "Enter", options: '{"modifiers":["shift"]}', summary: "Press shift enter." },
184
- { kind: "readtable", target: "#prices", summary: "Read the reviewed price table." },
185
- { kind: "drag", target: "#card", value: "#board", summary: "Drag the reviewed card to the board." },
186
- { kind: "tablist", summary: "List the open tabs." },
187
- { kind: "evaluate", value: "document.title", summary: "Read the title through a reviewed expression." },
188
- ],
189
- },
190
- },
191
- "https://example.com",
192
- );
193
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual([
194
- "interaction",
195
- "interaction",
196
- "sensitive",
197
- "read",
198
- "read",
199
- "sensitive",
200
- "read",
201
- "sensitive",
202
- "read",
203
- "sensitive",
204
- ]);
205
- expect(proposal.plan.steps[5]?.options).toBe('{"modifiers":["shift"]}');
206
- });
207
-
208
- it("accepts plans with an unlimited number of reviewed steps", () => {
209
- const steps = Array.from({ length: 150 }, (_, index) => ({
210
- kind: "wait",
211
- value: "1",
212
- summary: `Reviewed pause ${index + 1}.`,
213
- }));
214
- const proposal = parseproposal(
215
- { version: protocolversion, plan: { objective: "Long reviewed flow", steps } },
216
- "https://example.com",
217
- );
218
- expect(proposal.plan.steps).toHaveLength(150);
219
- expect(() =>
220
- parseproposal({ version: protocolversion, plan: { objective: "Empty flow", steps: [] } }, "https://example.com"),
221
- ).toThrow("at least one step");
222
- });
223
-
224
- it("honors a user configured plan expiry without a fixed ceiling", () => {
225
- const createdat = Date.now();
226
- const twohours = createdat + 2 * 60 * 60 * 1000;
227
- const proposal = parseproposal(
228
- {
229
- version: protocolversion,
230
- plan: {
231
- objective: "Long lived review",
232
- expiresat: twohours,
233
- steps: [{ kind: "observe", summary: "Observe once." }],
234
- },
235
- },
236
- "https://example.com",
237
- );
238
- expect(proposal.plan.expiresat).toBe(twohours);
239
- });
240
-
241
- it("shapes the proposal request with the capability report and wraps step outcomes", () => {
242
- const body = JSON.parse(
243
- requestbody({
244
- objective: "Read prices",
245
- session: { id: "session", tabid: 4, origin: "https://example.com", startedat: 1, expiresat: 2 },
246
- observation: {
247
- schemaversion: 2,
248
- url: "https://example.com",
249
- title: "Example",
250
- textpreview: "",
251
- textlength: 0,
252
- forms: [],
253
- interactive: [],
254
- capturedat: 1,
255
- },
256
- capabilities: { tabs: true, downloads: false, clipboardread: false, clipboardwrite: false, reportedat: 1 },
257
- }),
258
- );
259
- expect(body.capabilities.tabs).toBe(true);
260
- expect(body.version).toBe(protocolversion);
261
- const envelope = JSON.parse(
262
- outcomeresponse({
263
- outcome: { stepid: "step", ok: true, summary: "Done.", details: { count: 3 }, at: 5 },
264
- plan: {
265
- id: "plan",
266
- objective: "o",
267
- origin: "https://example.com",
268
- steps: [],
269
- createdat: 1,
270
- expiresat: 2,
271
- state: "approved",
272
- },
273
- }),
274
- );
275
- expect(envelope.outcome.details.count).toBe(3);
276
- expect(envelope.planstate).toBe("approved");
277
- expect(envelope.resolvedtarget).toBeUndefined();
278
- });
279
-
280
- it("attaches the resolvedtarget summary to the outcome envelope for review", () => {
281
- const plan = {
282
- id: "plan",
283
- objective: "o",
284
- origin: "https://example.com",
285
- steps: [],
286
- createdat: 1,
287
- expiresat: 2,
288
- state: "approved" as const,
289
- };
290
- const resolvedtarget = {
291
- mode: "text" as const,
292
- selector: "#signin",
293
- tag: "button",
294
- label: "Sign in",
295
- geometry: { x: 12, y: 30, width: 90, height: 32 },
296
- };
297
- const envelope = JSON.parse(
298
- outcomeresponse({
299
- outcome: {
300
- stepid: "t1",
301
- ok: true,
302
- summary: "Clicked Sign in.",
303
- details: { mode: "text", resolvedtarget },
304
- at: 9,
305
- },
306
- plan,
307
- resolvedtarget,
308
- }),
309
- );
310
- expect(envelope.version).toBe(protocolversion);
311
- expect(envelope.resolvedtarget).toEqual(resolvedtarget);
312
- expect(envelope.planid).toBe("plan");
313
- });
314
-
315
- it("wraps clickablemap payloads with numbered entries in the versioned envelope", () => {
316
- const plan = {
317
- id: "plan",
318
- objective: "o",
319
- origin: "https://example.com",
320
- steps: [],
321
- createdat: 1,
322
- expiresat: 2,
323
- state: "approved" as const,
324
- };
325
- const map = {
326
- version: 4,
327
- builtat: 77,
328
- entries: [
329
- { number: 1, selector: "a:nth-of-type(1)", role: "link", label: "Home", mode: "selector" as const },
330
- { number: 2, selector: "#search", role: "button", label: "Search", mode: "selector" as const },
331
- ],
332
- };
333
- const envelope = JSON.parse(mapresponse({ map, plan }));
334
- expect(envelope.version).toBe(protocolversion);
335
- expect(envelope.planid).toBe("plan");
336
- expect(envelope.map.version).toBe(4);
337
- expect((envelope.map.entries as Array<{ number: number }>).map((entry) => entry.number)).toEqual([1, 2]);
338
- expect(envelope.map.entries[1]).toMatchObject({ number: 2, selector: "#search", role: "button", label: "Search" });
339
- });
340
-
341
- it("reports the keys currently held on one tab in the context envelope", () => {
342
- const report = heldkeysreport({
343
- tabid: 4,
344
- holds: [
345
- { holdid: "shift1", key: "Shift", modifiers: ["shift"], tabid: 4, stepid: "s1", pressedat: 10 },
346
- { holdid: "ctrl1", key: "Control", tabid: 4, stepid: "s2", pressedat: 20, releasedat: 30 },
347
- ],
348
- });
349
- expect(report.version).toBe(protocolversion);
350
- expect(report.tabid).toBe(4);
351
- expect(report.heldkeys).toHaveLength(2);
352
- expect(report.heldkeys[0]).toMatchObject({ holdid: "shift1", key: "Shift" });
353
- expect(report.heldkeys[1]?.releasedat).toBe(30);
354
- });
355
-
356
- it("accepts the interaction universe vocabulary with its reviewed options grammar", () => {
357
- const proposal = parseproposal(
358
- {
359
- version: protocolversion,
360
- plan: {
361
- objective: "Complete the login flow",
362
- steps: [
363
- { id: "map", kind: "mapclicks", summary: "Number every clickable element." },
364
- {
365
- kind: "movepointer",
366
- summary: "Travel the reviewed path.",
367
- options:
368
- '{"pointpath":{"start":{"x":0,"y":0},"end":{"x":120,"y":60},"duration":400},"speedprofile":{"easing":"easeinout","peak":800}}',
369
- },
370
- {
371
- kind: "clicktext",
372
- summary: "Click the sign in control by text.",
373
- options: '{"targetref":{"mode":"text","text":"Sign in"}}',
374
- },
375
- {
376
- kind: "clickaria",
377
- summary: "Click the submit control by aria pair.",
378
- options: '{"targetref":{"mode":"aria","role":"button","name":"Submit"}}',
379
- },
380
- {
381
- kind: "typetime",
382
- target: "#email",
383
- value: "user@example.com",
384
- summary: "Type the reviewed email slowly.",
385
- options: '{"delay":90}',
386
- },
387
- { kind: "keyhold", value: "Shift", summary: "Hold shift for the range.", options: '{"holdid":"shift1"}' },
388
- { kind: "keyrelease", value: "shift1", summary: "Release the held shift key." },
389
- { kind: "setslider", target: "#volume", value: "40", summary: "Drag the volume slider." },
390
- { kind: "verifyvisible", target: "#results", summary: "Verify the results render." },
391
- { kind: "dismissdialog", summary: "Accept the next dialog.", options: '{"accept":true}' },
392
- {
393
- kind: "retryaction",
394
- summary: "Retry the moving banner click.",
395
- options: '{"stepid":"banner","retryrule":{"attempts":3,"settle":200,"tolerance":4}}',
396
- },
397
- { id: "banner", kind: "click", target: "#banner", summary: "Click the moving banner." },
398
- ],
399
- },
400
- },
401
- "https://example.com",
402
- );
403
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual([
404
- "read",
405
- "interaction",
406
- "interaction",
407
- "interaction",
408
- "sensitive",
409
- "sensitive",
410
- "sensitive",
411
- "sensitive",
412
- "read",
413
- "sensitive",
414
- "interaction",
415
- "sensitive",
416
- ]);
417
- expect(proposal.plan.steps[1]?.options).toContain("pointpath");
418
- });
419
-
420
- it("rejects wrapper steps that reference unknown sibling step ids", () => {
421
- expect(() =>
422
- parseproposal(
423
- {
424
- version: protocolversion,
425
- plan: {
426
- objective: "Retry a ghost",
427
- steps: [
428
- {
429
- id: "wrapper",
430
- kind: "retryaction",
431
- summary: "Retry the ghost click.",
432
- options: '{"stepid":"ghost","retryrule":{"attempts":2}}',
433
- },
434
- ],
435
- },
436
- },
437
- "https://example.com",
438
- ),
439
- ).toThrow("unknown step id");
440
- expect(() =>
441
- parseproposal(
442
- {
443
- version: protocolversion,
444
- plan: {
445
- objective: "Frame a ghost",
446
- steps: [
447
- {
448
- id: "frame",
449
- kind: "enterframe",
450
- summary: "Route inside the frame.",
451
- options: '{"framepath":[0],"stepid":"ghost"}',
452
- },
453
- ],
454
- },
455
- },
456
- "https://example.com",
457
- ),
458
- ).toThrow("unknown step id");
459
- });
460
-
461
- it("accepts the observation depth vocabulary with its reviewed options grammar", () => {
462
- const proposal = parseproposal(
463
- {
464
- version: protocolversion,
465
- plan: {
466
- objective: "See the whole page",
467
- steps: [
468
- { kind: "a11ytree", summary: "Capture the accessibility tree." },
469
- { kind: "readertree", summary: "Extract the reader view." },
470
- { kind: "readvisible", target: "#main", summary: "Read the visible text." },
471
- { kind: "detectlists", summary: "Detect repeated lists." },
472
- { kind: "detecttables", summary: "Detect data tables." },
473
- { kind: "readjson", summary: "Read the embedded json state." },
474
- { kind: "watchmutate", summary: "Watch the feed.", options: '{"lifetime":2000,"scopes":["#feed"]}' },
475
- {
476
- kind: "waitquiet",
477
- summary: "Wait for network quiet.",
478
- options: '{"quietrule":{"idle":400,"timeout":6000}}',
479
- },
480
- { kind: "watchbanner", summary: "Watch consent banners.", options: '{"lifetime":1500}' },
481
- { kind: "detectinfinitescroll", summary: "Detect infinite scroll." },
482
- { kind: "detectvirtual", summary: "Detect virtualized lists." },
483
- { kind: "detectlazy", summary: "Detect lazy images." },
484
- { kind: "readscrollpos", summary: "Read the scroll position." },
485
- { kind: "readlang", summary: "Read the page language." },
486
- { kind: "readoutline", summary: "Read the heading outline." },
487
- { kind: "countpages", summary: "Count the pagination." },
488
- { kind: "listshadow", summary: "List open shadow roots." },
489
- { kind: "listframes", summary: "List the iframes." },
490
- { kind: "classifypage", summary: "Classify the page template." },
491
- { kind: "fingerprintsection", target: "#prices", summary: "Fingerprint the section." },
492
- { kind: "diffsnapshots", summary: "Diff two observation versions.", options: '{"versions":[1,2]}' },
493
- { kind: "readselection", summary: "Read the current selection." },
494
- { kind: "watchfocus", summary: "Watch focus changes.", options: '{"lifetime":900}' },
495
- { kind: "detectsticky", summary: "Detect sticky overlays." },
496
- { kind: "detectscrolllock", summary: "Detect scroll locks." },
497
- { kind: "readopengraph", summary: "Read the open graph fields." },
498
- { kind: "detectlanguage", summary: "Detect the text language." },
499
- { kind: "deriveselector", target: "#checkout", summary: "Derive a stable selector." },
500
- ],
501
- },
502
- },
503
- "https://example.com",
504
- );
505
- expect(proposal.plan.steps).toHaveLength(28);
506
- expect(proposal.plan.steps.every((step) => step.risk === "read")).toBe(true);
507
- expect(() =>
508
- parseproposal(
509
- {
510
- version: protocolversion,
511
- plan: { objective: "Watch without a lifetime", steps: [{ kind: "watchmutate", summary: "Watch the feed." }] },
512
- },
513
- "https://example.com",
514
- ),
515
- ).toThrow("watch lifetime");
516
- });
517
-
518
- it("wraps the observation envelope with its a11y, reader, listpattern and tableshape sections", () => {
519
- const plan = {
520
- id: "plan",
521
- objective: "o",
522
- origin: "https://example.com",
523
- steps: [],
524
- createdat: 1,
525
- expiresat: 2,
526
- state: "approved" as const,
527
- };
528
- const tree: a11ynode = { role: "button", name: "Submit", states: ["disabled"], childcount: 0, children: [] };
529
- const observationcapture: observation = {
530
- schemaversion: 3,
531
- url: "https://example.com",
532
- title: "Example",
533
- textpreview: "text",
534
- textlength: 4,
535
- forms: [],
536
- interactive: [],
537
- capturedat: 9,
538
- mode: "passive",
539
- a11y: tree,
540
- reader: {
541
- title: "Example",
542
- byline: "",
543
- blocks: [{ kind: "p", text: "text", words: 1 }],
544
- words: 1,
545
- characters: 4,
546
- },
547
- listpattern: [{ container: "ul", itemselector: "li.item", repeat: 3, samples: ["a", "b", "c"] }],
548
- tableshape: [
549
- { selector: "table", headers: ["Name"], columns: [{ label: "Name", cells: 3 }], rows: 3, caption: "" },
550
- ],
551
- };
552
- const envelope = JSON.parse(observationresponse({ observation: observationcapture, plan }));
553
- expect(envelope.version).toBe(protocolversion);
554
- expect(envelope.planstate).toBe("approved");
555
- expect(envelope.observation.a11y).toEqual(tree);
556
- expect(envelope.observation.reader.blocks).toHaveLength(1);
557
- expect(envelope.observation.listpattern[0]).toMatchObject({ itemselector: "li.item", repeat: 3 });
558
- expect(envelope.observation.tableshape[0]).toMatchObject({ headers: ["Name"], rows: 3 });
559
- expect(envelope.observation.mode).toBe("passive");
560
- });
561
-
562
- it("wraps mutation, focus and banner event records in the versioned envelope", () => {
563
- const plan = {
564
- id: "plan",
565
- objective: "o",
566
- origin: "https://example.com",
567
- steps: [],
568
- createdat: 1,
569
- expiresat: 2,
570
- state: "approved" as const,
571
- };
572
- const mutation: mutationevent = {
573
- watchid: "w1",
574
- event: "childList",
575
- targetpath: "#feed > li",
576
- sessionid: "session",
577
- at: 100,
578
- };
579
- const focus: focusevent = { watchid: "w2", kind: "focus", targetpath: "#search", sessionid: "session", at: 150 };
580
- const banner: bannerreport = {
581
- kind: "cookie",
582
- selector: "#cookiebar",
583
- text: "We use cookies.",
584
- controls: ["Accept"],
585
- sessionid: "session",
586
- at: 200,
587
- };
588
- const envelope = JSON.parse(eventresponse({ events: [mutation, focus, banner], plan }));
589
- expect(envelope.version).toBe(protocolversion);
590
- expect(envelope.events).toHaveLength(3);
591
- expect(envelope.events[0]).toMatchObject({ watchid: "w1", event: "childList", targetpath: "#feed > li", at: 100 });
592
- expect(envelope.events[1]).toMatchObject({ kind: "focus", targetpath: "#search", at: 150 });
593
- expect(envelope.events[2]).toMatchObject({ kind: "cookie", controls: ["Accept"], at: 200 });
594
- });
595
-
596
- it("wraps snapshot diffs with their version pair and added, removed and changed nodes", () => {
597
- const plan = {
598
- id: "plan",
599
- objective: "o",
600
- origin: "https://example.com",
601
- steps: [],
602
- createdat: 1,
603
- expiresat: 2,
604
- state: "approved" as const,
605
- };
606
- const diff: snapshotdiff = {
607
- baseversion: 2,
608
- targetversion: 5,
609
- added: [{ kind: "added", selector: "#new", summary: "New" }],
610
- removed: [{ kind: "removed", selector: "#old", summary: "Old" }],
611
- changed: [{ kind: "changed", selector: "#go", summary: "Go became Stop" }],
612
- at: 42,
613
- };
614
- const envelope = JSON.parse(diffresponse({ diff, plan }));
615
- expect(envelope.version).toBe(protocolversion);
616
- expect(envelope.diff.baseversion).toBe(2);
617
- expect(envelope.diff.targetversion).toBe(5);
618
- expect(envelope.diff.added).toEqual([{ kind: "added", selector: "#new", summary: "New" }]);
619
- expect(envelope.diff.removed).toHaveLength(1);
620
- expect(envelope.diff.changed).toHaveLength(1);
621
- });
622
-
623
- it("reports page language, template, scroll lock and banner state in the context payload", () => {
624
- expect(
625
- signalsreport({
626
- signals: { language: "pt-BR", template: "article", scrolllocked: true, banner: "cookie", refreshedat: 7 },
627
- }),
628
- ).toMatchObject({
629
- version: protocolversion,
630
- language: "pt-BR",
631
- template: "article",
632
- scrolllocked: true,
633
- banner: "cookie",
634
- });
635
- expect(signalsreport({ signals: { language: "en", refreshedat: 8 } })).toEqual({
636
- version: protocolversion,
637
- language: "en",
638
- });
639
- expect(signalsreport({})).toEqual({ version: protocolversion });
640
- expect(signalsreport({ signals: { refreshedat: 9 } })).toEqual({ version: protocolversion });
641
- });
642
-
643
- it("wraps derived selector candidates with their stability scores", () => {
644
- const plan = {
645
- id: "plan",
646
- objective: "o",
647
- origin: "https://example.com",
648
- steps: [],
649
- createdat: 1,
650
- expiresat: 2,
651
- state: "approved" as const,
652
- };
653
- const candidates: selectorcandidate[] = [
654
- { selector: "#checkout", strategy: "id", score: 100 },
655
- { selector: 'button[data-testid="checkout"]', strategy: "attribute", score: 80 },
656
- ];
657
- const envelope = JSON.parse(selectorresponse({ candidates, plan }));
658
- expect(envelope.version).toBe(protocolversion);
659
- expect(envelope.candidates).toEqual(candidates);
660
- expect(envelope.candidates[0]).toMatchObject({ selector: "#checkout", strategy: "id", score: 100 });
661
- });
662
-
663
- it("accepts the navigation mastery vocabulary with its reviewed options grammar", () => {
664
- const proposal = parseproposal(
665
- {
666
- version: protocolversion,
667
- plan: {
668
- objective: "Move anywhere with review",
669
- steps: [
670
- {
671
- kind: "openlink",
672
- summary: "Open the reviewed url in a new tab.",
673
- options: '{"navtarget":{"url":"https://example.com/report","container":"tab","position":"adjacent"}}',
674
- },
675
- {
676
- kind: "openprivate",
677
- summary: "Open the reviewed url in a private window.",
678
- options: '{"navtarget":{"url":"https://example.com/report","container":"private"}}',
679
- },
680
- { kind: "reloadcache", summary: "Reload bypassing the cache." },
681
- { kind: "stopnav", summary: "Stop the pending navigation." },
682
- { kind: "waitload", summary: "Wait for the load event.", options: '{"timeout":3000}' },
683
- {
684
- kind: "waiturl",
685
- summary: "Wait for the reviewed pattern.",
686
- options:
687
- '{"urlpattern":{"mode":"prefix","url":"https://example.com/results","query":{"page":"*"}},"timeout":4000}',
688
- },
689
- { kind: "followlink", value: "Read the docs", summary: "Follow the reviewed link." },
690
- {
691
- kind: "spanav",
692
- value: "Open settings",
693
- summary: "Route the single page app.",
694
- options: '{"routepattern":{"mode":"prefix","url":"https://example.com/settings"},"timeout":2500}',
695
- },
696
- { kind: "spawait", summary: "Wait for the spa url change.", options: '{"timeout":2000,"poll":50}' },
697
- {
698
- kind: "rewritequery",
699
- summary: "Rewrite the query parameters.",
700
- options: '{"set":{"page":"3"},"remove":["session"]}',
701
- },
702
- { kind: "setfragment", value: "pricing", summary: "Set the reviewed fragment." },
703
- {
704
- kind: "navlist",
705
- summary: "Navigate the reviewed urls.",
706
- options: '{"urls":["https://example.com/a","https://example.com/b"]}',
707
- },
708
- {
709
- kind: "navprofile",
710
- summary: "Apply the per site wait profile.",
711
- options:
712
- '{"waitprofile":{"signals":["load","networkidle"],"idle":500,"overrides":[{"origin":"https://example.com","idle":1500}]}}',
713
- },
714
- { kind: "detecthttp", summary: "Detect http errors and offline states." },
715
- { kind: "readredirects", summary: "Read the redirect chain." },
716
- { kind: "readfinalurl", summary: "Read the final url." },
717
- { kind: "handleauth", value: "https://example.com", summary: "Answer the basic auth prompt." },
718
- { kind: "printpdf", summary: "Print the page to pdf.", options: '{"name":"report.pdf"}' },
719
- {
720
- kind: "prefetch",
721
- summary: "Prefetch the predicted pages.",
722
- options: '{"urls":["https://example.com/next"]}',
723
- },
724
- {
725
- kind: "preconnect",
726
- summary: "Preconnect to the expected origins.",
727
- options: '{"origins":["https://cdn.example"]}',
728
- },
729
- {
730
- kind: "deeplink",
731
- summary: "Build the deep link.",
732
- options:
733
- '{"app":"github","params":{"owner":"wenathlan","repo":"extension"},"navtarget":{"url":"https://example.com","container":"tab"}}',
734
- },
735
- { kind: "reopentab", value: "https://example.com/closed", summary: "Reopen the closed tab." },
736
- { kind: "trailaudit", summary: "Restore the navigation trail." },
737
- {
738
- kind: "pausenav",
739
- summary: "Pause navigation for the consent prompt.",
740
- options: '{"reason":"a consent banner is open"}',
741
- },
742
- { kind: "navintent", value: "open the pricing page", summary: "Record the navigation intent." },
743
- {
744
- kind: "navrate",
745
- summary: "Apply the per domain rate limit.",
746
- options: '{"ratelimit":{"domain":"example.com","window":600000,"ceiling":3}}',
747
- },
748
- { kind: "openclipboard", value: "clipboard url", summary: "Open the clipboard url on consent." },
749
- { kind: "checksafe", value: "https://example.com/report", summary: "Verify the url safety." },
750
- {
751
- kind: "batchopen",
752
- summary: "Open the curated list.",
753
- options: '{"urls":["https://example.com/a","https://example.com/b"]}',
754
- },
755
- ],
756
- },
757
- },
758
- "https://example.com",
759
- );
760
- expect(proposal.plan.steps).toHaveLength(29);
761
- expect(proposal.plan.steps.filter((step) => step.risk === "sensitive")).toHaveLength(20);
762
- expect(proposal.plan.steps.filter((step) => step.risk === "read")).toHaveLength(9);
763
- expect(() =>
764
- parseproposal(
765
- {
766
- version: protocolversion,
767
- plan: {
768
- objective: "Open without a target",
769
- steps: [{ kind: "openlink", summary: "Open without a navtarget." }],
770
- },
771
- },
772
- "https://example.com",
773
- ),
774
- ).toThrow("navtarget");
775
- expect(() =>
776
- parseproposal(
777
- {
778
- version: protocolversion,
779
- plan: { objective: "Rate without a limit", steps: [{ kind: "navrate", summary: "Apply without a limit." }] },
780
- },
781
- "https://example.com",
782
- ),
783
- ).toThrow("ratelimit");
784
- });
785
-
786
- it("wraps the navstate envelope with its load phase, final url and redirect chain", () => {
787
- const plan = {
788
- id: "plan",
789
- objective: "o",
790
- origin: "https://example.com",
791
- steps: [],
792
- createdat: 1,
793
- expiresat: 2,
794
- state: "approved" as const,
795
- };
796
- const chain: redirectchain = {
797
- hops: [
798
- { url: "https://example.com/start", status: 302, at: 10 },
799
- { url: "https://example.com/mid", status: 302, at: 20 },
800
- { url: "https://example.com/final", status: 200, at: 35 },
801
- ],
802
- startedat: 10,
803
- endedat: 35,
804
- };
805
- const state: navstate = { phase: "complete", finalurl: "https://example.com/final", redirects: chain };
806
- const envelope = JSON.parse(navstateresponse({ navstate: state, plan }));
807
- expect(envelope.version).toBe(protocolversion);
808
- expect(envelope.planstate).toBe("approved");
809
- expect(envelope.navstate.phase).toBe("complete");
810
- expect(envelope.navstate.finalurl).toBe("https://example.com/final");
811
- expect(envelope.navstate.redirects.hops).toHaveLength(3);
812
- expect(envelope.navstate.redirects.hops[1]).toMatchObject({ url: "https://example.com/mid", status: 302, at: 20 });
813
- expect(envelope.navstate.redirects.endedat - envelope.navstate.redirects.startedat).toBe(25);
814
- });
815
-
816
- it("carries the navigation trail of a session in the context envelope", () => {
817
- const report = trailreport({
818
- sessionid: "session",
819
- trail: [
820
- { url: "https://example.com/", title: "Home", stepid: "s1", at: 10 },
821
- { url: "https://example.com/pricing", title: "Pricing", stepid: "s2", at: 20 },
822
- { url: "https://example.com/pricing#faq", title: "", at: 30 },
823
- ],
824
- });
825
- expect(report.version).toBe(protocolversion);
826
- expect(report.sessionid).toBe("session");
827
- expect(report.trail).toHaveLength(3);
828
- expect(report.trail[1]).toMatchObject({
829
- url: "https://example.com/pricing",
830
- title: "Pricing",
831
- stepid: "s2",
832
- at: 20,
833
- });
834
- expect(report.trail[2]?.stepid).toBeUndefined();
835
- expect(trailreport({ trail: [] })).toEqual({ version: protocolversion, trail: [] });
836
- });
837
-
838
- it("wraps url safety verdicts with their reasons in the response envelope", () => {
839
- const plan = {
840
- id: "plan",
841
- objective: "o",
842
- origin: "https://example.com",
843
- steps: [],
844
- createdat: 1,
845
- expiresat: 2,
846
- state: "approved" as const,
847
- };
848
- const verdicts: safetyverdict[] = [
849
- { url: "https://partner.example/report", safe: true, reasons: [], at: 5 },
850
- { url: "http://tracker.example/pixel", safe: false, reasons: ["the url must use HTTPS"], at: 6 },
851
- ];
852
- const envelope = JSON.parse(safetyresponse({ verdicts, plan }));
853
- expect(envelope.version).toBe(protocolversion);
854
- expect(envelope.verdicts).toEqual(verdicts);
855
- expect(envelope.verdicts[1]).toMatchObject({ safe: false, reasons: ["the url must use HTTPS"] });
856
- });
857
- });
858
-
859
- describe("protocol tabs and windows command", () => {
860
- const plan = {
861
- id: "plan",
862
- objective: "o",
863
- origin: "https://example.com",
864
- steps: [],
865
- createdat: 1,
866
- expiresat: 2,
867
- state: "approved" as const,
868
- };
869
-
870
- it("wraps tab reports with matches, groups and badges in the response envelope", () => {
871
- const report: tabreport = {
872
- matches: [
873
- {
874
- tabid: 4,
875
- url: "https://example.com/a",
876
- title: "Example A",
877
- index: 0,
878
- windowid: 10,
879
- active: true,
880
- pinned: false,
881
- audible: false,
882
- muted: true,
883
- discarded: false,
884
- meta: { tabid: 4, taskrefs: ["plan"], provenance: "plan step", labels: ["research"], at: 5 },
885
- },
886
- ],
887
- groups: [{ name: "research", color: "blue", tabids: [4, 5], collapsed: false }],
888
- badges: [{ tabid: 4, taskid: "plan", label: "2/5", setat: 6 }],
889
- };
890
- const envelope = JSON.parse(tabreportresponse({ report, plan }));
891
- expect(envelope.version).toBe(protocolversion);
892
- expect(envelope.planid).toBe("plan");
893
- expect(envelope.planstate).toBe("approved");
894
- expect(envelope.report.matches[0]).toMatchObject({ tabid: 4, muted: true, meta: { labels: ["research"] } });
895
- expect(envelope.report.groups[0]).toMatchObject({ name: "research", color: "blue", tabids: [4, 5] });
896
- expect(envelope.report.badges[0]).toMatchObject({ label: "2/5", taskid: "plan" });
897
- });
898
-
899
- it("carries layout snapshots with their window bounds and group states in the session context", () => {
900
- const layout: tablayout = {
901
- name: "work",
902
- tabs: [{ url: "https://example.com/a", title: "A", pinned: true, index: 0, windowid: 10 }],
903
- groups: [{ name: "research", color: "blue", tabids: [4], collapsed: true }],
904
- windows: [
905
- {
906
- windowid: 10,
907
- state: { bounds: { left: 0, top: 0, width: 1280, height: 800 }, maximized: false, profile: "normal" },
908
- },
909
- ],
910
- savedat: 42,
911
- };
912
- const report = layoutreport({ layouts: [layout] });
913
- expect(report.version).toBe(protocolversion);
914
- expect(report.layouts).toHaveLength(1);
915
- expect(report.layouts[0]?.windows[0]?.state.bounds).toEqual({ left: 0, top: 0, width: 1280, height: 800 });
916
- expect(report.layouts[0]?.groups[0]).toMatchObject({ collapsed: true });
917
- expect(layoutreport({ layouts: [] })).toEqual({ version: protocolversion, layouts: [] });
918
- });
919
-
920
- it("wraps form reports, error reports and wizard states in versioned envelopes", () => {
921
- const plan: agentplan = {
922
- id: "p1",
923
- objective: "Fill the checkout form",
924
- origin: "https://example.com",
925
- steps: [{ id: "s1", kind: "detectfields", summary: "Detect the fields.", risk: "read" }],
926
- createdat: 1,
927
- expiresat: 2,
928
- state: "approved",
929
- };
930
- const report: formreport = {
931
- form: "#checkout",
932
- fields: [
933
- { selector: "#name", label: "Full name", kind: "text", matched: true },
934
- { selector: "#card", label: "Card number", kind: "card", matched: false },
935
- ],
936
- };
937
- const formenvelope = JSON.parse(formreportresponse({ report, plan })) as {
938
- version: string;
939
- planid: string;
940
- planstate: string;
941
- report: formreport;
942
- };
943
- expect(formenvelope).toMatchObject({ version: protocolversion, planid: "p1", planstate: "approved" });
944
- expect(formenvelope.report.fields[1]).toMatchObject({ kind: "card", matched: false });
945
- const error: errorreport = {
946
- form: "#checkout",
947
- errors: [{ field: "#email", message: "Enter a valid email address." }],
948
- at: 5,
949
- };
950
- const errorenvelope = JSON.parse(errorreportresponse({ report: error, plan })) as {
951
- version: string;
952
- report: errorreport;
953
- };
954
- expect(errorenvelope.version).toBe(protocolversion);
955
- expect(errorenvelope.report.errors[0]).toEqual({ field: "#email", message: "Enter a valid email address." });
956
- const wizards: wizardstate[] = [{ index: 2, steps: 3, completed: [true, true, false], at: 6 }];
957
- const picks: typeaheadpick[] = [{ field: "#city", query: "sao", pick: "São Paulo", at: 7 }];
958
- expect(wizardreport({ sessionid: "session", wizards, picks })).toEqual({
959
- version: protocolversion,
960
- sessionid: "session",
961
- wizards,
962
- picks,
963
- });
964
- expect(wizardreport({ wizards: [], picks: [] })).toEqual({ version: protocolversion, wizards: [], picks: [] });
965
- });
966
-
967
- it("wraps dataset payloads with column specs and sampled rows in the dataset envelope", () => {
968
- const columns: columnspec[] = [{ key: "sku", label: "Sku", kind: "text", normalized: "sku" }];
969
- const rows = Array.from({ length: 12 }, (_, index) => ({ sku: `sku-${index}` }));
970
- const datasetvalue: dataset = {
971
- id: "d1",
972
- name: "catalog",
973
- columns,
974
- rows,
975
- sources: [{ row: 0, url: "https://example.com/list", at: 5, stepid: "s1" }],
976
- at: 6,
977
- };
978
- const payload = JSON.parse(
979
- datasetresponse({ dataset: datasetvalue, plan: { ...plan, state: "approved" }, sample: 3 }),
980
- ) as { version: string; planstate: string; dataset: dataset & { totalrows: number } };
981
- expect(payload).toMatchObject({ version: protocolversion, planstate: "approved" });
982
- expect(payload.dataset.columns).toEqual(columns);
983
- expect(payload.dataset.rows).toHaveLength(3);
984
- expect(payload.dataset.totalrows).toBe(12);
985
- expect(payload.dataset.sources[0]).toMatchObject({ row: 0, stepid: "s1" });
986
- });
987
-
988
- it("carries extraction progress and provenance records per exported artifact in the context envelopes", () => {
989
- const sessionvalue: extractsession = {
990
- id: "e1",
991
- datasetid: "d1",
992
- name: "catalog",
993
- target: "table",
994
- next: "a.next",
995
- planned: 3,
996
- pages: ["https://example.com/1"],
997
- rows: 10,
998
- cursor: 1,
999
- startedat: 1,
1000
- updatedat: 2,
1001
- };
1002
- expect(extractionreport({ sessions: [sessionvalue] })).toEqual({
1003
- version: protocolversion,
1004
- sessions: [sessionvalue],
1005
- });
1006
- const record: provenancerecord = {
1007
- artifact: "a1",
1008
- name: "catalog.csv",
1009
- url: "https://example.com/list",
1010
- stepid: "s2",
1011
- rowstart: 1,
1012
- rowend: 10,
1013
- checksum: "fnv1a-1",
1014
- at: 3,
1015
- };
1016
- expect(provenancereport({ records: [record] })).toEqual({ version: protocolversion, records: [record] });
1017
- const rules: transformrule[] = [{ expression: "trim", sources: ["name"], target: "name" }];
1018
- expect(JSON.parse(transformgrammar(rules))).toEqual({ rules });
1019
- });
1020
-
1021
- it("accepts the extraction vocabulary and grades exports sensitive inside a proposal", () => {
1022
- const proposal = parseproposal(
1023
- {
1024
- version: protocolversion,
1025
- plan: {
1026
- objective: "Scrape the catalog",
1027
- steps: [
1028
- {
1029
- kind: "scrapetable",
1030
- target: "table.prices",
1031
- options: '{"name":"catalog","rowlimit":200}',
1032
- summary: "Scrape the price table.",
1033
- },
1034
- {
1035
- kind: "transformvalues",
1036
- options: '{"dataset":"d1","rules":[{"expression":"upper","sources":["name"],"target":"name"}]}',
1037
- summary: "Uppercase the names.",
1038
- },
1039
- ],
1040
- },
1041
- },
1042
- "https://example.com",
1043
- );
1044
- expect(proposal.plan.steps[0]?.risk).toBe("read");
1045
- expect(() =>
1046
- parseproposal(
1047
- {
1048
- version: protocolversion,
1049
- plan: {
1050
- objective: "Bad transform",
1051
- steps: [
1052
- {
1053
- kind: "transformvalues",
1054
- options: '{"dataset":"d1","rules":{"expression":"upper"}}',
1055
- summary: "Rules must be a list.",
1056
- },
1057
- ],
1058
- },
1059
- },
1060
- "https://example.com",
1061
- ),
1062
- ).toThrow("transform rules");
1063
- });
1064
-
1065
- it("accepts the forms and data vocabulary and refuses submissions without an asksubmit review", () => {
1066
- const proposal = parseproposal(
1067
- {
1068
- version: protocolversion,
1069
- plan: {
1070
- objective: "Fill and submit the checkout",
1071
- steps: [
1072
- { kind: "detectfields", summary: "Detect the form fields." },
1073
- {
1074
- kind: "fillform",
1075
- options:
1076
- '{"formrecord":{"form":"#checkout","entries":[{"match":{"mode":"label","label":"Full name"},"kind":"text","value":"Ana Alves"}]}}',
1077
- summary: "Fill from the structured record.",
1078
- },
1079
- { kind: "asksubmit", value: "#checkout", summary: "Ask before submitting." },
1080
- {
1081
- kind: "submitform",
1082
- target: "#checkout",
1083
- options: '{"consentref":"ask"}',
1084
- summary: "Submit through the owning form.",
1085
- },
1086
- { kind: "readerrors", target: "#checkout", summary: "Read the validation errors." },
1087
- ],
1088
- },
1089
- },
1090
- "https://example.com",
1091
- );
1092
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual(["read", "sensitive", "read", "sensitive", "read"]);
1093
- expect(() =>
1094
- parseproposal(
1095
- {
1096
- version: protocolversion,
1097
- plan: {
1098
- objective: "Submit without review",
1099
- steps: [
1100
- {
1101
- kind: "submitform",
1102
- target: "#checkout",
1103
- options: '{"consentref":"missing"}',
1104
- summary: "Submit without asksubmit.",
1105
- },
1106
- ],
1107
- },
1108
- },
1109
- "https://example.com",
1110
- ),
1111
- ).toThrow("asksubmit");
1112
- });
1113
- });
1114
-
1115
- describe("files, clipboard and downloads protocol", () => {
1116
- const plan = {
1117
- id: "p1",
1118
- objective: "Move files",
1119
- origin: "https://example.com",
1120
- steps: [],
1121
- createdat: 1,
1122
- expiresat: 2,
1123
- state: "approved" as const,
1124
- };
1125
-
1126
- it("wraps download, netlog and quarantine payloads in the versioned envelopes", () => {
1127
- const downloads: downloadrecord[] = [
1128
- {
1129
- id: "d1",
1130
- url: "https://example.com/report.pdf",
1131
- filename: "report.pdf",
1132
- state: "complete",
1133
- downloadid: 9,
1134
- path: "/downloads/report.pdf",
1135
- bytes: 2048,
1136
- checksum: "fnv1a-abc",
1137
- at: 4,
1138
- updatedat: 5,
1139
- },
1140
- { id: "d2", url: "https://example.com/data.zip", filename: "data.zip", state: "paused", at: 6, updatedat: 7 },
1141
- ];
1142
- const envelope = JSON.parse(downloadreport({ downloads, plan: { ...plan, state: "approved" } })) as {
1143
- version: string;
1144
- planid: string;
1145
- planstate: string;
1146
- downloads: downloadrecord[];
1147
- };
1148
- expect(envelope).toMatchObject({ version: protocolversion, planid: "p1", planstate: "approved" });
1149
- expect(envelope.downloads[0]).toMatchObject({
1150
- state: "complete",
1151
- path: "/downloads/report.pdf",
1152
- checksum: "fnv1a-abc",
1153
- bytes: 2048,
1154
- });
1155
- expect(envelope.downloads[1]).toMatchObject({ state: "paused" });
1156
- const records: netlogrecord[] = [
1157
- {
1158
- url: "https://example.com/list",
1159
- method: "GET",
1160
- status: 200,
1161
- timing: 120,
1162
- requestid: "nav-1-1",
1163
- stepid: "nav-1",
1164
- at: 8,
1165
- },
1166
- ];
1167
- expect(netlogreport({ records })).toEqual({ version: protocolversion, records });
1168
- const entries: quarantineentry[] = [
1169
- {
1170
- id: "q1",
1171
- path: "devthink-quarantine/invoice.pdf",
1172
- reason: "mime application/pdf matched",
1173
- scan: "clean",
1174
- release: "user-9",
1175
- at: 9,
1176
- updatedat: 10,
1177
- },
1178
- ];
1179
- expect(quarantinereport({ entries })).toEqual({ version: protocolversion, entries });
1180
- });
1181
-
1182
- it("accepts the files vocabulary and grades the sensitive batch and clipboard kinds", () => {
1183
- const proposal = parseproposal(
1184
- {
1185
- version: protocolversion,
1186
- plan: {
1187
- objective: "Move the files safely",
1188
- steps: [
1189
- {
1190
- kind: "batchdownload",
1191
- options:
1192
- '{"downloadspec":{"urls":["https://example.com/report.pdf","https://example.com/data.zip"],"filename":"report","complete":"size"},"concurrent":3}',
1193
- summary: "Download the reviewed batch.",
1194
- },
1195
- { kind: "pausedownload", value: "report.pdf", summary: "Pause the first download." },
1196
- { kind: "verifydownload", value: "report.pdf", options: '{"bytes":2048}', summary: "Verify the size." },
1197
- {
1198
- kind: "interceptmime",
1199
- options: '{"mimefilter":{"include":["application/pdf*"],"exclude":[],"default":"deny"}}',
1200
- summary: "Intercept pdf downloads.",
1201
- },
1202
- {
1203
- kind: "readclipboard",
1204
- options: '{"consentref":"clip-1","prompt":"Read the tracking number."}',
1205
- summary: "Read the clipboard once.",
1206
- },
1207
- { kind: "writeclipboard", value: "Reviewed summary.", summary: "Write the reviewed text." },
1208
- { kind: "copyscreen", summary: "Copy the visible tab." },
1209
- { kind: "exportnetlog", summary: "Export the network log." },
1210
- { kind: "namecaptures", options: '{"task":"task-1","extension":"png"}', summary: "Stamp capture names." },
1211
- {
1212
- kind: "cleanupartifacts",
1213
- options: '{"rules":[{"age":60000,"kind":"any","keep":"latest"}]}',
1214
- summary: "Sweep old artifacts.",
1215
- },
1216
- ],
1217
- },
1218
- },
1219
- "https://example.com",
1220
- );
1221
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual([
1222
- "sensitive",
1223
- "sensitive",
1224
- "read",
1225
- "sensitive",
1226
- "sensitive",
1227
- "sensitive",
1228
- "sensitive",
1229
- "read",
1230
- "read",
1231
- "sensitive",
1232
- ]);
1233
- expect(() =>
1234
- parseproposal(
1235
- {
1236
- version: protocolversion,
1237
- plan: {
1238
- objective: "Read without consent",
1239
- steps: [{ kind: "readclipboard", summary: "Read the clipboard without a consent ref." }],
1240
- },
1241
- },
1242
- "https://example.com",
1243
- ),
1244
- ).toThrow("consent ref");
1245
- expect(() =>
1246
- parseproposal(
1247
- {
1248
- version: protocolversion,
1249
- plan: {
1250
- objective: "Bad filter",
1251
- steps: [
1252
- {
1253
- kind: "interceptmime",
1254
- options: '{"mimefilter":{"include":[],"exclude":[],"default":"deny"}}',
1255
- summary: "Empty include list.",
1256
- },
1257
- ],
1258
- },
1259
- },
1260
- "https://example.com",
1261
- ),
1262
- ).toThrow("include");
1263
- });
1264
- });
1265
-
1266
- describe("media capture protocol", () => {
1267
- const plan = {
1268
- id: "p1",
1269
- objective: "See the page",
1270
- origin: "https://example.com",
1271
- steps: [],
1272
- createdat: 1,
1273
- expiresat: 2,
1274
- state: "approved" as const,
1275
- };
1276
-
1277
- it("wraps the capture block into the outcome envelope and carries the capture report", () => {
1278
- const outcome: stepoutcome = {
1279
- stepid: "shot-1",
1280
- ok: true,
1281
- summary: "Captured the visible viewport.",
1282
- details: { capture: { id: "cap-1", kind: "shotview", format: "png", width: 1280, height: 800, bytes: 5200 } },
1283
- at: 3,
1284
- };
1285
- const envelope = JSON.parse(
1286
- outcomeresponse({ outcome, plan, capture: { id: "cap-1", format: "png", bytes: 5200 } }),
1287
- ) as { version: string; capture: { id: string; format: string; bytes: number } };
1288
- expect(envelope).toMatchObject({ version: protocolversion, planid: "p1", planstate: "approved" });
1289
- expect(envelope.capture).toEqual({ id: "cap-1", format: "png", bytes: 5200 });
1290
- const plain = JSON.parse(outcomeresponse({ outcome, plan })) as { capture?: unknown };
1291
- expect(plain.capture).toBeUndefined();
1292
- const records: shotrecord[] = [
1293
- {
1294
- id: "cap-1",
1295
- runid: "p1",
1296
- stepid: "shot-1",
1297
- kind: "shotview",
1298
- format: "png",
1299
- width: 1280,
1300
- height: 800,
1301
- capturedat: 3,
1302
- bytes: "data:image/png;base64,xyz",
1303
- },
1304
- {
1305
- id: "cap-2",
1306
- runid: "p1",
1307
- stepid: "shot-2",
1308
- kind: "contactsheet",
1309
- format: "webp",
1310
- width: 960,
1311
- height: 536,
1312
- capturedat: 4,
1313
- annotated: true,
1314
- },
1315
- ];
1316
- const pairs: shotpair[] = [
1317
- {
1318
- id: "pair-1",
1319
- beforeid: "cap-1",
1320
- afterid: "cap-2",
1321
- actionkind: "click",
1322
- target: "#submit",
1323
- domsnapshotid: "7",
1324
- at: 5,
1325
- },
1326
- ];
1327
- expect(capturereport({ records, pairs })).toEqual({ version: protocolversion, records, pairs });
1328
- });
1329
-
1330
- it("accepts the capture vocabulary, refuses unknown formats and negative region coordinates", () => {
1331
- const proposal = parseproposal(
1332
- {
1333
- version: protocolversion,
1334
- plan: {
1335
- objective: "See the page with review",
1336
- steps: [
1337
- {
1338
- kind: "shotview",
1339
- options: '{"capture":{"format":"png","pixelratio":2}}',
1340
- summary: "Shoot the viewport.",
1341
- },
1342
- {
1343
- kind: "shotfullpage",
1344
- options: '{"settle":150,"overlap":40,"wait":60000}',
1345
- summary: "Shoot the full page.",
1346
- },
1347
- {
1348
- kind: "shotelement",
1349
- target: "#card",
1350
- options: '{"capture":{"format":"webp","exporttarget":"clipboard"}}',
1351
- summary: "Shoot the card.",
1352
- },
1353
- {
1354
- kind: "shotregion",
1355
- options: '{"regionrect":{"x":10,"y":20,"width":400,"height":300},"reviewed":true}',
1356
- summary: "Shoot the reviewed region.",
1357
- },
1358
- {
1359
- kind: "contactsheet",
1360
- options: '{"elements":["#a","#b"],"sheet":{"cellsize":240,"columns":3,"label":"both"}}',
1361
- summary: "Tile the cards.",
1362
- },
1363
- ],
1364
- },
1365
- },
1366
- "https://example.com",
1367
- );
1368
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual(["read", "read", "read", "read", "read"]);
1369
- expect(proposal.plan.steps[2]?.target).toBe("#card");
1370
- expect(() =>
1371
- parseproposal(
1372
- {
1373
- version: protocolversion,
1374
- plan: {
1375
- objective: "Bad format",
1376
- steps: [{ kind: "shotview", options: '{"capture":{"format":"gif"}}', summary: "Shoot with a bad format." }],
1377
- },
1378
- },
1379
- "https://example.com",
1380
- ),
1381
- ).toThrow("png, jpeg or webp");
1382
- expect(() =>
1383
- parseproposal(
1384
- {
1385
- version: protocolversion,
1386
- plan: {
1387
- objective: "Negative region",
1388
- steps: [
1389
- {
1390
- kind: "shotregion",
1391
- options: '{"regionrect":{"x":-1,"y":0,"width":40,"height":30},"reviewed":true}',
1392
- summary: "Shoot a negative region.",
1393
- },
1394
- ],
1395
- },
1396
- },
1397
- "https://example.com",
1398
- ),
1399
- ).toThrow("negative coordinates");
1400
- expect(() =>
1401
- parseproposal(
1402
- {
1403
- version: protocolversion,
1404
- plan: {
1405
- objective: "Unreviewed region",
1406
- steps: [
1407
- {
1408
- kind: "shotregion",
1409
- options: '{"regionrect":{"x":0,"y":0,"width":40,"height":30}}',
1410
- summary: "Shoot an unreviewed region.",
1411
- },
1412
- ],
1413
- },
1414
- },
1415
- "https://example.com",
1416
- ),
1417
- ).toThrow("reviewed flag");
1418
- });
1419
- });
1420
-
1421
- describe("media capture part two protocol", () => {
1422
- const plan = {
1423
- id: "p1",
1424
- objective: "See and hear the page",
1425
- origin: "https://example.com",
1426
- steps: [],
1427
- createdat: 1,
1428
- expiresat: 2,
1429
- state: "approved" as const,
1430
- };
1431
-
1432
- it("wraps the media block into the outcome envelope and carries the media report", () => {
1433
- const outcome: stepoutcome = {
1434
- stepid: "pdf-1",
1435
- ok: true,
1436
- summary: "Composed a derived pdf report.",
1437
- details: { media: { id: "pdf-1", kind: "pdf", bytes: 4200 } },
1438
- at: 3,
1439
- };
1440
- const envelope = JSON.parse(
1441
- outcomeresponse({ outcome, plan, media: { id: "pdf-1", kind: "pdf", bytes: 4200 } }),
1442
- ) as { version: string; media: { id: string; kind: string; bytes: number } };
1443
- expect(envelope).toMatchObject({ version: protocolversion, planid: "p1", planstate: "approved" });
1444
- expect(envelope.media).toEqual({ id: "pdf-1", kind: "pdf", bytes: 4200 });
1445
- const plain = JSON.parse(outcomeresponse({ outcome, plan })) as { media?: unknown };
1446
- expect(plain.media).toBeUndefined();
1447
- const records: mediarecord[] = [
1448
- {
1449
- id: "pdf-1",
1450
- runid: "p1",
1451
- stepid: "pdf-1",
1452
- pages: 3,
1453
- pagewidth: 612,
1454
- pageheight: 792,
1455
- margins: { top: 28.8, right: 28.8, bottom: 28.8, left: 28.8 },
1456
- landscape: false,
1457
- scale: 1,
1458
- bytes: 4200,
1459
- at: 3,
1460
- },
1461
- {
1462
- id: "rec-1",
1463
- runid: "p1",
1464
- stepid: "rec-1",
1465
- tabid: 4,
1466
- kind: "screen",
1467
- scope: "run",
1468
- format: "frames",
1469
- startedat: 4,
1470
- endedat: 9,
1471
- duration: 5,
1472
- fps: 2,
1473
- frames: ["f1", "f2"],
1474
- at: 4,
1475
- },
1476
- ];
1477
- const images: imagebatch[] = [
1478
- {
1479
- id: "batch-1",
1480
- runid: "p1",
1481
- stepid: "img-1",
1482
- images: [
1483
- { url: "https://example.com/a.png", alt: "Chart", width: 400, height: 300, bytes: 1200, mime: "image/png" },
1484
- ],
1485
- matched: 1,
1486
- downloaded: 1,
1487
- at: 5,
1488
- },
1489
- ];
1490
- expect(mediareport({ records, images })).toEqual({ version: protocolversion, records, images });
1491
- });
1492
-
1493
- it("accepts the media vocabulary and rejects negative margins, bad scopes and negative intervals", () => {
1494
- const proposal = parseproposal(
1495
- {
1496
- version: protocolversion,
1497
- plan: {
1498
- objective: "Capture media with review",
1499
- steps: [
1500
- {
1501
- kind: "capturepdf",
1502
- options:
1503
- '{"pdf":{"paperwidth":8.5,"paperheight":11,"margins":{"top":0.4,"right":0.4,"bottom":0.4,"left":0.4},"scale":1,"paginate":true},"breakpoints":["#section-2"],"exporttarget":"memory"}',
1504
- summary: "Print the report.",
1505
- },
1506
- {
1507
- kind: "recordscreen",
1508
- options: '{"recording":{"scope":"run","fps":2},"duration":1000,"consentref":"c1"}',
1509
- summary: "Record the run.",
1510
- },
1511
- {
1512
- kind: "captureaudio",
1513
- options: '{"recording":{"audio":true},"duration":1000,"consentref":"c2"}',
1514
- summary: "Record the audio.",
1515
- },
1516
- {
1517
- kind: "captureframe",
1518
- target: "video.tutorial",
1519
- options: '{"timestamp":5,"poster":true}',
1520
- summary: "Grab the frame.",
1521
- },
1522
- {
1523
- kind: "downloadimages",
1524
- options: '{"imagefilter":{"selector":"main img","minwidth":200}}',
1525
- summary: "Download the images.",
1526
- },
1527
- {
1528
- kind: "shotcanvas",
1529
- target: "canvas.chart",
1530
- options: '{"capture":{"format":"webp"}}',
1531
- summary: "Shoot the canvas.",
1532
- },
1533
- { kind: "probestream", summary: "Probe the streams." },
1534
- { kind: "readmedia", summary: "Read the sources." },
1535
- { kind: "readassets", summary: "Read the assets." },
1536
- {
1537
- kind: "timelapse",
1538
- options: '{"lapse":{"interval":500,"duration":5000},"wait":6000}',
1539
- summary: "Shoot the lapse.",
1540
- },
1541
- {
1542
- kind: "convertimage",
1543
- options: '{"capture":"cap-1","convert":{"target":"webp","quality":85}}',
1544
- summary: "Convert the capture.",
1545
- },
1546
- {
1547
- kind: "makethumbs",
1548
- options: '{"captures":["cap-1"],"thumb":{"size":240,"fit":"cover","suffix":"thumb"}}',
1549
- summary: "Thumb the captures.",
1550
- },
1551
- ],
1552
- },
1553
- },
1554
- "https://example.com",
1555
- );
1556
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual([
1557
- "read",
1558
- "sensitive",
1559
- "sensitive",
1560
- "read",
1561
- "sensitive",
1562
- "read",
1563
- "read",
1564
- "read",
1565
- "read",
1566
- "read",
1567
- "read",
1568
- "read",
1569
- ]);
1570
- expect(proposal.plan.steps[3]?.target).toBe("video.tutorial");
1571
- expect(() =>
1572
- parseproposal(
1573
- {
1574
- version: protocolversion,
1575
- plan: {
1576
- objective: "Negative margins",
1577
- steps: [
1578
- {
1579
- kind: "capturepdf",
1580
- options: '{"pdf":{"margins":{"left":-0.2}}}',
1581
- summary: "Print with negative margins.",
1582
- },
1583
- ],
1584
- },
1585
- },
1586
- "https://example.com",
1587
- ),
1588
- ).toThrow("negative margins are refused");
1589
- expect(() =>
1590
- parseproposal(
1591
- {
1592
- version: protocolversion,
1593
- plan: {
1594
- objective: "Bad scope",
1595
- steps: [
1596
- {
1597
- kind: "recordscreen",
1598
- options: '{"recording":{"scope":"window"},"consentref":"c1"}',
1599
- summary: "Record with a bad scope.",
1600
- },
1601
- ],
1602
- },
1603
- },
1604
- "https://example.com",
1605
- ),
1606
- ).toThrow("scope must be tab or run");
1607
- expect(() =>
1608
- parseproposal(
1609
- {
1610
- version: protocolversion,
1611
- plan: {
1612
- objective: "Negative interval",
1613
- steps: [
1614
- {
1615
- kind: "timelapse",
1616
- options: '{"lapse":{"interval":-500,"duration":5000}}',
1617
- summary: "Shoot a negative interval.",
1618
- },
1619
- ],
1620
- },
1621
- },
1622
- "https://example.com",
1623
- ),
1624
- ).toThrow("interval");
1625
- expect(() =>
1626
- parseproposal(
1627
- {
1628
- version: protocolversion,
1629
- plan: {
1630
- objective: "Recording without consent",
1631
- steps: [
1632
- { kind: "captureaudio", options: '{"recording":{"audio":true}}', summary: "Record without consent." },
1633
- ],
1634
- },
1635
- },
1636
- "https://example.com",
1637
- ),
1638
- ).toThrow("consent ref");
1639
- expect(() =>
1640
- parseproposal(
1641
- {
1642
- version: protocolversion,
1643
- plan: {
1644
- objective: "Lapse over budget",
1645
- steps: [
1646
- {
1647
- kind: "timelapse",
1648
- options: '{"lapse":{"interval":500,"duration":9000},"wait":5000}',
1649
- summary: "Shoot past the budget.",
1650
- },
1651
- ],
1652
- },
1653
- },
1654
- "https://example.com",
1655
- ),
1656
- ).toThrow("exceeds the reviewed wait budget");
1657
- });
1658
- });
1659
-
1660
- describe("network observation part one protocol", () => {
1661
- const plan = {
1662
- id: "p1",
1663
- objective: "Speak to the network under review",
1664
- origin: "https://example.com",
1665
- steps: [],
1666
- createdat: 1,
1667
- expiresat: 2,
1668
- state: "approved" as const,
1669
- };
1670
-
1671
- it("wraps the transport block into the outcome envelope and carries the calls report without bodies", () => {
1672
- const outcome: stepoutcome = {
1673
- stepid: "f1",
1674
- ok: true,
1675
- summary: "Fetched the reviewed url.",
1676
- details: { transport: { status: 200, headers: ["content-type"], bytes: 512, duration: 340 }, retries: 1 },
1677
- at: 3,
1678
- };
1679
- const envelope = JSON.parse(
1680
- outcomeresponse({
1681
- outcome,
1682
- plan,
1683
- transport: { status: 200, headers: ["content-type"], bytes: 512, duration: 340 },
1684
- }),
1685
- ) as { version: string; transport: { status: number; headers: string[]; bytes: number; duration: number } };
1686
- expect(envelope).toMatchObject({ version: protocolversion, planid: "p1", planstate: "approved" });
1687
- expect(envelope.transport).toEqual({ status: 200, headers: ["content-type"], bytes: 512, duration: 340 });
1688
- const plain = JSON.parse(outcomeresponse({ outcome, plan })) as { transport?: unknown };
1689
- expect(plain.transport).toBeUndefined();
1690
- const calls: callrecord[] = [
1691
- {
1692
- id: "c1",
1693
- runid: "p1",
1694
- stepid: "f1",
1695
- kind: "fetch",
1696
- url: "https://api.example/data",
1697
- origin: "https://api.example",
1698
- method: "GET",
1699
- status: 200,
1700
- statusclass: "success",
1701
- duration: 120,
1702
- retries: 1,
1703
- bytes: 512,
1704
- headernames: ["x-request-id"],
1705
- body: '{"secret":"no"}',
1706
- at: 4,
1707
- },
1708
- {
1709
- id: "c2",
1710
- runid: "p1",
1711
- stepid: "g1",
1712
- kind: "graphql",
1713
- url: "https://api.example/graphql",
1714
- origin: "https://api.example",
1715
- method: "POST",
1716
- status: 200,
1717
- statusclass: "success",
1718
- duration: 80,
1719
- retries: 0,
1720
- bytes: 64,
1721
- headernames: [],
1722
- errors: ["auth required"],
1723
- at: 5,
1724
- },
1725
- ];
1726
- const report = callsreport({ calls });
1727
- expect(report.version).toBe(protocolversion);
1728
- expect(report.calls).toHaveLength(2);
1729
- expect(report.calls[0]).toMatchObject({ id: "c1", statusclass: "success", retries: 1 });
1730
- expect(report.calls[0]?.streambytes).toBeUndefined();
1731
- expect(report.calls[0]?.body).toBeUndefined();
1732
- expect(report.calls[1]).toMatchObject({ kind: "graphql", errors: ["auth required"] });
1733
- });
1734
-
1735
- it("rejects fetch requests to ungranted origins and header allowlists with empty names", () => {
1736
- const proposal = parseproposal(
1737
- {
1738
- version: protocolversion,
1739
- plan: {
1740
- objective: "Fetch under review",
1741
- steps: [
1742
- {
1743
- kind: "fetchurl",
1744
- options: '{"fetch":{"url":"https://example.com/data"}}',
1745
- summary: "Fetch the granted origin.",
1746
- },
1747
- { kind: "callrest", options: '{"endpoint":"issues"}', summary: "Call the typed endpoint." },
1748
- ],
1749
- },
1750
- },
1751
- "https://example.com",
1752
- );
1753
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual(["read", "sensitive"]);
1754
- expect(() =>
1755
- parseproposal(
1756
- {
1757
- version: protocolversion,
1758
- plan: {
1759
- objective: "Fetch outside",
1760
- steps: [
1761
- {
1762
- kind: "fetchurl",
1763
- options: '{"fetch":{"url":"https://api.example/data"}}',
1764
- summary: "Fetch an ungranted origin.",
1765
- },
1766
- ],
1767
- },
1768
- },
1769
- "https://example.com",
1770
- ),
1771
- ).toThrow("outside the grants");
1772
- const granted = parseproposal(
1773
- {
1774
- version: protocolversion,
1775
- plan: {
1776
- objective: "Fetch the granted api",
1777
- steps: [
1778
- {
1779
- kind: "fetchurl",
1780
- options: '{"fetch":{"url":"https://api.example/data"}}',
1781
- summary: "Fetch the granted api origin.",
1782
- },
1783
- ],
1784
- },
1785
- },
1786
- "https://example.com",
1787
- ["https://example.com", "https://api.example"],
1788
- );
1789
- expect(granted.plan.steps).toHaveLength(1);
1790
- expect(() =>
1791
- parseproposal(
1792
- {
1793
- version: protocolversion,
1794
- plan: {
1795
- objective: "Empty header name",
1796
- steps: [
1797
- {
1798
- kind: "fetchurl",
1799
- options: '{"fetch":{"url":"https://example.com/data","headers":{"":"v"}}}',
1800
- summary: "Fetch with an empty header name.",
1801
- },
1802
- ],
1803
- },
1804
- },
1805
- "https://example.com",
1806
- ),
1807
- ).toThrow("empty names");
1808
- });
1809
-
1810
- it("accepts the http options grammar with stream budgets, path rules, queries and graphql operations", () => {
1811
- const proposal = parseproposal(
1812
- {
1813
- version: protocolversion,
1814
- plan: {
1815
- objective: "Observe the network",
1816
- steps: [
1817
- {
1818
- kind: "fetchurl",
1819
- options:
1820
- '{"fetch":{"url":"https://example.com/data","method":"POST","headers":{"x-request-id":"42"},"body":"{}","mode":"cors"},"fetchoptions":{"timeout":1000,"retries":2,"backoff":100,"follow":3},"stream":{"budget":4096},"wait":5000,"consentref":"c1"}',
1821
- summary: "Fetch a reviewed url.",
1822
- },
1823
- {
1824
- kind: "parsejson",
1825
- options: '{"call":"c1","fields":[{"name":"title","path":"data.title","kind":"text"}]}',
1826
- summary: "Parse the fetched body.",
1827
- },
1828
- {
1829
- kind: "parsehtml",
1830
- options: '{"call":"c1","queries":[{"selector":"a.link","attribute":"href","multi":true}]}',
1831
- summary: "Query the fetched markup.",
1832
- },
1833
- {
1834
- kind: "callgraphql",
1835
- options: '{"endpoint":"graph","graphql":{"query":"query{hero}","operationkind":"query"}}',
1836
- summary: "Run the reviewed query.",
1837
- },
1838
- ],
1839
- },
1840
- },
1841
- "https://example.com",
1842
- );
1843
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual(["read", "read", "read", "sensitive"]);
1844
- expect(() =>
1845
- parseproposal(
1846
- {
1847
- version: protocolversion,
1848
- plan: {
1849
- objective: "Bad path",
1850
- steps: [
1851
- {
1852
- kind: "parsejson",
1853
- options: '{"call":"c1","fields":[{"name":"a","path":"bad..path"}]}',
1854
- summary: "Parse a bad path.",
1855
- },
1856
- ],
1857
- },
1858
- },
1859
- "https://example.com",
1860
- ),
1861
- ).toThrow("dotted path");
1862
- });
1863
- });
1864
-
1865
- describe("network observation part two protocol", () => {
1866
- const now = 1_800_000_000_000;
1867
- const origin = "https://example.com";
1868
- const version = protocolversion;
1869
-
1870
- it("wraps step outcomes with the network block of exchange counts and channel state", () => {
1871
- const plan: agentplan = {
1872
- id: "plan",
1873
- objective: "Observe",
1874
- origin,
1875
- steps: [{ id: "s1", kind: "watchrequests", summary: "Watch.", risk: "read" }],
1876
- createdat: now,
1877
- expiresat: now + 1000,
1878
- state: "approved",
1879
- };
1880
- const outcome: stepoutcome = {
1881
- stepid: "s1",
1882
- ok: true,
1883
- summary: "Observed 3 requests.",
1884
- details: { network: { exchanges: 3, channelstate: "none", messages: 0 } },
1885
- at: now,
1886
- };
1887
- const parsed = JSON.parse(
1888
- outcomeresponse({ outcome, plan, network: { exchanges: 3, channelstate: "none", messages: 0 } }),
1889
- ) as { network?: { exchanges: number; channelstate: string; messages: number } };
1890
- expect(parsed.network).toEqual({ exchanges: 3, channelstate: "none", messages: 0 });
1891
- const bare = JSON.parse(outcomeresponse({ outcome, plan })) as { network?: unknown };
1892
- expect(bare.network).toBeUndefined();
1893
- });
1894
-
1895
- it("carries every exchange, channel, subscription and api map entry in the exchanges report", () => {
1896
- const exchange: exchangerecord = {
1897
- id: "e1",
1898
- runid: "run",
1899
- stepid: "s1",
1900
- correlationid: "run-1",
1901
- url: "https://api.example/items",
1902
- origin: "https://api.example",
1903
- method: "GET",
1904
- status: 200,
1905
- statusclass: "success",
1906
- source: "extension",
1907
- timing: 42,
1908
- bytes: 128,
1909
- mime: "application/json",
1910
- bodyref: "body1",
1911
- at: now,
1912
- };
1913
- const channel: channelrecord = {
1914
- id: "ch1",
1915
- runid: "run",
1916
- stepid: "s2",
1917
- kind: "websocket",
1918
- url: "wss://api.example/live",
1919
- origin: "https://api.example",
1920
- state: "open",
1921
- openedat: now,
1922
- sent: 2,
1923
- received: 5,
1924
- reconnects: 1,
1925
- };
1926
- const subscription: eventsubscription = {
1927
- id: "sub1",
1928
- runid: "run",
1929
- stepid: "s3",
1930
- url: "https://api.example/stream",
1931
- origin: "https://api.example",
1932
- state: "open",
1933
- events: 9,
1934
- names: ["userjoin"],
1935
- lasteventid: "41",
1936
- cancel: { kind: "stop", value: "done" },
1937
- openedat: now,
1938
- };
1939
- const apimap: apimapentry = {
1940
- endpoint: "https://api.example/items",
1941
- method: "GET",
1942
- mime: "application/json",
1943
- frequency: 3,
1944
- payloadshape: ["id"],
1945
- jsonshare: 1,
1946
- stability: 1,
1947
- origin: "https://api.example",
1948
- correlationids: ["run-1"],
1949
- };
1950
- const report = exchangesreport({
1951
- exchanges: [exchange],
1952
- channels: [channel],
1953
- subscriptions: [subscription],
1954
- apimap: [apimap],
1955
- });
1956
- expect(report.version).toBe(version);
1957
- expect(report.exchanges[0]?.correlationid).toBe("run-1");
1958
- expect(report.channels[0]).toMatchObject({ state: "open", sent: 2, received: 5 });
1959
- expect(report.subscriptions[0]).toMatchObject({ events: 9, lasteventid: "41" });
1960
- expect(report.apimap[0]).toMatchObject({ frequency: 3 });
1961
- });
1962
-
1963
- it("rejects channel urls outside the grants, subscriptions without cancellation and lifetimes beyond the plan window", () => {
1964
- const planof = (steps: Array<Record<string, unknown>>, expiresat?: number): unknown => ({
1965
- version,
1966
- plan: { objective: "Observe", origin, steps, ...(expiresat !== undefined ? { expiresat } : {}) },
1967
- });
1968
- const stepinput = (kind: string, options: Record<string, unknown>): Record<string, unknown> => ({
1969
- id: "s1",
1970
- kind,
1971
- summary: "Observe.",
1972
- options: JSON.stringify(options),
1973
- });
1974
- const socket = parseproposal(
1975
- planof([stepinput("opensocket", { socket: { url: "wss://api.example/live" } })]) as Record<string, unknown>,
1976
- origin,
1977
- [origin, "https://api.example"],
1978
- );
1979
- expect(socket.plan.steps[0]?.risk).toBe("read");
1980
- expect(() =>
1981
- parseproposal(
1982
- planof([stepinput("opensocket", { socket: { url: "wss://other.example/live" } })]) as Record<string, unknown>,
1983
- origin,
1984
- [origin],
1985
- ),
1986
- ).toThrow("outside the grants");
1987
- expect(() =>
1988
- parseproposal(
1989
- planof([
1990
- stepinput("subscribesse", {
1991
- subscription: { url: "https://other.example/stream", cancel: { kind: "stop", value: "done" } },
1992
- }),
1993
- ]) as Record<string, unknown>,
1994
- origin,
1995
- [origin],
1996
- ),
1997
- ).toThrow("outside the grants");
1998
- expect(() =>
1999
- parseproposal(
2000
- planof([stepinput("subscribesse", { subscription: { url: "https://api.example/stream" } })]) as Record<
2001
- string,
2002
- unknown
2003
- >,
2004
- origin,
2005
- ["https://api.example"],
2006
- ),
2007
- ).toThrow("cancellation path");
2008
- expect(() =>
2009
- parseproposal(
2010
- planof([
2011
- stepinput("longpoll", { poll: { url: "https://api.example/poll", cursorfield: "c", interval: 50 } }),
2012
- ]) as Record<string, unknown>,
2013
- origin,
2014
- ["https://api.example"],
2015
- ),
2016
- ).toThrow("stop condition");
2017
- expect(() =>
2018
- parseproposal(
2019
- planof(
2020
- [stepinput("opensocket", { socket: { url: "wss://api.example/live", lifetime: 60_000 } })],
2021
- Date.now() + 5000,
2022
- ),
2023
- origin,
2024
- ["https://api.example"],
2025
- ),
2026
- ).toThrow("exceeds the reviewed plan window");
2027
- expect(() =>
2028
- parseproposal(
2029
- planof(
2030
- [
2031
- stepinput("subscribesse", {
2032
- subscription: {
2033
- url: "https://api.example/stream",
2034
- lifetime: 60_000,
2035
- cancel: { kind: "lifetime", value: 60_000 },
2036
- },
2037
- }),
2038
- ],
2039
- Date.now() + 5000,
2040
- ),
2041
- origin,
2042
- ["https://api.example"],
2043
- ),
2044
- ).toThrow("exceeds the reviewed plan window");
2045
- expect(() =>
2046
- parseproposal(
2047
- planof(
2048
- [stepinput("opensocket", { socket: { url: "wss://api.example/live", lifetime: 4000 } })],
2049
- Date.now() + 5000,
2050
- ),
2051
- origin,
2052
- ["https://api.example"],
2053
- ),
2054
- ).not.toThrow();
2055
- });
2056
-
2057
- it("grades conditional capturebodies and extractapi steps through the resolved risk", () => {
2058
- const stepinput = (kind: string, options: Record<string, unknown>): Record<string, unknown> => ({
2059
- id: "s1",
2060
- kind,
2061
- summary: "Observe.",
2062
- options: JSON.stringify(options),
2063
- });
2064
- const privatebodies = parseproposal(
2065
- {
2066
- version,
2067
- plan: {
2068
- objective: "Capture",
2069
- origin,
2070
- steps: [stepinput("capturebodies", { body: { mimes: ["application/json"], ceiling: 100 } })],
2071
- },
2072
- },
2073
- origin,
2074
- [origin],
2075
- );
2076
- expect(privatebodies.plan.steps[0]?.risk).toBe("sensitive");
2077
- const publicbodies = parseproposal(
2078
- {
2079
- version,
2080
- plan: {
2081
- objective: "Capture",
2082
- origin,
2083
- steps: [stepinput("capturebodies", { body: { mimes: ["image/png"], ceiling: 100 } })],
2084
- },
2085
- },
2086
- origin,
2087
- [origin],
2088
- );
2089
- expect(publicbodies.plan.steps[0]?.risk).toBe("interaction");
2090
- const readreplay = parseproposal(
2091
- {
2092
- version,
2093
- plan: {
2094
- objective: "Replay",
2095
- origin,
2096
- steps: [stepinput("extractapi", { replay: { endpoint: "https://api.example/items", verb: "GET" } })],
2097
- },
2098
- },
2099
- origin,
2100
- [origin, "https://api.example"],
2101
- );
2102
- expect(readreplay.plan.steps[0]?.risk).toBe("read");
2103
- const mutreplay = parseproposal(
2104
- {
2105
- version,
2106
- plan: {
2107
- objective: "Replay",
2108
- origin,
2109
- steps: [stepinput("extractapi", { replay: { endpoint: "https://api.example/items", verb: "POST" } })],
2110
- },
2111
- },
2112
- origin,
2113
- [origin, "https://api.example"],
2114
- );
2115
- expect(mutreplay.plan.steps[0]?.risk).toBe("sensitive");
2116
- const send = parseproposal(
2117
- {
2118
- version,
2119
- plan: {
2120
- objective: "Send",
2121
- origin,
2122
- steps: [stepinput("sendmessage", { message: { channel: "ch1", payload: "x" } })],
2123
- },
2124
- },
2125
- origin,
2126
- [origin],
2127
- );
2128
- expect(send.plan.steps[0]?.risk).toBe("sensitive");
2129
- });
2130
- });
2131
-
2132
- describe("network control protocol", () => {
2133
- const plan: agentplan = {
2134
- id: "plan",
2135
- objective: "Control traffic",
2136
- origin: "https://example.com",
2137
- steps: [],
2138
- createdat: 1,
2139
- expiresat: 2,
2140
- state: "approved",
2141
- };
2142
- const outcome: stepoutcome = { stepid: "s1", ok: true, summary: "Applied the reviewed rules.", at: 3 };
2143
-
2144
- it("attaches the control block with applied rule counts to the response envelope", () => {
2145
- const envelope = JSON.parse(outcomeresponse({ outcome, plan, control: { applied: 3, blocked: 2, mocked: 1 } }));
2146
- expect(envelope.control).toEqual({ applied: 3, blocked: 2, mocked: 1 });
2147
- const bare = JSON.parse(outcomeresponse({ outcome, plan }));
2148
- expect(bare.control).toBeUndefined();
2149
- });
2150
-
2151
- it("carries token scopes without token values in the auth report", () => {
2152
- const report = authreport({
2153
- tokens: [
2154
- {
2155
- id: "t1",
2156
- provider: "providerco",
2157
- origin: "https://api.example",
2158
- scopes: ["read"],
2159
- accessstorageid: "token-t1-access",
2160
- expiresat: 99,
2161
- at: 1,
2162
- },
2163
- ],
2164
- });
2165
- expect(report.tokens[0]).toMatchObject({ id: "t1", provider: "providerco", scopes: ["read"] });
2166
- expect(JSON.stringify(report)).not.toContain("token-t1-access");
2167
- });
2168
-
2169
- it("carries the traffic control state with mock bodies held back", () => {
2170
- const report = controlreport({
2171
- blocks: [
2172
- { id: "b1", runid: "run1", stepid: "s1", urlpattern: "https://ads.example/*", hits: 2, registeredat: 1 },
2173
- ],
2174
- mocks: [
2175
- {
2176
- id: "m1",
2177
- runid: "run1",
2178
- stepid: "s1",
2179
- urlpattern: "https://api.example/status",
2180
- status: 204,
2181
- body: "fixture-body",
2182
- reviewed: true,
2183
- hits: 1,
2184
- registeredat: 1,
2185
- },
2186
- ],
2187
- rewrites: [
2188
- {
2189
- id: "h1",
2190
- runid: "run1",
2191
- stepid: "s1",
2192
- urlpattern: "https://api.example/*",
2193
- name: "accept",
2194
- operation: "set",
2195
- value: "json",
2196
- hits: 4,
2197
- registeredat: 1,
2198
- },
2199
- ],
2200
- cookies: [
2201
- { id: "c1", runid: "run1", stepid: "s1", kind: "write", domain: "example.com", names: ["session"], at: 1 },
2202
- ],
2203
- proxies: [
2204
- {
2205
- id: "p1",
2206
- runid: "run1",
2207
- stepid: "s1",
2208
- scheme: "socks5",
2209
- host: "proxy.example",
2210
- port: 1080,
2211
- bypass: ["https://api.example"],
2212
- appliedat: 1,
2213
- },
2214
- ],
2215
- ratelimits: [{ origin: "https://api.example", remaining: 3, limit: 10, resetat: 50, at: 1 }],
2216
- });
2217
- expect(report.blocks[0]).toMatchObject({ hits: 2 });
2218
- expect(report.mocks[0]).toMatchObject({ status: 204, hits: 1 });
2219
- expect(report.mocks[0]?.body).toBeUndefined();
2220
- expect(JSON.stringify(report)).not.toContain("fixture-body");
2221
- expect(report.cookies[0]).toMatchObject({ kind: "write" });
2222
- expect(report.ratelimits[0]).toMatchObject({ remaining: 3 });
2223
- });
2224
-
2225
- it("rejects block rules without an origin pattern and proxy routes without a bypass list", () => {
2226
- expect(() =>
2227
- parseproposal(
2228
- {
2229
- version: protocolversion,
2230
- plan: {
2231
- objective: "Block",
2232
- steps: [
2233
- {
2234
- kind: "blockrequest",
2235
- summary: "Block",
2236
- options: JSON.stringify({ block: { urlpattern: "ads.example/*", reviewed: true } }),
2237
- },
2238
- ],
2239
- },
2240
- },
2241
- "https://example.com",
2242
- ),
2243
- ).toThrow("Block rules without a named origin pattern are refused.");
2244
- expect(() =>
2245
- parseproposal(
2246
- {
2247
- version: protocolversion,
2248
- plan: {
2249
- objective: "Proxy",
2250
- steps: [
2251
- {
2252
- kind: "routeproxy",
2253
- summary: "Route",
2254
- options: JSON.stringify({
2255
- consentref: "c",
2256
- proxy: { scheme: "socks5", host: "proxy.example", port: 1080, bypass: [] },
2257
- }),
2258
- },
2259
- ],
2260
- },
2261
- },
2262
- "https://example.com",
2263
- ),
2264
- ).toThrow("Proxy routes without a bypass list are refused.");
2265
- });
2266
- });
2267
-
2268
- describe("debugging protocol", () => {
2269
- const plan = {
2270
- id: "plan",
2271
- objective: "Debug the page",
2272
- origin: "https://example.com",
2273
- steps: [],
2274
- createdat: 1,
2275
- expiresat: 2,
2276
- state: "approved" as const,
2277
- };
2278
-
2279
- it("attaches the timeline block to the outcome envelope and carries the timeline and console diff reports", () => {
2280
- const outcome: stepoutcome = {
2281
- stepid: "w1",
2282
- ok: true,
2283
- summary: "Captured 4 console calls.",
2284
- details: { errorids: ["e1"], rejectionids: [] },
2285
- at: 5,
2286
- };
2287
- const envelope = JSON.parse(
2288
- outcomeresponse({
2289
- outcome,
2290
- plan,
2291
- timeline: { entries: 4, levels: { error: 1, warn: 1, info: 2, log: 0, debug: 0, trace: 0 }, collapsed: 2 },
2292
- }),
2293
- ) as { version: string; timeline: { entries: number; levels: Record<string, number>; collapsed: number } };
2294
- expect(envelope.version).toBe(protocolversion);
2295
- expect(envelope.timeline).toEqual({
2296
- entries: 4,
2297
- levels: { error: 1, warn: 1, info: 2, log: 0, debug: 0, trace: 0 },
2298
- collapsed: 2,
2299
- });
2300
- const plain = JSON.parse(outcomeresponse({ outcome, plan })) as { timeline?: unknown };
2301
- expect(plain.timeline).toBeUndefined();
2302
- const timeline = timelinereport({
2303
- entries: [{ id: "t1", runid: "plan", stepid: "w1", time: 3, level: "error", source: "error", message: "boom" }],
2304
- errors: [
2305
- {
2306
- id: "e1",
2307
- runid: "plan",
2308
- stepid: "w1",
2309
- message: "boom",
2310
- frames: [{ url: "https://example.com/app.js", line: 42 }],
2311
- sourceurl: "https://example.com/app.js",
2312
- line: 42,
2313
- at: 3,
2314
- },
2315
- ],
2316
- rejections: [],
2317
- longtasks: [
2318
- { id: "l1", runid: "plan", stepid: "w1", duration: 90, starttime: 10, attributions: ["same-origin"], at: 3 },
2319
- ],
2320
- levelcounts: { error: 1 },
2321
- });
2322
- expect(timeline.version).toBe(protocolversion);
2323
- expect(timeline.entries[0]?.message).toBe("boom");
2324
- expect(timeline.errors[0]?.frames).toHaveLength(1);
2325
- expect(timeline.longtasks[0]?.attributions).toEqual(["same-origin"]);
2326
- const diff = consolediffreport({
2327
- diff: {
2328
- base: "run1",
2329
- target: "run2",
2330
- lines: [
2331
- { kind: "added", text: "fresh" },
2332
- { kind: "removed", text: "gone" },
2333
- { kind: "repeated", text: "boot", count: 1 },
2334
- ],
2335
- added: 1,
2336
- removed: 1,
2337
- repeated: 1,
2338
- at: 8,
2339
- },
2340
- });
2341
- expect(diff.diff.base).toBe("run1");
2342
- expect(diff.diff.lines).toHaveLength(3);
2343
- });
2344
-
2345
- it("accepts the watch options grammar and rejects level floors outside the level set", () => {
2346
- const proposal = parseproposal(
2347
- {
2348
- version: protocolversion,
2349
- plan: {
2350
- objective: "Debug",
2351
- steps: [
2352
- {
2353
- kind: "watchconsole",
2354
- options: JSON.stringify({
2355
- watch: { window: 250 },
2356
- level: "warn",
2357
- depth: 3,
2358
- redact: ["secret"],
2359
- spam: { pattern: "retry", windowsize: 500, collapse: 2 },
2360
- rotation: { maxentries: 40, overflowtarget: "overflow1" },
2361
- }),
2362
- summary: "Capture console output at every level.",
2363
- },
2364
- {
2365
- kind: "watcherrors",
2366
- options: JSON.stringify({ watch: { window: 250 }, level: "error" }),
2367
- summary: "Capture errors with stacks.",
2368
- },
2369
- {
2370
- kind: "watchtasks",
2371
- options: JSON.stringify({ watch: { window: 250 }, threshold: 50 }),
2372
- summary: "Capture long task timings.",
2373
- },
2374
- ],
2375
- },
2376
- },
2377
- "https://example.com",
2378
- );
2379
- expect(proposal.plan.steps.map((step) => step.risk)).toEqual(["read", "read", "read"]);
2380
- expect(() =>
2381
- parseproposal(
2382
- {
2383
- version: protocolversion,
2384
- plan: {
2385
- objective: "Debug",
2386
- steps: [
2387
- {
2388
- kind: "watchconsole",
2389
- options: JSON.stringify({ watch: { window: 250 }, redact: ["s"], level: "verbose" }),
2390
- summary: "Bad level floor.",
2391
- },
2392
- ],
2393
- },
2394
- },
2395
- "https://example.com",
2396
- ),
2397
- ).toThrow("level floor");
2398
- expect(() =>
2399
- parseproposal(
2400
- {
2401
- version: protocolversion,
2402
- plan: {
2403
- objective: "Debug",
2404
- steps: [
2405
- {
2406
- kind: "watchconsole",
2407
- options: JSON.stringify({ watch: { window: 250 } }),
2408
- summary: "Missing redaction.",
2409
- },
2410
- ],
2411
- },
2412
- },
2413
- "https://example.com",
2414
- ),
2415
- ).toThrow("redaction");
2416
- });
2417
-
2418
- it("rejects watch steps for ungranted origins", () => {
2419
- expect(() =>
2420
- parseproposal(
2421
- {
2422
- version: protocolversion,
2423
- plan: {
2424
- objective: "Debug",
2425
- steps: [
2426
- {
2427
- kind: "watchconsole",
2428
- options: JSON.stringify({ watch: { window: 250 }, redact: ["secret"] }),
2429
- summary: "Watch the console.",
2430
- },
2431
- ],
2432
- },
2433
- },
2434
- "https://example.com",
2435
- ["https://other.example"],
2436
- ),
2437
- ).toThrow("outside the grants");
2438
- const granted = parseproposal(
2439
- {
2440
- version: protocolversion,
2441
- plan: {
2442
- objective: "Debug",
2443
- steps: [
2444
- {
2445
- kind: "watchconsole",
2446
- options: JSON.stringify({ watch: { window: 250 }, redact: ["secret"] }),
2447
- summary: "Watch the console.",
2448
- },
2449
- ],
2450
- },
2451
- },
2452
- "https://example.com",
2453
- ["https://example.com"],
2454
- );
2455
- expect(granted.plan.steps).toHaveLength(1);
2456
- });
2457
- });
2458
-
2459
- describe("debugging part two protocol", () => {
2460
- const attachoptions = {
2461
- domains: ["Runtime", "Debugger"],
2462
- teardown: { revertsteps: ["revert breakpoints", "revert overrides"], resumepolicy: "pause" },
2463
- };
2464
-
2465
- it("attaches the cdp block to the outcome envelope and carries the cdp report with sources held back", () => {
2466
- const outcome: stepoutcome = { stepid: "c1", ok: true, summary: "The command returned.", at: 4 };
2467
- const plan: agentplan = {
2468
- id: "plan",
2469
- objective: "Debug",
2470
- origin: "https://example.com",
2471
- steps: [
2472
- {
2473
- id: "c1",
2474
- kind: "cdpcmd",
2475
- summary: "Evaluate",
2476
- risk: "sensitive",
2477
- options: JSON.stringify({ command: { method: "Runtime.evaluate" } }),
2478
- },
2479
- ],
2480
- createdat: 1,
2481
- expiresat: 9000,
2482
- state: "approved",
2483
- };
2484
- const envelope = JSON.parse(
2485
- outcomeresponse({ outcome, plan, cdp: { sessionid: "cdp1", state: "attached", commandids: ["cmd1", "cmd2"] } }),
2486
- ) as { cdp: { sessionid: string; state: string; commandids: string[] } };
2487
- expect(envelope.cdp).toEqual({ sessionid: "cdp1", state: "attached", commandids: ["cmd1", "cmd2"] });
2488
- const plain = JSON.parse(outcomeresponse({ outcome, plan })) as { cdp?: unknown };
2489
- expect(plain.cdp).toBeUndefined();
2490
- const session: cdpsession = {
2491
- id: "cdp1",
2492
- runid: "plan",
2493
- stepid: "a1",
2494
- tabid: 4,
2495
- origin: "https://example.com",
2496
- attachedat: 2,
2497
- domains: ["Runtime"],
2498
- debuggerversion: "devthink instrumented harness 1.1.46 (no chrome.debugger permission)",
2499
- };
2500
- const command: cdpcommand = {
2501
- id: "cmd1",
2502
- sessionid: "cdp1",
2503
- runid: "plan",
2504
- stepid: "c1",
2505
- method: "Runtime.evaluate",
2506
- domain: "Runtime",
2507
- duration: 8,
2508
- at: 3,
2509
- };
2510
- const rule: cdpeventrule = {
2511
- id: "r1",
2512
- sessionid: "cdp1",
2513
- runid: "plan",
2514
- stepid: "w1",
2515
- domain: "Log",
2516
- event: "entryAdded",
2517
- events: 2,
2518
- registeredat: 2,
2519
- };
2520
- const breakpoint: breakpointspec = {
2521
- id: "bp1",
2522
- runid: "plan",
2523
- stepid: "b1",
2524
- url: "https://example.com/app.js",
2525
- line: 5,
2526
- condition: "items.length > 0",
2527
- hits: 3,
2528
- registeredat: 2,
2529
- };
2530
- const pause: pausestate = {
2531
- id: "p1",
2532
- runid: "plan",
2533
- stepid: "b1",
2534
- reason: "breakpoint",
2535
- callframes: [{ url: "https://example.com/app.js", line: 5 }],
2536
- hitbreakpoint: "bp1",
2537
- domsnapshotid: "dom-3",
2538
- at: 4,
2539
- };
2540
- const watch: watchexpression = {
2541
- id: "we1",
2542
- runid: "plan",
2543
- stepid: "w1",
2544
- expression: "items.length",
2545
- scope: "topframe",
2546
- reviewed: true,
2547
- values: [{ pauseid: "p1", value: "3", at: 4 }],
2548
- at: 2,
2549
- };
2550
- const override: scriptoverride = {
2551
- id: "ov1",
2552
- runid: "plan",
2553
- stepid: "o1",
2554
- urlpattern: "https://cdn.example/vendor.js",
2555
- source: "window.fixture = true;",
2556
- reviewed: true,
2557
- hits: 1,
2558
- appliedat: 2,
2559
- };
2560
- const grant: debuggergrant = {
2561
- id: "g1",
2562
- prompt: "Debugger attach prompt",
2563
- origin: "https://example.com",
2564
- domains: ["Runtime"],
2565
- approved: true,
2566
- consentedat: 1,
2567
- };
2568
- const report = cdpreport({
2569
- sessions: [session],
2570
- commands: [command],
2571
- events: [rule],
2572
- breakpoints: [breakpoint],
2573
- pauses: [pause],
2574
- watches: [watch],
2575
- overrides: [override],
2576
- grants: [grant],
2577
- });
2578
- expect(report.version).toBe(protocolversion);
2579
- expect(report.sessions[0]?.domains).toEqual(["Runtime"]);
2580
- expect(report.commands[0]).toMatchObject({ method: "Runtime.evaluate", duration: 8 });
2581
- expect(report.breakpoints[0]).toMatchObject({ hits: 3, condition: "items.length > 0" });
2582
- expect(report.pauses[0]).toMatchObject({ domsnapshotid: "dom-3" });
2583
- expect(report.watches[0]?.values).toHaveLength(1);
2584
- expect(report.overrides[0]?.source).toBeUndefined();
2585
- expect(report.overrides[0]?.urlpattern).toBe("https://cdn.example/vendor.js");
2586
- expect(report.grants[0]?.prompt).toBeUndefined();
2587
- expect(report.grants[0]?.domains).toEqual(["Runtime"]);
2588
- });
2589
-
2590
- it("rejects cdp proposals outside the attach contract and accepts the reviewed chain", () => {
2591
- const attach = {
2592
- kind: "attachcdp",
2593
- options: JSON.stringify(attachoptions),
2594
- summary: "Attach the devtools session.",
2595
- };
2596
- const command = {
2597
- kind: "cdpcmd",
2598
- options: JSON.stringify({ command: { method: "Runtime.evaluate" } }),
2599
- summary: "Evaluate an expression.",
2600
- };
2601
- const accepted = parseproposal(
2602
- { version: protocolversion, plan: { objective: "Debug", steps: [attach, command] } },
2603
- "https://example.com",
2604
- );
2605
- expect(accepted.plan.steps).toHaveLength(2);
2606
- expect(accepted.plan.steps[1]?.risk).toBe("sensitive");
2607
- expect(() =>
2608
- parseproposal(
2609
- {
2610
- version: protocolversion,
2611
- plan: {
2612
- objective: "Debug",
2613
- steps: [
2614
- {
2615
- kind: "attachcdp",
2616
- options: JSON.stringify({ domains: ["Runtime"] }),
2617
- summary: "Attach without a teardown plan.",
2618
- },
2619
- ],
2620
- },
2621
- },
2622
- "https://example.com",
2623
- ),
2624
- ).toThrow("teardown plan");
2625
- expect(() =>
2626
- parseproposal(
2627
- {
2628
- version: protocolversion,
2629
- plan: {
2630
- objective: "Debug",
2631
- steps: [
2632
- {
2633
- kind: "attachcdp",
2634
- options: JSON.stringify({ domains: [], teardown: { revertsteps: ["x"] } }),
2635
- summary: "Attach without domains.",
2636
- },
2637
- ],
2638
- },
2639
- },
2640
- "https://example.com",
2641
- ),
2642
- ).toThrow("domain");
2643
- expect(() =>
2644
- parseproposal(
2645
- { version: protocolversion, plan: { objective: "Debug", steps: [command] } },
2646
- "https://example.com",
2647
- ),
2648
- ).toThrow("attachcdp step");
2649
- expect(() =>
2650
- parseproposal(
2651
- {
2652
- version: protocolversion,
2653
- plan: {
2654
- objective: "Debug",
2655
- steps: [
2656
- attach,
2657
- {
2658
- kind: "cdpcmd",
2659
- options: JSON.stringify({ command: { method: "DOM.getSnapshot" } }),
2660
- summary: "Command outside the allowlist.",
2661
- },
2662
- ],
2663
- },
2664
- },
2665
- "https://example.com",
2666
- ),
2667
- ).toThrow("allowlist");
2668
- expect(() =>
2669
- parseproposal(
2670
- {
2671
- version: protocolversion,
2672
- plan: {
2673
- objective: "Debug",
2674
- steps: [
2675
- {
2676
- kind: "watchcdp",
2677
- options: JSON.stringify({ events: [{ domain: "Log", event: "entryAdded" }], watch: { window: 100 } }),
2678
- summary: "Watch events on an ungranted origin.",
2679
- },
2680
- ],
2681
- },
2682
- },
2683
- "https://example.com",
2684
- ["https://other.example"],
2685
- ),
2686
- ).toThrow("outside the grants");
2687
- const gated = {
2688
- kind: "attachcdp",
2689
- options: JSON.stringify({ ...attachoptions, allowlist: { domains: ["Runtime"], methods: ["Runtime.evaluate"] } }),
2690
- summary: "Attach with method gates.",
2691
- };
2692
- expect(
2693
- parseproposal(
2694
- { version: protocolversion, plan: { objective: "Debug", steps: [gated, command] } },
2695
- "https://example.com",
2696
- ).plan.steps,
2697
- ).toHaveLength(2);
2698
- expect(() =>
2699
- parseproposal(
2700
- {
2701
- version: protocolversion,
2702
- plan: {
2703
- objective: "Debug",
2704
- steps: [
2705
- gated,
2706
- {
2707
- kind: "cdpcmd",
2708
- options: JSON.stringify({ command: { method: "Runtime.callFunctionOn" } }),
2709
- summary: "Gated out.",
2710
- },
2711
- ],
2712
- },
2713
- },
2714
- "https://example.com",
2715
- ),
2716
- ).toThrow("allowlist");
2717
- });
2718
- });
2719
-
2720
- describe("debugging part three protocol", () => {
2721
- it("attaches the profile block to the outcome envelope and carries the profile report with consent prompts held back", () => {
2722
- const outcome: stepoutcome = { stepid: "m1", ok: true, summary: "The flow measurement ran.", at: 4 };
2723
- const plan: agentplan = {
2724
- id: "plan",
2725
- objective: "Profile",
2726
- origin: "https://example.com",
2727
- steps: [
2728
- {
2729
- id: "m1",
2730
- kind: "measureflow",
2731
- summary: "Measure",
2732
- risk: "read",
2733
- options: JSON.stringify({
2734
- flow: { prefix: "flow", steps: ["s1"], metrics: ["navigation"] },
2735
- watch: { window: 100 },
2736
- }),
2737
- },
2738
- ],
2739
- createdat: 1,
2740
- expiresat: 9000,
2741
- state: "approved",
2742
- };
2743
- const envelope = JSON.parse(outcomeresponse({ outcome, plan, profile: { metrics: 3, samples: 5 } })) as {
2744
- profile: { metrics: number; samples: number };
2745
- };
2746
- expect(envelope.profile).toEqual({ metrics: 3, samples: 5 });
2747
- const plain = JSON.parse(outcomeresponse({ outcome, plan })) as { profile?: unknown };
2748
- expect(plain.profile).toBeUndefined();
2749
- const flow: flowmetric = {
2750
- id: "f1",
2751
- runid: "plan",
2752
- stepid: "m1",
2753
- name: "navigation",
2754
- start: 0,
2755
- end: 800,
2756
- duration: 800,
2757
- steps: ["s1"],
2758
- at: 2,
2759
- };
2760
- const heap: heaprecord = {
2761
- id: "h1",
2762
- runid: "plan",
2763
- stepid: "hs",
2764
- origin: "https://example.com",
2765
- bytesize: 12_000_000,
2766
- nodecount: 1450,
2767
- capturedat: 3,
2768
- };
2769
- const sample: growsample = {
2770
- id: "g1",
2771
- runid: "plan",
2772
- stepid: "s1",
2773
- usedbytes: 1_000_000,
2774
- limitbytes: 4_000_000,
2775
- at: 3,
2776
- };
2777
- const trend: memorytrend = { runid: "plan", slope: 2000, samples: 2, flaggedsteps: ["s2"], at: 4 };
2778
- const cpu: cpuprofile = {
2779
- id: "c1",
2780
- runid: "plan",
2781
- stepid: "cpu",
2782
- origin: "https://example.com",
2783
- duration: 500,
2784
- samplecount: 3,
2785
- hotfunctions: ["render"],
2786
- at: 4,
2787
- };
2788
- const shift: shiftentry = {
2789
- id: "sh1",
2790
- runid: "plan",
2791
- stepid: "ws",
2792
- score: 0.12,
2793
- starttime: 40,
2794
- selectors: ["img#hero"],
2795
- at: 4,
2796
- };
2797
- const trace: tracerecord = {
2798
- id: "t1",
2799
- runid: "plan",
2800
- stepid: "tl",
2801
- origin: "https://example.com",
2802
- categories: ["scripting"],
2803
- bytesize: 400,
2804
- events: 2,
2805
- annotations: [{ stepid: "s1", label: "open", offset: 100 }],
2806
- startedat: 1,
2807
- endedat: 4,
2808
- };
2809
- const map: sourcemapref = {
2810
- id: "sm1",
2811
- runid: "plan",
2812
- stepid: "sm",
2813
- origin: "https://example.com",
2814
- scripturl: "https://example.com/app.js",
2815
- mapurl: "https://example.com/app.js.map",
2816
- parsed: true,
2817
- at: 4,
2818
- };
2819
- const consent: sourcemapconsent = {
2820
- id: "c1",
2821
- prompt: "Source map capture prompt",
2822
- origin: "https://example.com",
2823
- approved: true,
2824
- consentedat: 1,
2825
- };
2826
- const report = profilereport({
2827
- flows: [flow],
2828
- heaps: [heap],
2829
- samples: [sample],
2830
- trends: [trend],
2831
- profiles: [cpu],
2832
- shifts: [shift],
2833
- traces: [trace],
2834
- sourcemaps: [map],
2835
- consents: [consent],
2836
- });
2837
- expect(report.version).toBe(protocolversion);
2838
- expect(report.flows[0]).toMatchObject({ name: "navigation", duration: 800, steps: ["s1"] });
2839
- expect(report.heaps[0]).toMatchObject({ bytesize: 12_000_000, nodecount: 1450 });
2840
- expect(report.trends[0]?.flaggedsteps).toEqual(["s2"]);
2841
- expect(report.profiles[0]?.hotfunctions).toEqual(["render"]);
2842
- expect(report.shifts[0]?.selectors).toEqual(["img#hero"]);
2843
- expect(report.traces[0]?.annotations).toHaveLength(1);
2844
- expect(report.sourcemaps[0]).toMatchObject({ parsed: true });
2845
- expect(report.consents[0]?.prompt).toBeUndefined();
2846
- expect(report.consents[0]?.origin).toBe("https://example.com");
2847
- });
2848
-
2849
- it("rejects profiling proposals outside the grants, the reviewed categories and the annotation contract", () => {
2850
- const flow = {
2851
- kind: "measureflow",
2852
- options: JSON.stringify({
2853
- flow: { prefix: "flow", steps: ["s1"], metrics: ["navigation", "blocking"] },
2854
- watch: { window: 100 },
2855
- }),
2856
- summary: "Measure the flow.",
2857
- };
2858
- expect(
2859
- parseproposal({ version: protocolversion, plan: { objective: "Profile", steps: [flow] } }, "https://example.com")
2860
- .plan.steps[0]?.risk,
2861
- ).toBe("read");
2862
- expect(() =>
2863
- parseproposal(
2864
- { version: protocolversion, plan: { objective: "Profile", steps: [flow] } },
2865
- "https://example.com",
2866
- ["https://other.example"],
2867
- ),
2868
- ).toThrow("outside the grants");
2869
- expect(() =>
2870
- parseproposal(
2871
- {
2872
- version: protocolversion,
2873
- plan: {
2874
- objective: "Profile",
2875
- steps: [
2876
- {
2877
- kind: "traceload",
2878
- options: JSON.stringify({ trace: { categories: ["secret"], window: 100 } }),
2879
- summary: "Trace with an unreviewed category.",
2880
- },
2881
- ],
2882
- },
2883
- },
2884
- "https://example.com",
2885
- ),
2886
- ).toThrow("reviewed list");
2887
- expect(() =>
2888
- parseproposal(
2889
- {
2890
- version: protocolversion,
2891
- plan: {
2892
- objective: "Profile",
2893
- steps: [
2894
- {
2895
- kind: "heapshot",
2896
- options: JSON.stringify({ target: { kind: "iframe", url: "https://elsewhere.example/frame" } }),
2897
- summary: "Snapshot an ungranted frame.",
2898
- },
2899
- ],
2900
- },
2901
- },
2902
- "https://example.com",
2903
- ),
2904
- ).toThrow("outside the granted origins");
2905
- const framed = {
2906
- kind: "heapshot",
2907
- options: JSON.stringify({ target: { kind: "worker", url: "https://cdn.example/worker.js" } }),
2908
- summary: "Snapshot a granted worker.",
2909
- };
2910
- expect(
2911
- parseproposal(
2912
- { version: protocolversion, plan: { objective: "Profile", steps: [framed] } },
2913
- "https://example.com",
2914
- ["https://example.com", "https://cdn.example"],
2915
- ).plan.steps,
2916
- ).toHaveLength(1);
2917
- expect(() =>
2918
- parseproposal(
2919
- {
2920
- version: protocolversion,
2921
- plan: {
2922
- objective: "Profile",
2923
- steps: [
2924
- {
2925
- kind: "capturesourcemaps",
2926
- options: JSON.stringify({ scripts: ["https://elsewhere.example/app.js"] }),
2927
- summary: "Capture maps of an ungranted origin.",
2928
- },
2929
- ],
2930
- },
2931
- },
2932
- "https://example.com",
2933
- ),
2934
- ).toThrow("outside the grants");
2935
- expect(() =>
2936
- parseproposal(
2937
- {
2938
- version: protocolversion,
2939
- plan: {
2940
- objective: "Profile",
2941
- steps: [
2942
- {
2943
- kind: "annotatetrace",
2944
- options: JSON.stringify({ trace: { traceid: "t1" } }),
2945
- summary: "Annotate without annotations.",
2946
- },
2947
- ],
2948
- },
2949
- },
2950
- "https://example.com",
2951
- ),
2952
- ).toThrow("step annotations");
2953
- const annotated = {
2954
- kind: "annotatetrace",
2955
- options: JSON.stringify({ trace: { traceid: "t1" }, annotations: [{ stepid: "s1", label: "open" }] }),
2956
- summary: "Annotate the trace.",
2957
- };
2958
- expect(
2959
- parseproposal(
2960
- { version: protocolversion, plan: { objective: "Profile", steps: [annotated] } },
2961
- "https://example.com",
2962
- ).plan.steps,
2963
- ).toHaveLength(1);
2964
- });
2965
- });
2966
-
2967
- describe("emulation protocol", () => {
2968
- const outcome: stepoutcome = { stepid: "e1", ok: true, summary: "Applied the device layer phone.", at: 1 };
2969
- const plan: agentplan = {
2970
- id: "plan",
2971
- objective: "Emulate",
2972
- origin: "https://example.com",
2973
- steps: [],
2974
- createdat: 1,
2975
- expiresat: 2,
2976
- state: "approved",
2977
- };
2978
- const device = { name: "phone", width: 390, height: 844, pixelratio: 3, mobile: true };
2979
- const network = { name: "slow3g", latency: 400, download: 400, upload: 400, offline: true };
2980
- const location = { name: "lisbon", latitude: 38.7223, longitude: -9.1393, accuracy: 100 };
2981
- const agent = {
2982
- name: "desktopmask",
2983
- useragent: "Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0.0.0 Safari/537.36",
2984
- platform: "Linux x86_64",
2985
- brands: ["Chromium"],
2986
- };
2987
- const revertplan = ["restore the prior page state"];
2988
-
2989
- it("carries the applied and reverted layer names in the emulation block of the response envelope", () => {
2990
- const envelope = JSON.parse(
2991
- outcomeresponse({ outcome, plan, emulation: { applied: ["phone"], reverted: ["slow3g"] } }),
2992
- ) as { emulation?: { applied: string[]; reverted: string[] } };
2993
- expect(envelope.emulation).toEqual({ applied: ["phone"], reverted: ["slow3g"] });
2994
- const bare = JSON.parse(outcomeresponse({ outcome, plan })) as { emulation?: unknown };
2995
- expect(bare.emulation).toBeUndefined();
2996
- });
2997
-
2998
- it("builds the emulation report envelope with the layer history, preset libraries, blackbox rules and permission overrides while the consent prompt text stays out", () => {
2999
- const report = emulationreport({
3000
- state: {
3001
- runid: "run",
3002
- tabid: 4,
3003
- origin: "https://example.com",
3004
- layers: [
3005
- {
3006
- id: "l1",
3007
- runid: "run",
3008
- stepid: "e1",
3009
- family: "device",
3010
- name: "phone",
3011
- originscope: "https://example.com",
3012
- appliedat: 1,
3013
- revertplan,
3014
- },
3015
- ],
3016
- updatedat: 2,
3017
- },
3018
- devices: [device],
3019
- networks: [network],
3020
- locations: [location],
3021
- agents: [agent],
3022
- blackbox: [
3023
- { origin: "https://example.com", rules: [{ urlpatterns: ["https://cdn.example/**"], tracescope: "both" }] },
3024
- ],
3025
- permissions: [
3026
- {
3027
- id: "p1",
3028
- runid: "run",
3029
- stepid: "e1",
3030
- origin: "https://example.com",
3031
- name: "geolocation",
3032
- state: "granted",
3033
- priorstate: "prompt",
3034
- appliedat: 1,
3035
- },
3036
- ],
3037
- consents: [
3038
- {
3039
- id: "c1",
3040
- prompt: "Where?",
3041
- origin: "https://example.com",
3042
- latitude: 38.7223,
3043
- longitude: -9.1393,
3044
- approved: true,
3045
- consentedat: 1,
3046
- },
3047
- ],
3048
- });
3049
- expect(report.layers[0]?.name).toBe("phone");
3050
- expect(report.layers[0]?.revertplan).toEqual(revertplan);
3051
- expect(report.devices[0]?.name).toBe("phone");
3052
- expect(report.networks[0]?.offline).toBe(true);
3053
- expect(report.blackbox[0]?.rules[0]?.tracescope).toBe("both");
3054
- expect(report.permissions[0]?.priorstate).toBe("prompt");
3055
- expect(report.consents[0]?.latitude).toBe(38.7223);
3056
- expect(report.consents[0]).not.toHaveProperty("prompt");
3057
- const empty = emulationreport({
3058
- devices: [],
3059
- networks: [],
3060
- locations: [],
3061
- agents: [],
3062
- blackbox: [],
3063
- permissions: [],
3064
- consents: [],
3065
- });
3066
- expect(empty.layers).toEqual([]);
3067
- expect(empty).not.toHaveProperty("state");
3068
- });
3069
-
3070
- it("rejects emulation steps without a revert plan, location values out of range and unknown permission names", () => {
3071
- expect(() =>
3072
- parseproposal(
3073
- {
3074
- version: protocolversion,
3075
- plan: {
3076
- objective: "Emulate",
3077
- steps: [
3078
- {
3079
- kind: "emulatedevice",
3080
- options: JSON.stringify({ reviewed: true, device }),
3081
- summary: "Mask without a revert plan.",
3082
- },
3083
- ],
3084
- },
3085
- },
3086
- "https://example.com",
3087
- ),
3088
- ).toThrow("revert plan");
3089
- expect(() =>
3090
- parseproposal(
3091
- {
3092
- version: protocolversion,
3093
- plan: {
3094
- objective: "Emulate",
3095
- steps: [
3096
- {
3097
- kind: "emulatelocate",
3098
- options: JSON.stringify({ reviewed: true, location: { ...location, latitude: 91 }, revertplan }),
3099
- summary: "Mask a coordinate out of range.",
3100
- },
3101
- ],
3102
- },
3103
- },
3104
- "https://example.com",
3105
- ),
3106
- ).toThrow("latitude and longitude ranges");
3107
- expect(() =>
3108
- parseproposal(
3109
- {
3110
- version: protocolversion,
3111
- plan: {
3112
- objective: "Emulate",
3113
- steps: [
3114
- {
3115
- kind: "overridepermission",
3116
- options: JSON.stringify({
3117
- reviewed: true,
3118
- permission: { name: "screen-capture", state: "granted" },
3119
- revertplan,
3120
- }),
3121
- summary: "Override an unknown permission.",
3122
- },
3123
- ],
3124
- },
3125
- },
3126
- "https://example.com",
3127
- ),
3128
- ).toThrow("unknown permission names");
3129
- expect(() =>
3130
- parseproposal(
3131
- {
3132
- version: protocolversion,
3133
- plan: {
3134
- objective: "Emulate",
3135
- steps: [
3136
- {
3137
- kind: "setuseragent",
3138
- options: JSON.stringify({ reviewed: true, agent, revertplan }),
3139
- summary: "Mask the agent.",
3140
- },
3141
- ],
3142
- },
3143
- },
3144
- "https://example.com",
3145
- ["https://other.example"],
3146
- ),
3147
- ).toThrow("outside the grants");
3148
- });
3149
-
3150
- it("accepts reviewed emulation steps with their revert plans into a pending plan", () => {
3151
- const proposal = parseproposal(
3152
- {
3153
- version: protocolversion,
3154
- plan: {
3155
- objective: "Emulate",
3156
- steps: [
3157
- {
3158
- kind: "emulatedevice",
3159
- options: JSON.stringify({ reviewed: true, device, revertplan, reload: true }),
3160
- summary: "Wear the reviewed phone mask.",
3161
- },
3162
- {
3163
- kind: "blackboxscripts",
3164
- options: JSON.stringify({
3165
- reviewed: true,
3166
- rules: [{ urlpatterns: ["https://cdn.example/**"], tracescope: "both" }],
3167
- revertplan,
3168
- }),
3169
- summary: "Blackbox the vendor scripts.",
3170
- },
3171
- ],
3172
- },
3173
- },
3174
- "https://example.com",
3175
- );
3176
- expect(proposal.plan.steps[0]?.risk).toBe("sensitive");
3177
- expect(proposal.plan.steps[1]?.risk).toBe("read");
3178
- });
3179
- });
3180
-
3181
- describe("workflow editor protocol", () => {
3182
- it("builds the editor state envelope for panel synchronization", () => {
3183
- const envelope = editorstate({
3184
- versions: [{ workflowid: "wf1", version: 2, createdat: 5, note: "tuned the delay", steps: 3, risk: "read" }],
3185
- diffs: [{ workflowid: "wf1", from: 1, to: 2, added: [], removed: [], changed: [], at: 6 }],
3186
- history: [
3187
- {
3188
- runid: "r1",
3189
- workflowid: "wf1",
3190
- outcome: "done",
3191
- steps: 3,
3192
- total: 3,
3193
- duration: 42,
3194
- cause: "manual",
3195
- startedat: 7,
3196
- endedat: 49,
3197
- },
3198
- ],
3199
- breakpoints: ["s2"],
3200
- overrides: [
3201
- { id: "o1", workflowid: "wf1", pattern: "https://example.com", deltas: { waitms: 500 }, createdat: 8 },
3202
- ],
3203
- imports: [
3204
- { id: "imp1", workflowid: "wf1", name: "imported", version: 1, steps: 2, risk: "sensitive", importedat: 9 },
3205
- ],
3206
- backgroundruns: { wf1: true },
3207
- watchdog: {
3208
- config: { enabled: true, stallthreshold: 30_000, action: "pause" },
3209
- events: [
3210
- { id: "w1", runid: "r1", verdict: "stalled", action: "pause", outcome: "Paused at the checkpoint.", at: 10 },
3211
- ],
3212
- },
3213
- });
3214
- expect(envelope.version).toBe(protocolversion);
3215
- expect(envelope.editor.versions[0]?.note).toBe("tuned the delay");
3216
- expect(envelope.editor.history[0]?.cause).toBe("manual");
3217
- expect(envelope.editor.breakpoints).toEqual(["s2"]);
3218
- expect(envelope.editor.imports[0]?.workflowid).toBe("wf1");
3219
- expect(envelope.editor.backgroundruns).toEqual({ wf1: true });
3220
- expect(envelope.editor.watchdog.config?.action).toBe("pause");
3221
- expect(envelope.editor.watchdog.events[0]?.verdict).toBe("stalled");
3222
- expect("model" in envelope).toBe(false);
3223
- const withmodel = editorstate({
3224
- versions: [],
3225
- history: [],
3226
- overrides: [],
3227
- imports: [],
3228
- backgroundruns: {},
3229
- watchdog: { events: [] },
3230
- model: {
3231
- workflowid: "wf1",
3232
- name: "canvas",
3233
- version: 1,
3234
- origins: ["https://example.com"],
3235
- nodes: [],
3236
- edges: [],
3237
- blocks: [],
3238
- layout: { width: 640, height: 480, viewportx: 0, viewporty: 0, zoom: 1 },
3239
- minimap: { width: 160, height: 100, scale: 1, zoom: 1, viewport: { x: 0, y: 0, width: 160, height: 100 } },
3240
- dirty: false,
3241
- },
3242
- });
3243
- expect(withmodel.model?.name).toBe("canvas");
3244
- expect(withmodel.editor.backgroundruns).toEqual({});
3245
- expect(workflowfileversion).toBe(1);
3246
- });
3247
-
3248
- it("parses run history queries with filters and echoes them in the report", () => {
3249
- expect(runhistoryquery(undefined)).toEqual({});
3250
- expect(runhistoryquery({ workflowid: "wf1", outcome: "done", since: 100, limit: 25 })).toEqual({
3251
- workflowid: "wf1",
3252
- outcome: "done",
3253
- since: 100,
3254
- limit: 25,
3255
- });
3256
- expect(() => runhistoryquery({ workflowid: " " })).toThrow("non-empty string");
3257
- expect(() => runhistoryquery({ outcome: "" })).toThrow("non-empty string");
3258
- expect(() => runhistoryquery({ since: Number.NaN })).toThrow("finite");
3259
- expect(() => runhistoryquery({ limit: 0 })).toThrow("positive integer");
3260
- expect(() => runhistoryquery({ limit: 1.5 })).toThrow("positive integer");
3261
- const report = runhistoryreport({
3262
- entries: [
3263
- {
3264
- runid: "r1",
3265
- workflowid: "wf1",
3266
- outcome: "failed",
3267
- steps: 1,
3268
- total: 3,
3269
- duration: 5,
3270
- cause: "cron",
3271
- startedat: 1,
3272
- endedat: 6,
3273
- },
3274
- ],
3275
- query: { outcome: "failed" },
3276
- });
3277
- expect(report.version).toBe(protocolversion);
3278
- expect(report.entries[0]?.cause).toBe("cron");
3279
- expect(report.query).toEqual({ outcome: "failed" });
3280
- expect(runhistoryreport({ entries: [] }).query).toEqual({});
3281
- });
3282
- });
3283
-
3284
- describe("agent protocol protocol", () => {
3285
- it("builds toolcall and toolresult frames with the json rpc grammar", () => {
3286
- const call = toolcallframe({ id: 7, name: "browser.click", params: { stepid: "s1" } });
3287
- expect(call).toMatchObject({ jsonrpc: "2.0", id: 7, method: "tools/call" });
3288
- expect(call.params).toEqual({ stepid: "s1", name: "browser.click" });
3289
- expect(toolcallframe({ id: "alpha", name: "browser.snapshot" }).params).toEqual({ name: "browser.snapshot" });
3290
- const result = toolresultframe({ id: 7, result: { content: "ran click", iserror: false } });
3291
- expect(result.result).toEqual({ content: "ran click", iserror: false });
3292
- expect(result.error).toBeUndefined();
3293
- const refusal = toolresultframe({
3294
- id: null,
3295
- error: { code: "consentrefused", message: "The consent gates refused the tool call." },
3296
- });
3297
- expect(refusal.id).toBeNull();
3298
- expect(refusal.error?.code).toBe("consentrefused");
3299
- expect(refusal.result).toBeUndefined();
3300
- });
3301
-
3302
- it("carries tool errors with the json rpc error codes in the response envelope", () => {
3303
- const plan: agentplan = {
3304
- id: "plan",
3305
- objective: "Run the tool calls",
3306
- origin: "https://example.com",
3307
- steps: [],
3308
- createdat: 1,
3309
- expiresat: 2,
3310
- state: "approved",
3311
- };
3312
- const outcome: stepoutcome = { stepid: "s1", ok: false, summary: "The tool call was refused.", at: 5 };
3313
- const refused = JSON.parse(
3314
- outcomeresponse({
3315
- outcome,
3316
- plan,
3317
- tool: {
3318
- clientid: "client1",
3319
- tool: "browser.click",
3320
- origin: "https://example.com",
3321
- ok: false,
3322
- code: "consentrefused",
3323
- },
3324
- }),
3325
- ) as { tool?: { clientid: string; tool: string; ok: boolean; code?: string } };
3326
- expect(refused.tool).toEqual({
3327
- clientid: "client1",
3328
- tool: "browser.click",
3329
- origin: "https://example.com",
3330
- ok: false,
3331
- code: "consentrefused",
3332
- });
3333
- const ran = JSON.parse(
3334
- outcomeresponse({
3335
- outcome: { ...outcome, ok: true },
3336
- plan,
3337
- tool: { clientid: "client1", tool: "browser.readtext", origin: "https://example.com", ok: true },
3338
- }),
3339
- ) as { tool?: { ok: boolean; code?: string } };
3340
- expect(ran.tool?.ok).toBe(true);
3341
- expect(ran.tool?.code).toBeUndefined();
3342
- const without = JSON.parse(outcomeresponse({ outcome, plan })) as { tool?: unknown };
3343
- expect(without.tool).toBeUndefined();
3344
- });
3345
- });
3346
-
3347
- describe("agent protocol part two envelopes", () => {
3348
- const now = 1_800_000_000_000;
3349
-
3350
- it("builds the http stream transport report with its event channels", () => {
3351
- const stream = {
3352
- endpoint: "/mcp",
3353
- streampath: "/mcp/stream",
3354
- tls: { mode: "on" as const },
3355
- heartbeatms: 15_000,
3356
- idlewindowms: 45_000,
3357
- };
3358
- const report = httpstreamreport({
3359
- stream,
3360
- channels: [
3361
- { id: "c1", clientid: "client1", openedat: now - 2000, lastbeatat: now - 1000 },
3362
- { id: "c2", clientid: "client2", openedat: now - 2000, lastbeatat: now - 50_000 },
3363
- ],
3364
- now,
3365
- });
3366
- expect(report.endpoint).toBe("/mcp");
3367
- expect(report.streampath).toBe("/mcp/stream");
3368
- expect(report.heartbeatms).toBe(15_000);
3369
- expect(report.idlewindowms).toBe(45_000);
3370
- expect(report.channelsopen).toBe(1);
3371
- expect(report.channelsdead).toBe(1);
3372
- expect(
3373
- httpstreamreport({
3374
- stream: { endpoint: "/mcp", streampath: "/mcp/stream", tls: { mode: "off" } },
3375
- channels: [],
3376
- now,
3377
- }).heartbeatms,
3378
- ).toBe(30_000);
3379
- });
3380
-
3381
- it("builds the pairing and auth handshake frames for issued, verified and refused outcomes", () => {
3382
- const challenge = { nonce: "nonce-1", method: "pairingcode" as const, issuedat: now, expiresat: now + 120_000 };
3383
- const issued = pairingframes({ id: 1, challenge, outcome: "issued" });
3384
- expect(issued.request.method).toBe("pairing");
3385
- expect(issued.request.params?.nonce).toBe("nonce-1");
3386
- expect((issued.response.result as { challenge?: string }).challenge).toBe("nonce-1");
3387
- const verified = pairingframes({ id: 2, challenge, answer: "DT-CODE", outcome: "verified" });
3388
- expect(verified.request.params?.answer).toBe("DT-CODE");
3389
- expect((verified.response.result as { paired?: boolean }).paired).toBe(true);
3390
- const refused = pairingframes({ id: 3, challenge, outcome: "refused" });
3391
- expect(refused.response.error?.code).toBe("consentrefused");
3392
- expect(refused.response.error?.message).toBe("The remote frame failed its authentication handshake.");
3393
- });
3394
-
3395
- it("builds the token report without raw tokens or digests", () => {
3396
- const tokens = [
3397
- {
3398
- id: "t1",
3399
- clientid: "client1",
3400
- hash: "sha256:abc",
3401
- scopes: ["browser" as const],
3402
- issuedat: now - 1000,
3403
- expiresat: now + 500_000,
3404
- },
3405
- {
3406
- id: "t2",
3407
- clientid: "client2",
3408
- hash: "sha256:def",
3409
- scopes: ["memory" as const],
3410
- issuedat: now - 1000,
3411
- expiresat: now + 500_000,
3412
- revokedat: now - 500,
3413
- },
3414
- ];
3415
- const report = tokenreport(tokens, now);
3416
- expect(report.tokens).toHaveLength(2);
3417
- expect(report.tokens[0]?.msremaining).toBe(500_000);
3418
- expect(report.tokens[1]?.revokedat).toBe(now - 500);
3419
- expect(JSON.stringify(report)).not.toContain("sha256:abc");
3420
- expect(JSON.stringify(report)).not.toContain("raw");
3421
- });
3422
-
3423
- it("builds the approval gate request and response frames with redacted arguments", () => {
3424
- const gate = {
3425
- id: "gate-1",
3426
- clientid: "client1",
3427
- tool: "browser.type",
3428
- reason: "Typing.",
3429
- params: { stepid: "s1", value: "hunter2" },
3430
- secretfields: ["value"],
3431
- state: "pending" as const,
3432
- raisedat: now,
3433
- timeoutat: now + 120_000,
3434
- };
3435
- const pending = approvalframes({ id: 9, request: gate });
3436
- expect(pending.raise.method).toBe("approval");
3437
- expect(pending.raise.params?.approvalid).toBe("gate-1");
3438
- expect(pending.raise.params?.arguments).toContain("[redacted]");
3439
- expect(pending.raise.params?.arguments).not.toContain("hunter2");
3440
- expect((pending.decision.result as { state?: string }).state).toBe("pending");
3441
- const approved = approvalframes({
3442
- id: 9,
3443
- request: { ...gate, state: "approved" },
3444
- identity: { fingerprint: "fp-1", displayname: "Laptop agent" },
3445
- });
3446
- expect((approved.decision.result as { client?: string }).client).toBe("Laptop agent");
3447
- const refused = approvalframes({ id: 9, request: { ...gate, state: "refused" } });
3448
- expect(refused.decision.error?.code).toBe("consentrefused");
3449
- expect(refused.decision.error?.message).toMatch(/was refused/i);
3450
- const expired = approvalframes({ id: 9, request: { ...gate, state: "expired" } });
3451
- expect(expired.decision.error?.message).toMatch(/expired/i);
3452
- });
3453
-
3454
- it("builds the tls, allowlist and heartbeat reports", () => {
3455
- expect(tlsreport({ mode: "required", certificatefingerprint: "sha256:aa", verifiedat: now })).toEqual({
3456
- version: protocolversion,
3457
- mode: "required",
3458
- certificaterequired: true,
3459
- verified: true,
3460
- });
3461
- expect(tlsreport({ mode: "off" })).toEqual({
3462
- version: protocolversion,
3463
- mode: "off",
3464
- certificaterequired: false,
3465
- verified: false,
3466
- });
3467
- const entries = [
3468
- {
3469
- fingerprint: "aa11",
3470
- displayname: "Laptop agent",
3471
- namespaces: ["browser" as const, "memory" as const],
3472
- grantedat: now,
3473
- history: [
3474
- { at: now, actor: "user", change: "Granted." },
3475
- { at: now + 1, actor: "user", change: "Rescoped." },
3476
- ],
3477
- },
3478
- ];
3479
- const allow = allowlistreport(entries);
3480
- expect(allow.entries[0]?.namespaces).toEqual(["browser", "memory"]);
3481
- expect(allow.entries[0]?.grants).toBe(2);
3482
- const beats = heartbeatreport({
3483
- channels: [
3484
- { id: "c1", clientid: "client1", openedat: now, lastbeatat: now },
3485
- { id: "c2", clientid: "client2", openedat: now - 1000, lastbeatat: now - 500 },
3486
- ],
3487
- now,
3488
- idlewindow: 1000,
3489
- });
3490
- expect(beats.beats).toBe(1);
3491
- expect(beats.open).toBe(2);
3492
- expect(beats.dead).toBe(0);
3493
- expect(heartbeatreport({ channels: [], now }).open).toBe(0);
3494
- });
3495
- });
3496
-
3497
- describe("agent protocol part three envelopes", () => {
3498
- const now = 1_800_000_000_000;
3499
-
3500
- it("builds the event subscription frames and the event notification frames", () => {
3501
- const subscription = {
3502
- id: "sub1",
3503
- clientid: "client1",
3504
- kinds: ["callresult" as const, "progress" as const],
3505
- origin: "https://example.com",
3506
- createdat: now,
3507
- };
3508
- const frames = subscriptionframes({ id: 9, subscription });
3509
- expect(frames.subscribe.method).toBe("events/subscribe");
3510
- expect(frames.subscribe.params?.kinds).toEqual(["callresult", "progress"]);
3511
- expect(frames.subscribe.params?.origin).toBe("https://example.com");
3512
- expect(frames.unsubscribe.method).toBe("events/unsubscribe");
3513
- expect(frames.unsubscribe.params?.subscriptionid).toBe("sub1");
3514
- const notification = eventnotification({
3515
- subscriptionid: "sub1",
3516
- kind: "callresult",
3517
- origin: "https://example.com",
3518
- tool: "browser.readtext",
3519
- payload: { ok: true },
3520
- now,
3521
- });
3522
- expect(notification.method).toBe("events/notify");
3523
- expect(notification.params?.payload).toEqual({ ok: true });
3524
- expect(notification.id).toBeUndefined();
3525
- });
3526
-
3527
- it("builds the page state delta reports of the resource watchers", () => {
3528
- const report = resourcedeltareport({
3529
- watchid: "watch1",
3530
- clientid: "client1",
3531
- resource: "page",
3532
- delta: { url: "https://example.com/next" },
3533
- now,
3534
- });
3535
- expect(report).toMatchObject({
3536
- watchid: "watch1",
3537
- clientid: "client1",
3538
- resource: "page",
3539
- delta: { url: "https://example.com/next" },
3540
- });
3541
- expect(report.version).toBe(protocolversion);
3542
- });
3543
-
3544
- it("builds the sampling callback frames with the exact prompt payload", () => {
3545
- const pending = {
3546
- id: "sample1",
3547
- clientid: "client1",
3548
- prompt: "Summarize the run.",
3549
- pagecontent: "the page text",
3550
- state: "pending" as const,
3551
- requestedat: now,
3552
- };
3553
- const pendingframes = samplingframes({ id: 4, request: pending });
3554
- expect(pendingframes.request.method).toBe("sampling/request");
3555
- expect(pendingframes.request.params?.prompt).toBe("Summarize the run.");
3556
- expect(pendingframes.request.params?.pagecontent).toBe("the page text");
3557
- expect(pendingframes.answer.params?.state).toBe("pending");
3558
- const answered = samplingframes({
3559
- id: 5,
3560
- request: { ...pending, state: "answered", answeredat: now + 1, answer: "the model answer" },
3561
- });
3562
- expect(answered.answer.params?.answer).toBe("the model answer");
3563
- const refused = samplingframes({ id: 6, request: { ...pending, state: "refused", answeredat: now + 1 } });
3564
- expect(refused.answer.params?.refused).toBe(true);
3565
- });
3566
-
3567
- it("builds the prompt tool reports and the prompt call frames", () => {
3568
- const report = promptreport({
3569
- prompts: [
3570
- {
3571
- name: "runreview",
3572
- description: "Review the run.",
3573
- arguments: [{ name: "objective", description: "The plan objective.", required: true }],
3574
- template: "Review {{objective}}.",
3575
- },
3576
- ],
3577
- });
3578
- expect(report.prompts[0]?.name).toBe("runreview");
3579
- expect(report.prompts[0]?.template).toBe("Review {{objective}}.");
3580
- const call = promptcallframe({ id: 7, name: "runreview", args: { objective: "fill the form" } });
3581
- expect(call.method).toBe("prompts/call");
3582
- expect(call.params?.arguments).toEqual({ objective: "fill the form" });
3583
- });
3584
-
3585
- it("builds the stream chunk and progress notice frames", () => {
3586
- const chunk = streamchunkframe({ callid: "call1", seq: 2, content: "more text", done: false, at: now });
3587
- expect(chunk.method).toBe("calls/stream");
3588
- expect(chunk.params).toMatchObject({ callid: "call1", seq: 2, content: "more text", done: false });
3589
- const notice = progressnoticeframe({
3590
- callid: "call1",
3591
- percent: 60,
3592
- message: "Typing the reviewed field.",
3593
- cancellable: true,
3594
- at: now,
3595
- });
3596
- expect(notice.method).toBe("calls/progress");
3597
- expect(notice.params).toMatchObject({ percent: 60, cancellable: true });
3598
- });
3599
-
3600
- it("builds the cancellation frames with the preserved partial result", () => {
3601
- const frames = cancelframes({
3602
- id: 8,
3603
- frame: { callid: "call1", reason: "the user asked", at: now },
3604
- partial: { content: "the first chunks", iserror: false },
3605
- });
3606
- expect(frames.cancel.method).toBe("calls/cancel");
3607
- expect(frames.cancel.params?.reason).toBe("the user asked");
3608
- expect(frames.cancelled.result).toMatchObject({
3609
- cancelled: true,
3610
- callid: "call1",
3611
- partial: { content: "the first chunks" },
3612
- });
3613
- });
3614
-
3615
- it("builds the structured error, rate limit, idempotency, batch, call log, in flight, dry run and mock reports", () => {
3616
- const error = structurederrorreport({
3617
- error: { code: "ratelimited", message: "The budget is spent.", retryhint: "wait", retryafter: 4000 },
3618
- usage: { clientid: "client1", used: 5, budget: 5 },
3619
- });
3620
- expect(error).toMatchObject({ code: "ratelimited", retryhint: "wait", retryafter: 4000 });
3621
- expect(error.usage).toEqual({ clientid: "client1", used: 5, budget: 5 });
3622
- const limits = ratelimitreport({
3623
- limits: [
3624
- { clientid: "client1", windowms: 60_000, budget: 5, windowstartedat: now, used: 3 },
3625
- { clientid: "client2", windowms: 30_000, windowstartedat: now, used: 0 },
3626
- ],
3627
- now,
3628
- });
3629
- expect(limits.limits[0]).toMatchObject({ used: 3, unbounded: false });
3630
- expect(limits.limits[1]?.unbounded).toBe(true);
3631
- const replay = idempotencyreplayframe({
3632
- id: 11,
3633
- key: "key-1",
3634
- result: { content: "stored", iserror: false },
3635
- originalat: now,
3636
- });
3637
- expect(replay.result).toMatchObject({ replayed: true, idempotencykey: "key-1" });
3638
- const idempotency = idempotencyreport(
3639
- [
3640
- {
3641
- key: "key-1",
3642
- clientid: "client1",
3643
- tool: "browser.readtext",
3644
- result: { content: "stored", iserror: false },
3645
- createdat: now,
3646
- expiresat: now + 5000,
3647
- },
3648
- ],
3649
- now + 6000,
3650
- );
3651
- expect(idempotency.records[0]?.live).toBe(false);
3652
- const batch = batchreport({
3653
- batch: {
3654
- id: "batch1",
3655
- clientid: "client1",
3656
- calls: [
3657
- { id: "m1", name: "browser.readtext", params: {} },
3658
- { id: "m2", name: "memory.list", params: {} },
3659
- ],
3660
- stoponerror: true,
3661
- state: "stopped",
3662
- createdat: now,
3663
- finishedat: now + 100,
3664
- outcomes: [
3665
- { callid: "m1", tool: "browser.readtext", ok: true, at: now + 1 },
3666
- { callid: "m2", tool: "memory.list", ok: false, at: now + 2 },
3667
- ],
3668
- },
3669
- });
3670
- expect(batch).toMatchObject({ batchid: "batch1", state: "stopped", done: 2, total: 2 });
3671
- const log = calllogreport({
3672
- calls: [
3673
- {
3674
- id: "call1",
3675
- clientid: "client1",
3676
- tool: "browser.readtext",
3677
- origin: "https://example.com",
3678
- ok: true,
3679
- at: now,
3680
- idempotencykey: "key-1",
3681
- replayed: true,
3682
- },
3683
- ],
3684
- filters: { clientid: "client1" },
3685
- });
3686
- expect(log.calls[0]?.replayed).toBe(true);
3687
- expect(log.filters.clientid).toBe("client1");
3688
- const inflight = inflightreport({
3689
- contexts: [
3690
- {
3691
- callid: "call1",
3692
- clientid: "client1",
3693
- tool: "browser.readtext",
3694
- state: "inflight",
3695
- startedat: now,
3696
- chunks: 3,
3697
- dryrun: true,
3698
- },
3699
- ],
3700
- now: now + 5000,
3701
- });
3702
- expect(inflight.inflight[0]).toMatchObject({ chunks: 3, msopen: 5000, dryrun: true });
3703
- const dry = dryrunreport({
3704
- callid: "call1",
3705
- tool: "browser.click",
3706
- argsvalid: false,
3707
- consentok: true,
3708
- findings: ["The required argument stepid stays empty."],
3709
- executed: false,
3710
- mutations: [],
3711
- at: now,
3712
- });
3713
- expect(dry.findings[0]).toMatch(/stepid stays empty/);
3714
- const mocks = mockreport([
3715
- { tool: "browser.readtext", result: { content: "canned", iserror: false }, testcontext: true, createdat: now },
3716
- ]);
3717
- expect(mocks.mocks[0]).toMatchObject({ tool: "browser.readtext", testcontext: true });
3718
- });
3719
- });
3720
-
3721
- describe("protocol llm envelopes", () => {
3722
- const now = 1_800_000_000_000;
3723
-
3724
- it("carries a model drafted plan in the model proposal envelope for review", () => {
3725
- const draft: plandraft = {
3726
- id: "draft1",
3727
- goal: "Read the docs page",
3728
- steps: [
3729
- { id: "s1", kind: "navigate", value: "https://docs.example", summary: "Open the docs page." },
3730
- { id: "t1", kind: "reload", summary: "Reload.", freshreview: true },
3731
- ],
3732
- openquestions: ["which section?"],
3733
- providerid: "prov1",
3734
- model: "model-a",
3735
- state: "draft",
3736
- lintfindings: ["The drafted step s9 violates the action grammar."],
3737
- createdat: now,
3738
- };
3739
- const envelope = modelproposal({ draft });
3740
- expect(envelope.version).toBe(protocolversion);
3741
- expect(envelope.modelproposal.draftid).toBe("draft1");
3742
- expect(envelope.modelproposal.steps[1]?.freshreview).toBe(true);
3743
- expect(envelope.modelproposal.openquestions).toEqual(["which section?"]);
3744
- expect(envelope.modelproposal.lintfindings).toHaveLength(1);
3745
- expect(envelope.modelproposal.state).toBe("draft");
3746
- });
3747
-
3748
- it("reports the usage totals and guard verdicts in the model outcome envelope", () => {
3749
- const outputs: modeloutput[] = [
3750
- { raw: "{}", parsed: { intent: "navigate" }, verdict: "valid", attempts: 1 },
3751
- { raw: "nope", verdict: "invalid", reason: "The model answer is not json.", attempts: 3 },
3752
- ];
3753
- const envelope = modeloutcome({
3754
- runid: "run1",
3755
- outputs,
3756
- totals: { prompttokens: 10, completiontokens: 5, totaltokens: 15, cost: 0.02, calls: 2 },
3757
- });
3758
- expect(envelope.version).toBe(protocolversion);
3759
- expect(envelope.modeloutcome.runid).toBe("run1");
3760
- expect(envelope.modeloutcome.usage.totaltokens).toBe(15);
3761
- expect(envelope.modeloutcome.guards[1]).toMatchObject({ verdict: "invalid", attempts: 3 });
3762
- });
3763
- });
3764
-
3765
- describe("protocol swarm envelopes", () => {
3766
- const now = 1_800_000_000_000;
3767
-
3768
- it("carries the agents, the task queue and the mailboxes in the swarm state report", () => {
3769
- const state: swarmstate = {
3770
- agents: [
3771
- {
3772
- id: "a1",
3773
- name: "Scout",
3774
- role: "worker",
3775
- depth: 0,
3776
- state: "active",
3777
- tabid: 7,
3778
- registeredat: now,
3779
- heartbeatat: now,
3780
- },
3781
- {
3782
- id: "a2",
3783
- name: "Scout sub 1",
3784
- role: "planner",
3785
- depth: 1,
3786
- state: "paused",
3787
- parentid: "a1",
3788
- registeredat: now,
3789
- },
3790
- ],
3791
- queue: {
3792
- lanes: ["extraction"],
3793
- priorities: [1, 2],
3794
- completionpolicy: "all",
3795
- items: [
3796
- {
3797
- id: "t1",
3798
- lane: "extraction",
3799
- priority: 2,
3800
- payload: "Read the pricing table",
3801
- state: "claimed",
3802
- enqueuedat: now,
3803
- },
3804
- ],
3805
- claims: [{ agentid: "a1", taskid: "t1", claimedat: now, heartbeatat: now }],
3806
- },
3807
- mailboxes: [
3808
- {
3809
- agentid: "a1",
3810
- inbox: [{ id: "m1", senderid: "a2", recipient: "a1", routing: "direct", payload: "Ready.", sentat: now }],
3811
- outbox: [],
3812
- unread: 1,
3813
- },
3814
- ],
3815
- killswitch: { engaged: true, engagedat: now, reason: "The user halted the swarm." },
3816
- };
3817
- const report = swarmstatereport(state);
3818
- expect(report.version).toBe(protocolversion);
3819
- expect(report.swarm.agents[0]).toMatchObject({ id: "a1", name: "Scout", role: "worker", tabid: 7 });
3820
- expect(report.swarm.agents[1]?.parentid).toBe("a1");
3821
- expect(report.swarm.queue.lanes).toEqual(["extraction"]);
3822
- expect(report.swarm.queue.items[0]).toMatchObject({ payload: "Read the pricing table", state: "claimed" });
3823
- expect(report.swarm.queue.claims[0]).toMatchObject({ agentid: "a1", taskid: "t1" });
3824
- expect(report.swarm.mailboxes[0]).toMatchObject({ agentid: "a1", unread: 1, inbox: 1, outbox: 0 });
3825
- expect(report.swarm.killswitch).toMatchObject({ engaged: true, reason: "The user halted the swarm." });
3826
- });
3827
-
3828
- it("builds the agent event notification frame of one lifecycle change", () => {
3829
- const frame = agenteventframe({
3830
- id: "e1",
3831
- kind: "claimed",
3832
- agentid: "a1",
3833
- taskid: "t1",
3834
- summary: "The agent claimed the task.",
3835
- at: now,
3836
- });
3837
- expect(frame.method).toBe("agents/notify");
3838
- expect(frame.params).toMatchObject({
3839
- eventid: "e1",
3840
- kind: "claimed",
3841
- agentid: "a1",
3842
- taskid: "t1",
3843
- summary: "The agent claimed the task.",
3844
- at: now,
3845
- });
3846
- const bare = agenteventframe({ id: "e2", kind: "killall", summary: "The user halted the swarm.", at: now });
3847
- expect(bare.params).not.toHaveProperty("agentid");
3848
- expect(bare.params).not.toHaveProperty("taskid");
3849
- });
3850
- });
3851
-
3852
- describe("protocol multi agent part two envelopes", () => {
3853
- const now = 1_800_000_000_000;
3854
-
3855
- it("builds the boardstate snapshot for dashboards with every lane and milestone", () => {
3856
- const snapshot = boardstatesnapshot({
3857
- id: "board1",
3858
- builtat: now,
3859
- lanes: [
3860
- {
3861
- agentid: "a1",
3862
- name: "Scout",
3863
- role: "worker",
3864
- state: "active",
3865
- lane: "extraction",
3866
- currenttask: "Read the table",
3867
- milestones: [{ label: "Rows read", done: true, at: now }],
3868
- },
3869
- { agentid: "a2", name: "Scribe", role: "critic", state: "active", lane: "critic", milestones: [] },
3870
- ],
3871
- });
3872
- expect(snapshot.version).toBe(protocolversion);
3873
- expect(snapshot.board.lanes).toHaveLength(2);
3874
- expect(snapshot.board.lanes[0]).toMatchObject({ agentid: "a1", lane: "extraction", currenttask: "Read the table" });
3875
- expect(snapshot.board.lanes[0]?.milestones[0]).toMatchObject({ label: "Rows read", done: true, at: now });
3876
- expect(snapshot.board.lanes[1]?.currenttask).toBeUndefined();
3877
- });
3878
-
3879
- it("builds the handoff frame that pushes one tab handoff state", () => {
3880
- const frame = handoffframe({
3881
- id: "h1",
3882
- fromagentid: "a1",
3883
- toagentid: "a2",
3884
- tabid: 7,
3885
- taskstate: "Halfway through the footer.",
3886
- state: "transferred",
3887
- createdat: now,
3888
- transferredat: now + 1,
3889
- });
3890
- expect(frame.method).toBe("agents/handoff");
3891
- expect(frame.params).toMatchObject({
3892
- id: "h1",
3893
- from: "a1",
3894
- to: "a2",
3895
- tabid: 7,
3896
- taskstate: "Halfway through the footer.",
3897
- state: "transferred",
3898
- transferredat: now + 1,
3899
- });
3900
- const resumed = handoffframe({
3901
- id: "h1",
3902
- fromagentid: "a1",
3903
- toagentid: "a2",
3904
- taskstate: "Halfway.",
3905
- state: "resumed",
3906
- createdat: now,
3907
- transferredat: now + 1,
3908
- resumedat: now + 2,
3909
- });
3910
- expect(resumed.params).toMatchObject({ state: "resumed", resumedat: now + 2 });
3911
- const bare = handoffframe({
3912
- id: "h2",
3913
- fromagentid: "a1",
3914
- toagentid: "a2",
3915
- taskstate: "State.",
3916
- state: "prepared",
3917
- createdat: now,
3918
- });
3919
- expect(bare.params).not.toHaveProperty("transferredat");
3920
- expect(bare.params).not.toHaveProperty("resumedat");
3921
- });
3922
-
3923
- it("builds the review frame with the routed request and its optional critic verdict", () => {
3924
- const frame = reviewframe({
3925
- request: {
3926
- id: "r1",
3927
- fromagentid: "a1",
3928
- toagentid: "a2",
3929
- subject: "The extraction output",
3930
- payload: "The rows",
3931
- state: "answered",
3932
- requestedat: now,
3933
- ackedat: now + 1,
3934
- answeredat: now + 2,
3935
- },
3936
- review: {
3937
- id: "r1",
3938
- reviewerid: "a2",
3939
- subjectagentid: "a1",
3940
- verdict: "changes",
3941
- issues: ["One stale row."],
3942
- requiredchanges: ["Re-read the footer."],
3943
- reviewedat: now + 2,
3944
- },
3945
- });
3946
- expect(frame.method).toBe("agents/review");
3947
- expect(frame.params).toMatchObject({
3948
- id: "r1",
3949
- from: "a1",
3950
- to: "a2",
3951
- subject: "The extraction output",
3952
- state: "answered",
3953
- verdict: "changes",
3954
- });
3955
- expect(frame.params).toMatchObject({ issues: ["One stale row."], requiredchanges: ["Re-read the footer."] });
3956
- const bare = reviewframe({
3957
- request: {
3958
- id: "r2",
3959
- fromagentid: "a1",
3960
- toagentid: "a2",
3961
- subject: "s",
3962
- payload: "p",
3963
- state: "open",
3964
- requestedat: now,
3965
- },
3966
- });
3967
- expect(bare.params).not.toHaveProperty("verdict");
3968
- expect(bare.params).not.toHaveProperty("ackedat");
3969
- });
3970
- });
3971
-
3972
- describe("execution environment envelopes", () => {
3973
- it("documents the environment grammar of every reviewed action kind", () => {
3974
- const grammar = environmentgrammar();
3975
- expect(grammar.version).toBe(protocolversion);
3976
- expect(grammar.kinds.length).toBeGreaterThan(330);
3977
- expect(grammar.kinds.find((entry) => entry.kind === "evaluate")?.environments).toEqual(["isolatedworld"]);
3978
- expect(grammar.kinds.find((entry) => entry.kind === "readhtml")?.environments).toEqual([
3979
- "pagecontext",
3980
- "offscreenworker",
3981
- ]);
3982
- expect(grammar.notes.join(" ")).toMatch(/isolated world/);
3983
- expect(grammar.notes.join(" ")).toMatch(/sandboxframe/);
3984
- expect(grammar.notes.join(" ")).toMatch(/bypasses the human review/);
3985
- });
3986
-
3987
- it("wraps the environment view with the step environments, turnarounds, offscreen registry, workers and keepalive", () => {
3988
- const report = environmentreport({
3989
- environments: { s1: "pagecontext", s2: "offscreenworker" },
3990
- turnarounds: { s2: 42 },
3991
- offscreen: [
3992
- {
3993
- document: "offscreen.html",
3994
- runid: "run1",
3995
- reasons: ["DOM_PARSER", "WORKERS"],
3996
- justification: "Heavy parsing of reviewed snapshots.",
3997
- createdat: 1,
3998
- },
3999
- ],
4000
- workers: 3,
4001
- keepalive: { runid: "run1", state: "active", beats: 5, lastbeatat: 99, portopen: true },
4002
- });
4003
- expect(report.version).toBe(protocolversion);
4004
- expect(report.environments).toEqual([
4005
- { stepid: "s1", environment: "pagecontext" },
4006
- { stepid: "s2", environment: "offscreenworker" },
4007
- ]);
4008
- expect(report.turnarounds).toEqual([{ stepid: "s2", milliseconds: 42 }]);
4009
- expect(report.offscreen[0]?.reasons).toEqual(["DOM_PARSER", "WORKERS"]);
4010
- expect(report.workers).toBe(3);
4011
- expect(report.keepalive).toMatchObject({ runid: "run1", beats: 5, portopen: true });
4012
- const empty = environmentreport({ environments: {} });
4013
- expect(empty.environments).toEqual([]);
4014
- expect(empty.turnarounds).toEqual([]);
4015
- expect(empty.offscreen).toEqual([]);
4016
- expect(empty.workers).toBe(0);
4017
- expect(empty.keepalive).toBeUndefined();
4018
- });
4019
-
4020
- it("returns the execution environment beside the step outcome in the response envelope", () => {
4021
- const plan = {
4022
- id: "p1",
4023
- objective: "o",
4024
- origin: "https://example.com",
4025
- steps: [],
4026
- createdat: 1,
4027
- expiresat: 2,
4028
- state: "approved" as const,
4029
- };
4030
- const envelope = JSON.parse(
4031
- outcomeresponse({
4032
- outcome: { stepid: "s1", ok: true, summary: "Done.", environment: "offscreenworker", at: 1 },
4033
- plan,
4034
- }),
4035
- ) as { outcome: { environment?: string } };
4036
- expect(envelope.outcome.environment).toBe("offscreenworker");
4037
- });
4038
- });
4039
-
4040
- describe("security envelopes", () => {
4041
- it("documents the consent model grammar with the denydefault posture and the four sensitive classes", () => {
4042
- const model = consentmodel();
4043
- expect(model.version).toBe(protocolversion);
4044
- expect(model.posture).toBe("denydefault");
4045
- expect(model.sensitiveclasses).toEqual(["payment", "credential", "delete", "publish"]);
4046
- expect(model.maskshapes).toEqual(["password", "token", "card", "secret"]);
4047
- expect(model.notes.join(" ")).toMatch(/no wildcard expansion/i);
4048
- expect(model.notes.join(" ")).toMatch(/defaults to unlimited/i);
4049
- expect(model.notes.join(" ")).toMatch(/revokerun is a terminal session event/i);
4050
- expect(model.notes.join(" ")).toMatch(/loghash/i);
4051
- });
4052
-
4053
- it("wraps the security view in the versioned response envelope", () => {
4054
- const envelope = securityreport({
4055
- allowlist: [{ origin: "https://example.com", profileid: "default", grantedat: 1 }],
4056
- profiles: [],
4057
- windows: [
4058
- {
4059
- id: "w1",
4060
- sessionid: "s1",
4061
- origin: "https://example.com",
4062
- startedat: 1,
4063
- duration: 60_000,
4064
- expiresat: 61_000,
4065
- boundary: "60000 milliseconds the user chose",
4066
- kinds: [],
4067
- state: "active",
4068
- },
4069
- ],
4070
- consents: [{ id: "c1", origin: "https://example.com", sensitiveclass: "payment", grantedat: 1 }],
4071
- revocations: [],
4072
- maskrules: [],
4073
- chain: [{ runid: "run1", valid: true, entries: 2, reason: "The hash chain verifies." }],
4074
- });
4075
- expect(envelope.version).toBe(protocolversion);
4076
- expect(envelope.posture).toBe("denydefault");
4077
- expect(envelope.allowlist).toHaveLength(1);
4078
- expect(envelope.windows[0]?.state).toBe("active");
4079
- expect(envelope.chain[0]?.valid).toBe(true);
4080
- });
4081
-
4082
- it("wraps one run log chain verification with the seal hash when the log sealed", () => {
4083
- const openchain = logchainreport({ runid: "run1", valid: true, entries: 3, reason: "The hash chain verifies." });
4084
- expect(openchain.version).toBe(protocolversion);
4085
- expect(openchain.sealhash).toBeUndefined();
4086
- const sealed = logchainreport({
4087
- runid: "run2",
4088
- valid: false,
4089
- entries: 3,
4090
- brokenat: 1,
4091
- reason: "The chain reports tamper evidence.",
4092
- });
4093
- expect(sealed.brokenat).toBe(1);
4094
- expect(sealed.valid).toBe(false);
4095
- });
4096
- });
4097
-
4098
- import { transparencyreport } from "../protocol.js";
4099
-
4100
- describe("protocol transparency", () => {
4101
- it("serves the transparencypage data in one versioned envelope with the denydefault posture", () => {
4102
- const report = transparencyreport({
4103
- grants: [
4104
- {
4105
- origin: "https://example.com",
4106
- scope: "automation allowlist",
4107
- boundary: "the user revokes the entry",
4108
- grantedat: 1,
4109
- },
4110
- ],
4111
- windows: [
4112
- {
4113
- id: "w",
4114
- origin: "https://example.com",
4115
- state: "closed",
4116
- boundary: "1000 milliseconds the user chose",
4117
- startedat: 1,
4118
- expiresat: 2,
4119
- },
4120
- ],
4121
- connectallow: [{ senderid: "sender", displayname: "Bridge", addedat: 1 }],
4122
- permdiffs: [
4123
- { fromversion: "1.1.61", toversion: "1.1.62", added: ["optional:downloads"], removed: [], computedat: 1 },
4124
- ],
4125
- safedefaults: [{ origin: "https://first.example", firstseenat: 1 }],
4126
- vault: [{ vaultid: "v", label: "Bank login", scope: "https://bank.example", provenance: "user", createdat: 1 }],
4127
- });
4128
- expect(report.version).toBe(consentmodel().version);
4129
- expect(report.posture).toBe("denydefault");
4130
- expect(report.grants[0]?.origin).toBe("https://example.com");
4131
- expect(report.permdiffs[0]?.added).toEqual(["optional:downloads"]);
4132
- expect(report.vault[0]?.label).toBe("Bank login");
4133
- });
4134
-
4135
- it("wraps one interface surface snapshot in the versioned envelope", () => {
4136
- const report = surfacesnapshot({
4137
- surface: "sidepanel",
4138
- palette: [
4139
- {
4140
- entry: {
4141
- id: "starttask",
4142
- label: "Start task",
4143
- keywords: ["task"],
4144
- action: { command: "starttask", surface: "popup" },
4145
- },
4146
- score: 100,
4147
- reason: "The query matches the starttask command exactly.",
4148
- },
4149
- ],
4150
- timeline: [{ stepid: "s1", kind: "readtable", status: "running", active: true, anchor: "#step-s1" }],
4151
- logstream: {
4152
- events: [
4153
- {
4154
- id: "e1",
4155
- level: "info",
4156
- source: "background",
4157
- origin: "https://example.com",
4158
- summary: "The session started.",
4159
- masked: false,
4160
- maskverdict: "none",
4161
- at: 1,
4162
- },
4163
- ],
4164
- chainvalid: true,
4165
- reason: "The logstream chain of 1 event verifies link by link.",
4166
- },
4167
- plancards: [
4168
- {
4169
- risk: "sensitive",
4170
- cards: [
4171
- {
4172
- stepid: "s3",
4173
- kind: "type",
4174
- risk: "sensitive",
4175
- environment: "pagecontext",
4176
- options: "",
4177
- summary: "Type the card.",
4178
- corrections: [],
4179
- editable: true,
4180
- },
4181
- ],
4182
- expanded: true,
4183
- },
4184
- ],
4185
- onboarding: { stepscompleted: ["origingrants"], done: false },
4186
- });
4187
- expect(report.version).toBe(consentmodel().version);
4188
- expect(report.surface).toBe("sidepanel");
4189
- expect(report.palette[0]?.entry.id).toBe("starttask");
4190
- expect(report.timeline[0]?.status).toBe("running");
4191
- expect(report.logstream.chainvalid).toBe(true);
4192
- expect(report.plancards[0]?.risk).toBe("sensitive");
4193
- expect(report.onboarding?.done).toBe(false);
4194
- });
4195
-
4196
- it("wraps the finishing interface snapshot in the versioned envelope", () => {
4197
- const report = interfaceviews({
4198
- exportmenu: [{ format: "csv", scope: "run", destination: "download" }],
4199
- badge: { state: "attention", waitingcount: 2, runid: "run-1" },
4200
- recenttray: [
4201
- {
4202
- runid: "run-1",
4203
- origin: "https://example.com",
4204
- outcome: "halted",
4205
- title: "Pricing pass",
4206
- at: 1,
4207
- resumable: true,
4208
- reopenable: false,
4209
- },
4210
- ],
4211
- toasts: { live: [{ id: "t-1", stepid: "s-1", kind: "click", durationms: 40, at: 1 }], total: 3 },
4212
- appearance: { mode: "dark", tokens: { surface: "#1f1f1f" }, source: "os" },
4213
- });
4214
- expect(report.version).toBe(protocolversion);
4215
- expect(report.badge.state).toBe("attention");
4216
- expect(report.recenttray[0]?.resumable).toBe(true);
4217
- expect(report.toasts.total).toBe(3);
4218
- expect(report.appearance.source).toBe("os");
4219
- expect(report.datagrid).toBeUndefined();
4220
- });
4221
-
4222
- it("wraps the ecosystem snapshot in the versioned envelope", () => {
4223
- const report = ecosystemviews({
4224
- library: [
4225
- {
4226
- id: "invoice@1.0.0",
4227
- title: "Invoice digest",
4228
- publisher: "example publisher",
4229
- version: "1.0.0",
4230
- grants: ["https://example.com"],
4231
- sensitive: false,
4232
- state: "available",
4233
- registry: "https://registry.example.test",
4234
- },
4235
- ],
4236
- installed: [{ id: "invoice@1.0.0", title: "Invoice digest", version: "1.0.0", forkable: true }],
4237
- syncbridge: {
4238
- hooks: [
4239
- {
4240
- id: "sync:file:1",
4241
- provider: "file",
4242
- direction: "both",
4243
- optin: false,
4244
- endpoint: "manifests.json",
4245
- state: "idle",
4246
- },
4247
- ],
4248
- conflicts: [{ id: "conflict-1", manifestid: "invoice", localversion: "1.0.0", remoteversion: "2.0.0" }],
4249
- },
4250
- attention: [
4251
- {
4252
- id: "attention:gatewait:run1:g1",
4253
- cause: "gatewait",
4254
- severity: "critical",
4255
- runid: "run1",
4256
- summary: "A gate waits for one human action.",
4257
- deeplink: "devthink://gate/g1?run=run1",
4258
- at: 1,
4259
- },
4260
- ],
4261
- backgroundruns: [
4262
- {
4263
- id: "background:wf:1",
4264
- workflowid: "wf",
4265
- state: "running",
4266
- keepaliveheld: true,
4267
- progress: "Running with the keepalive signal held",
4268
- },
4269
- ],
4270
- replay: {
4271
- runid: "run1",
4272
- cursor: 1,
4273
- playing: false,
4274
- steps: [
4275
- { stepid: "s1", index: 0, summary: "completed" },
4276
- { stepid: "s2", index: 1, summary: "waited at a gate" },
4277
- ],
4278
- restored: { stepid: "s2", summary: "The gate resolved after a human action.", captureid: "c-99" },
4279
- },
4280
- compare: {
4281
- runids: ["runa", "runb"],
4282
- metrics: ["agreement", "divergence", "durationdelta"],
4283
- firstdivergence: 1,
4284
- steps: [
4285
- {
4286
- stepid: "s2",
4287
- agreement: "diverge",
4288
- summarya: "clicked",
4289
- summaryb: "clicked twice",
4290
- durationdelta: -100,
4291
- highlighted: true,
4292
- },
4293
- ],
4294
- },
4295
- });
4296
- expect(report.version).toBe(protocolversion);
4297
- expect(report.library[0]?.publisher).toBe("example publisher");
4298
- expect(report.installed[0]?.forkable).toBe(true);
4299
- expect(report.syncbridge.hooks[0]?.optin).toBe(false);
4300
- expect(report.syncbridge.conflicts[0]?.remoteversion).toBe("2.0.0");
4301
- expect(report.attention[0]?.deeplink).toBe("devthink://gate/g1?run=run1");
4302
- expect(report.backgroundruns[0]?.keepaliveheld).toBe(true);
4303
- expect(report.replay?.restored.captureid).toBe("c-99");
4304
- expect(report.compare?.steps[0]?.highlighted).toBe(true);
4305
- });
4306
- });
4307
-
4308
- /* ── The protocolv2 api freeze of the 1.1.91 release. ── */
4309
- import { readFile, readdir } from "node:fs/promises";
4310
- import {
4311
- errorcodetable,
4312
- framingrules,
4313
- frozenmessagecatalog,
4314
- frozenmessagecarriers,
4315
- memoryitemframe,
4316
- responseenvelopeoutcomes,
4317
- stabilityrules,
4318
- } from "../protocol.js";
4319
- import { initialize, negotiate, handleframe, servercapabilities } from "../mcp.js";
4320
- import { buildtoolcatalog } from "../tools.js";
4321
- import { defaultmcpconfig } from "../mcp.js";
4322
- import { deprecationwindow, protocolsupported } from "../apifreeze.js";
4323
- import type { agentsession, clientrecord, jsonrpcframe, toolcatalog } from "../types.js";
4324
-
4325
- const freezenow = 1_800_000_000_000;
4326
- const freezeconfig = { ...defaultmcpconfig(), enabled: true };
4327
- const freezecatalog: toolcatalog = buildtoolcatalog();
4328
- const freezesession: agentsession = {
4329
- id: "sess",
4330
- tabid: 7,
4331
- origin: "https://example.com",
4332
- startedat: freezenow - 5000,
4333
- expiresat: freezenow + 600_000,
4334
- grants: ["https://example.com"],
4335
- };
4336
- const freezeplan: agentplan = {
4337
- id: "plan1",
4338
- objective: "Validate the frozen contracts",
4339
- origin: "https://example.com",
4340
- steps: [{ id: "s1", kind: "observe", summary: "Snapshot the tab", risk: "read" }],
4341
- createdat: freezenow - 4000,
4342
- expiresat: freezenow + 600_000,
4343
- state: "approved",
4344
- };
4345
- const v2client: clientrecord = {
4346
- id: "v2client",
4347
- transport: "stdio",
4348
- paired: true,
4349
- connectedat: freezenow - 1000,
4350
- pairedat: freezenow - 500,
4351
- capabilities: {
4352
- protocolversion: "2.0.0",
4353
- name: "v2client",
4354
- version: "2",
4355
- toolversion: freezecatalog.version,
4356
- tools: 0,
4357
- namespaces: ["browser"],
4358
- transports: ["stdio"],
4359
- },
4360
- };
4361
- const v1client: clientrecord = {
4362
- ...v2client,
4363
- id: "v1client",
4364
- capabilities: {
4365
- protocolversion: "1.1.54",
4366
- name: "v1client",
4367
- version: "1",
4368
- toolversion: freezecatalog.version,
4369
- tools: 0,
4370
- namespaces: ["browser"],
4371
- transports: ["stdio"],
4372
- },
4373
- };
4374
-
4375
- /** Builds one json rpc request frame for the freeze tests. */
4376
- function freezeframe(id: number | string, method: string, params?: Record<string, unknown>): jsonrpcframe {
4377
- return { jsonrpc: "2.0", id, method, ...(params !== undefined ? { params } : {}) };
4378
- }
4379
-
4380
- describe("the protocolv2 frozen contract of 1.1.91", () => {
4381
- it("validates every frozen message against its schema file", async () => {
4382
- if (!existsSync("dist/schemas"))
4383
- return; /* the schema artifacts ride the build: the pass runs after pnpm build in the validate chain and the ci lanes */
4384
- const schemafiles = (await readdir("dist/schemas")).filter((file) => file.endsWith(".json")).sort();
4385
- expect(schemafiles).toHaveLength(10);
4386
- const catalogschemas = new Set(frozenmessagecatalog.map((entry) => entry.schema));
4387
- for (const file of schemafiles) expect(catalogschemas.has(`schemas/${file}`)).toBe(true);
4388
- for (const entry of frozenmessagecatalog) {
4389
- const schema = JSON.parse(await readFile(`dist/${entry.schema}`, "utf8")) as {
4390
- $id?: string;
4391
- version?: string;
4392
- title?: string;
4393
- description?: string;
4394
- };
4395
- expect(schema.$id).toContain("protocolv2");
4396
- expect(schema.version).toMatch(/^\d+\.\d+\.\d+$/);
4397
- expect(schema.description?.toLowerCase()).toContain(entry.family);
4398
- expect(frozenmessagecarriers(entry).length).toBeGreaterThan(0);
4399
- expect(frozenmessagecarriers(entry).join(",")).not.toContain("nowhere");
4400
- }
4401
- });
4402
-
4403
- it("wraps concrete messages in the versioned envelope their schema freezes", async () => {
4404
- if (!existsSync("dist/schemas"))
4405
- return; /* the schema artifacts ride the build: the pass runs after pnpm build in the validate chain and the ci lanes, and a lane that skipped the build asserts the frozen catalog above and leaves the envelope walk to the chain that builds first */
4406
- const planschema = JSON.parse(await readFile("dist/schemas/plan.schema.json", "utf8")) as {
4407
- plan?: { properties?: Record<string, { type?: string }> };
4408
- };
4409
- const proposal = parseproposal(
4410
- {
4411
- version: protocolversion,
4412
- plan: {
4413
- objective: "Read the page",
4414
- steps: [{ kind: "observe", summary: "Capture an approved page snapshot" }],
4415
- },
4416
- },
4417
- "https://example.com",
4418
- );
4419
- for (const field of ["id", "objective", "origin", "steps", "createdat", "expiresat", "state"])
4420
- expect(planschema.plan?.properties?.[field]).toBeDefined();
4421
- expect(planschema.plan?.properties?.createdat?.type).toBe("number");
4422
- expect(proposal.version).toBe(protocolversion);
4423
- expect(proposal.plan.state).toBe("pending");
4424
- const memoryschema = JSON.parse(await readFile("dist/schemas/memory.schema.json", "utf8")) as {
4425
- provenancefields?: { fields?: string[] };
4426
- };
4427
- const provenance = { origin: "https://example.com", runid: "run1", stepid: "step1", capturedat: 1 };
4428
- for (const field of memoryschema.provenancefields?.fields ?? []) expect(provenance).toHaveProperty(field);
4429
- const item = memoryitemframe({ version: protocolversion, key: "k", value: 1, provenance });
4430
- expect(item.provenance.runid).toBe("run1");
4431
- const envelopeschema = JSON.parse(await readFile("dist/schemas/envelope.schema.json", "utf8")) as {
4432
- outcomes?: { values?: string[] };
4433
- errorcodetable?: { items?: Array<{ code?: string; retry?: string }> };
4434
- };
4435
- expect(envelopeschema.outcomes?.values).toEqual(["success", "error", "cancel"]);
4436
- expect(responseenvelopeoutcomes).toEqual(["success", "error", "cancel"]);
4437
- const envelope = JSON.parse(
4438
- outcomeresponse({ outcome: { stepid: "step", ok: true, summary: "Done.", at: 5 }, plan: freezeplan }),
4439
- );
4440
- expect(envelope.version).toBe(protocolversion);
4441
- for (const entry of errorcodetable)
4442
- expect(
4443
- envelopeschema.errorcodetable?.items?.some((item) => item.code === entry.code && item.retry === entry.retry),
4444
- ).toBe(true);
4445
- for (const entry of errorcodetable) expect(["never", "immediate", "afterbackoff"]).toContain(entry.retry);
4446
- expect(framingrules.stdio).toContain("newline");
4447
- expect(framingrules.http).toContain("http post");
4448
- expect(stabilityrules.additive).toContain("additive");
4449
- expect(stabilityrules.breaking).toContain("new major protocol version");
4450
- });
4451
-
4452
- it("covers version negotiation for protocol one and two clients", () => {
4453
- const server = servercapabilities({ config: freezeconfig, catalog: freezecatalog });
4454
- const handshake = initialize({ config: freezeconfig, catalog: freezecatalog });
4455
- expect(handshake.protocolmajor).toBe(2);
4456
- expect(handshake.protocolversion).toBe(protocolversion);
4457
- const undeclared = negotiate({ server });
4458
- expect(undeclared.agreed).toBe(true);
4459
- expect(undeclared.protocolmajor).toBe(2);
4460
- expect(undeclared.deprecation).toBeUndefined();
4461
- /* the 2.0.0 sunset closed the window: the deprecated full version string the version one clients exchanged parses nowhere, so a client that still sends it answers the frozen default of two without a deprecation notice */
4462
- const stringform = negotiate({ client: { protocolversion: "1.1.54" }, server });
4463
- expect(stringform.agreed).toBe(true);
4464
- expect(stringform.protocolmajor).toBe(2);
4465
- expect(stringform.deprecation).toBeUndefined();
4466
- /* the numeric declaration is the frozen form: a major one declaration refuses below the supported floor with the conversion path inside the mismatch */
4467
- const legacy = negotiate({ client: { protocolmajor: 1 }, server });
4468
- expect(legacy.agreed).toBe(false);
4469
- expect(legacy.mismatch).toContain("below the supported floor");
4470
- expect(legacy.mismatch).toContain("migrateplan");
4471
- expect(legacy.mismatch).toContain("docs/migrationguide.md");
4472
- const frozen = negotiate({ client: { protocolmajor: 2 }, server });
4473
- expect(frozen.agreed).toBe(true);
4474
- expect(frozen.protocolmajor).toBe(2);
4475
- expect(frozen.deprecation).toBeUndefined();
4476
- const future = negotiate({ client: { protocolmajor: 3 }, server });
4477
- expect(future.agreed).toBe(false);
4478
- expect(future.mismatch).toContain("the supported protocol versions are 2 through 2");
4479
- expect(protocolsupported).toEqual({ minimum: 2, maximum: 2 });
4480
- expect(deprecationwindow).toEqual({ opens: "1.1.91", closes: "2.0.0" });
4481
- });
4482
-
4483
- it("answers the strict unknown field refusal for every client after the sunset", async () => {
4484
- const strictframe = { ...freezeframe(21, "ping"), mysteryfield: true };
4485
- const strict = await handleframe({
4486
- frame: strictframe as jsonrpcframe,
4487
- client: v2client,
4488
- catalog: freezecatalog,
4489
- config: freezeconfig,
4490
- session: freezesession,
4491
- plan: freezeplan,
4492
- origin: freezesession.origin,
4493
- tabid: freezesession.tabid,
4494
- now: freezenow,
4495
- execute: async (step) => ({ content: `ran ${step.kind}`, iserror: false }),
4496
- });
4497
- expect(strict.error?.code).toBe("params");
4498
- expect(strict.error?.message).toContain("Strict schema validation");
4499
- /* the version one client that still sends the deprecated protocolversion string parses to the frozen default of two, so its unknown fields meet the same strict refusal the 2.0.0 sunset answers every major with */
4500
- const sunset = await handleframe({
4501
- frame: strictframe as jsonrpcframe,
4502
- client: v1client,
4503
- catalog: freezecatalog,
4504
- config: freezeconfig,
4505
- session: freezesession,
4506
- plan: freezeplan,
4507
- origin: freezesession.origin,
4508
- tabid: freezesession.tabid,
4509
- now: freezenow,
4510
- execute: async (step) => ({ content: `ran ${step.kind}`, iserror: false }),
4511
- });
4512
- expect(sunset.error?.code).toBe("params");
4513
- expect(sunset.error?.message).toContain("Strict schema validation");
4514
- });
4515
- });
4516
-
4517
- /* ── The 1.1.95 quarantine review flow messages of the security hardening release. ── */
4518
-
4519
- import { quarantineverdictreport } from "../protocol.js";
4520
- import { enforcequarantine, exportall, inventoryentry, purgeonrequest } from "../export.js";
4521
-
4522
- describe("the quarantine review flow messages", () => {
4523
- it("wraps the verdict report of a held quarantine entry in the versioned envelope", () => {
4524
- const held = enforcequarantine({
4525
- path: "/sandbox/invoice.pdf",
4526
- reason: "download moved out of the downloads folder",
4527
- digest: `${"a".repeat(64)}`,
4528
- now: 1000,
4529
- });
4530
- expect(held.action).toBe("hold");
4531
- expect(held.entry.digest).toBe("a".repeat(64));
4532
- const report = quarantineverdictreport({
4533
- entry: {
4534
- id: held.entry.id,
4535
- path: held.entry.path,
4536
- reason: held.entry.reason,
4537
- scan: held.entry.scan,
4538
- status: held.entry.status ?? "held",
4539
- at: held.entry.at,
4540
- updatedat: held.entry.updatedat,
4541
- },
4542
- });
4543
- expect(report.version).toBe(protocolversion);
4544
- expect(report.scan).toBe("pending");
4545
- expect(report.status).toBe("held");
4546
- expect(report.released).toBe(false);
4547
- });
4548
-
4549
- it("releases only a clean verdict and never reports a held or flagged file as released", () => {
4550
- const clean = enforcequarantine({ path: "/sandbox/invoice.pdf", reason: "clean scan", scan: "clean", now: 2000 });
4551
- const cleanreport = quarantineverdictreport({
4552
- entry: {
4553
- id: clean.entry.id,
4554
- path: clean.entry.path,
4555
- reason: clean.entry.reason,
4556
- scan: clean.entry.scan,
4557
- status: clean.entry.status ?? "held",
4558
- at: clean.entry.at,
4559
- updatedat: clean.entry.updatedat,
4560
- },
4561
- });
4562
- expect(cleanreport.released).toBe(true);
4563
- expect(cleanreport.scan).toBe("clean");
4564
- const flagged = enforcequarantine({
4565
- path: "/sandbox/invoice.pdf",
4566
- reason: "flagged scan",
4567
- scan: "flagged",
4568
- now: 3000,
4569
- });
4570
- expect(flagged.action).toBe("delete");
4571
- expect(() =>
4572
- quarantineverdictreport({
4573
- entry: {
4574
- id: flagged.entry.id,
4575
- path: flagged.entry.path,
4576
- reason: flagged.entry.reason,
4577
- scan: "flagged",
4578
- status: "released",
4579
- at: flagged.entry.at,
4580
- updatedat: flagged.entry.updatedat,
4581
- },
4582
- }),
4583
- ).toThrow("Only a clean scanner verdict releases a quarantined file");
4584
- });
4585
-
4586
- it("carries the quarantine entries with their digests through the session context report", () => {
4587
- const held = enforcequarantine({
4588
- path: "/sandbox/report.pdf",
4589
- reason: "quarantine step",
4590
- digest: `${"b".repeat(64)}`,
4591
- now: 1000,
4592
- });
4593
- const context = quarantinereport({ entries: [held.entry] });
4594
- expect(context.version).toBe(protocolversion);
4595
- expect(context.entries[0]?.digest).toBe("b".repeat(64));
4596
- expect(context.entries[0]?.status).toBe("held");
4597
- });
4598
- });
4599
-
4600
- /* ── The 1.1.95 purge and export completeness messages of the minimization family. ── */
4601
-
4602
- describe("the purge and export completeness messages", () => {
4603
- it("reports the full inventory scope of a purge with the audit hashes preserved", () => {
4604
- const now = 4000;
4605
- const inventory = ["runs", "memoryitems", "captures", "provlog"].map((key) =>
4606
- inventoryentry({
4607
- key,
4608
- dataclass: key === "memoryitems" ? "memory" : key === "provlog" ? "provenance" : key,
4609
- value: [{}],
4610
- now,
4611
- }),
4612
- );
4613
- const purge = purgeonrequest({
4614
- policy: { scope: ["all"], confirmation: "delete everything", at: now },
4615
- typed: "delete everything",
4616
- inventory,
4617
- now,
4618
- });
4619
- expect(purge.deleted.sort()).toEqual(["captures", "memoryitems", "provlog", "runs"].sort());
4620
- expect(purge.audithashespreserved).toBe(true);
4621
- });
4622
-
4623
- it("returns every stored record inside the one exportall archive bundle", () => {
4624
- const bundle = exportall({
4625
- runs: ["run-one", "run-two"],
4626
- memory: ["memory-one"],
4627
- captures: ["capture-one"],
4628
- settings: true,
4629
- provenance: ["prov-one"],
4630
- now: 5000,
4631
- });
4632
- expect(bundle.records).toBe(6);
4633
- expect(bundle.runs).toEqual(["run-one", "run-two"]);
4634
- expect(bundle.memory).toEqual(["memory-one"]);
4635
- expect(bundle.captures).toEqual(["capture-one"]);
4636
- expect(bundle.settings).toBe(true);
4637
- expect(bundle.provenance).toEqual(["prov-one"]);
4638
- });
4639
- });
4640
-
4641
- /* ── The 1.1.96 multi agent certification: the coordination messages against the frozen schemas. ── */
4642
- import {
4643
- aggregationreport,
4644
- consensusreport,
4645
- costledgerreport,
4646
- lessonreport,
4647
- parsespawnrequest,
4648
- spawnreply,
4649
- } from "../protocol.js";
4650
- import type { agentrecord, consensusrecord, aggregaterecord, costentry, lessonrecord, spawnrecord } from "../types.js";
4651
-
4652
- describe("protocol coordination messages", () => {
4653
- const now = 1_800_000_000_000;
4654
-
4655
- it("wraps the consensus round with every vote, the tally, the quorum and the outcome", () => {
4656
- const record: consensusrecord = {
4657
- id: "con1",
4658
- proposal: "Merge the two extractions of the pricing table.",
4659
- votes: [
4660
- { agentid: "w1", vote: "yes", castat: now },
4661
- { agentid: "w2", vote: "yes", castat: now + 1 },
4662
- { agentid: "c1", vote: "no", reason: "One stale row.", castat: now + 2 },
4663
- { agentid: "v1", vote: "abstain", castat: now + 3 },
4664
- ],
4665
- tally: { yes: 2, no: 1, abstain: 1 },
4666
- quorum: 3,
4667
- outcome: "failed",
4668
- closedat: now + 4,
4669
- };
4670
- const report = consensusreport({ record });
4671
- expect(report.version).toBe(protocolversion);
4672
- expect(report.proposal).toBe("Merge the two extractions of the pricing table.");
4673
- expect(report.votes).toHaveLength(4);
4674
- expect(report.votes[2]).toMatchObject({ agentid: "c1", vote: "no", reason: "One stale row." });
4675
- expect(report.tally).toEqual({ yes: 2, no: 1, abstain: 1 });
4676
- expect(report.quorum).toBe(3);
4677
- expect(report.outcome).toBe("failed");
4678
- });
4679
-
4680
- it("wraps the aggregation report with the per agent sections, the conflicts and the open state", () => {
4681
- const record: aggregaterecord = {
4682
- id: "agg1",
4683
- subject: "The pricing table read",
4684
- cells: [
4685
- { agentid: "w1", runid: "run-1", section: "rowcount", output: "14" },
4686
- { agentid: "w2", runid: "run-2", section: "rowcount", output: "15" },
4687
- ],
4688
- conflicts: [{ key: "rowcount" }],
4689
- state: "open",
4690
- createdat: now,
4691
- };
4692
- const report = aggregationreport({ record });
4693
- expect(report.version).toBe(protocolversion);
4694
- expect(report.subject).toBe("The pricing table read");
4695
- expect(report.sections).toHaveLength(2);
4696
- expect(report.sections[0]).toMatchObject({ agentid: "w1", runid: "run-1", section: "rowcount" });
4697
- expect(report.conflicts).toEqual([{ key: "rowcount" }]);
4698
- expect(report.state).toBe("open");
4699
- });
4700
-
4701
- it("wraps the lessonshare matches with the agent, the finding, the origin and the reuse count", () => {
4702
- const lessons: lessonrecord[] = [
4703
- {
4704
- id: "l1",
4705
- agentid: "w1",
4706
- finding: "The pricing table loads only after the hero settles.",
4707
- origin: "https://example.com",
4708
- reusecount: 2,
4709
- recordedat: now,
4710
- lastusedat: now + 1,
4711
- },
4712
- {
4713
- id: "l2",
4714
- agentid: "w2",
4715
- finding: "The login form refuses pasted passwords.",
4716
- origin: "https://shop.example",
4717
- reusecount: 0,
4718
- recordedat: now,
4719
- },
4720
- ];
4721
- const report = lessonreport({ lessons });
4722
- expect(report.version).toBe(protocolversion);
4723
- expect(report.lessons).toHaveLength(2);
4724
- expect(report.lessons[0]).toMatchObject({ id: "l1", agentid: "w1", origin: "https://example.com", reusecount: 2 });
4725
- expect(report.lessons[0]?.lastusedat).toBe(now + 1);
4726
- expect(report.lessons[1]).not.toHaveProperty("lastusedat");
4727
- });
4728
-
4729
- it("wraps the shared cost ledger with every entry attributed to its agent and the split", () => {
4730
- const entries: costentry[] = [
4731
- { agentid: "w1", runid: "run-1", units: 4, description: "The pricing table read.", at: now },
4732
- { agentid: "w2", runid: "run-2", units: 2, description: "The footer read.", at: now + 1 },
4733
- ];
4734
- const split = [
4735
- { agentid: "w1", units: 4, share: 2, sharedwith: ["w2"] },
4736
- { agentid: "w2", units: 2, share: 2 },
4737
- ];
4738
- const report = costledgerreport({ entries, split });
4739
- expect(report.version).toBe(protocolversion);
4740
- expect(report.entries[0]).toMatchObject({
4741
- agentid: "w1",
4742
- runid: "run-1",
4743
- units: 4,
4744
- description: "The pricing table read.",
4745
- });
4746
- expect(report.split[0]).toMatchObject({ agentid: "w1", units: 4, share: 2, sharedwith: ["w2"] });
4747
- expect(report.split[1]).not.toHaveProperty("sharedwith");
4748
- });
4749
-
4750
- it("parses the spawn request against the fleet registry and answers the spawn reply with the lineage", () => {
4751
- const agents: agentrecord[] = [
4752
- { id: "a1", name: "Scout", role: "worker", origin: "https://example.com", state: "active", registeredat: now },
4753
- { id: "w1", name: "Scribe", role: "worker", origin: "https://example.com", state: "active", registeredat: now },
4754
- ];
4755
- const spawns: spawnrecord[] = [];
4756
- const parsed = parsespawnrequest(
4757
- {
4758
- parentid: "a1",
4759
- objective: "Index the footer links.",
4760
- role: "worker",
4761
- depth: 1,
4762
- narrowscope: { origins: ["https://example.com"] },
4763
- },
4764
- { maxdepth: 2 },
4765
- agents,
4766
- spawns,
4767
- );
4768
- expect(parsed.spec.parentid).toBe("a1");
4769
- expect(parsed.spec.depth).toBe(1);
4770
- expect(parsed.spec.narrowscope).toEqual({ origins: ["https://example.com"] });
4771
- expect(parsed.parent.id).toBe("a1");
4772
- const reply = spawnreply({
4773
- child: {
4774
- id: "g1",
4775
- name: "Probe",
4776
- role: "worker",
4777
- origin: "https://example.com",
4778
- state: "active",
4779
- registeredat: now + 1,
4780
- },
4781
- parentid: "a1",
4782
- depth: 1,
4783
- });
4784
- expect(reply.version).toBe(protocolversion);
4785
- expect(reply).toMatchObject({ agentid: "g1", name: "Probe", role: "worker", parentid: "a1", depth: 1 });
4786
- expect(() =>
4787
- parsespawnrequest({ parentid: "a1", objective: "Too deep.", depth: 9 }, { maxdepth: 2 }, agents, spawns),
4788
- ).toThrow(/depth limit/);
4789
- expect(() =>
4790
- parsespawnrequest({ parentid: "ghost", objective: "Unknown.", depth: 1 }, { maxdepth: 2 }, agents, spawns),
4791
- ).toThrow(/does not carry/);
4792
- });
4793
- });