harnery 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/README.md +1 -1
  2. package/dist/commander.js +1 -0
  3. package/dist/commands/agents.d.ts.map +1 -1
  4. package/dist/commands/agents.js +25 -19
  5. package/dist/commands/browse-ai.d.ts.map +1 -1
  6. package/dist/commands/browse-ai.js +4 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +141 -10
  9. package/dist/commands/checkpoint.js +8 -12
  10. package/dist/commands/deinit.d.ts +1 -1
  11. package/dist/commands/deinit.d.ts.map +1 -1
  12. package/dist/commands/deinit.js +8 -4
  13. package/dist/commands/docs.d.ts.map +1 -1
  14. package/dist/commands/docs.js +2 -0
  15. package/dist/commands/doctor.js +10 -2
  16. package/dist/commands/fetch.d.ts.map +1 -1
  17. package/dist/commands/fetch.js +10 -0
  18. package/dist/commands/init.d.ts +19 -3
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +232 -104
  21. package/dist/commands/instructions.js +5 -4
  22. package/dist/commands/ledger-v3.d.ts.map +1 -1
  23. package/dist/commands/ledger-v3.js +2 -1
  24. package/dist/commands/webview.d.ts +4 -0
  25. package/dist/commands/webview.d.ts.map +1 -0
  26. package/dist/commands/webview.js +94 -0
  27. package/dist/core/adapter.d.ts +50 -1
  28. package/dist/core/adapter.d.ts.map +1 -1
  29. package/dist/core/adapter.js +59 -1
  30. package/dist/core/adapters/attestation.d.ts.map +1 -1
  31. package/dist/core/adapters/attestation.js +3 -2
  32. package/dist/core/adapters/profiles.d.ts +20 -3
  33. package/dist/core/adapters/profiles.d.ts.map +1 -1
  34. package/dist/core/adapters/profiles.js +32 -1
  35. package/dist/core/adapters/registry.d.ts.map +1 -1
  36. package/dist/core/adapters/registry.js +33 -0
  37. package/dist/core/agents/canonical-emit.d.ts +1 -1
  38. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  39. package/dist/core/agents/canonical-emit.js +2 -7
  40. package/dist/core/agents/cli.js +18 -16
  41. package/dist/core/agents/coord-client.d.ts.map +1 -1
  42. package/dist/core/agents/coord-client.js +7 -2
  43. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/claim-conflict.js +3 -9
  45. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  46. package/dist/core/agents/rules/commit-conflict.js +2 -8
  47. package/dist/core/agents/rules/stop-hook.d.ts +2 -0
  48. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  49. package/dist/core/agents/rules/stop-hook.js +1 -1
  50. package/dist/core/agents/session-events.d.ts +6 -2
  51. package/dist/core/agents/session-events.d.ts.map +1 -1
  52. package/dist/core/agents/session-events.js +105 -26
  53. package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
  54. package/dist/core/agents/session-finalization-state-v3.js +2 -1
  55. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  56. package/dist/core/agents/session-finalizer-v3.js +5 -4
  57. package/dist/core/agents/session-name-display.d.ts +28 -0
  58. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  59. package/dist/core/agents/session-name-display.js +43 -0
  60. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  61. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  62. package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
  63. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  64. package/dist/core/agents/state/heartbeat-writer.js +21 -2
  65. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  66. package/dist/core/agents/state/live-coordination-view.js +1 -0
  67. package/dist/core/agents/state/live-coordination-writer.js +3 -0
  68. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  69. package/dist/core/agents/state/stale-sweep.js +2 -10
  70. package/dist/core/agents/turn-changes.d.ts +17 -0
  71. package/dist/core/agents/turn-changes.d.ts.map +1 -0
  72. package/dist/core/agents/turn-changes.js +76 -0
  73. package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
  74. package/dist/core/artifacts/delivery-card.js +2 -1
  75. package/dist/core/artifacts/index.d.ts.map +1 -1
  76. package/dist/core/artifacts/index.js +4 -3
  77. package/dist/core/config.d.ts +5 -0
  78. package/dist/core/config.d.ts.map +1 -1
  79. package/dist/core/config.js +14 -0
  80. package/dist/core/context/index.d.ts.map +1 -1
  81. package/dist/core/context/index.js +2 -1
  82. package/dist/core/conversations/archive.d.ts.map +1 -1
  83. package/dist/core/conversations/archive.js +4 -3
  84. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  85. package/dist/core/diagnostics/bundle.js +3 -2
  86. package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/canary.js +5 -4
  88. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
  89. package/dist/core/events/legacy-storage/compression.js +2 -1
  90. package/dist/core/events/v3/adapter-id.d.ts +10 -2
  91. package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
  92. package/dist/core/events/v3/adapter-id.js +16 -0
  93. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  94. package/dist/core/events/v3/authority-outbox.js +2 -1
  95. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  96. package/dist/core/events/v3/bootstrap.js +5 -4
  97. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  98. package/dist/core/events/v3/capabilities.js +17 -0
  99. package/dist/core/events/v3/contract.d.ts +5 -0
  100. package/dist/core/events/v3/contract.d.ts.map +1 -1
  101. package/dist/core/events/v3/contract.js +7 -1
  102. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  103. package/dist/core/events/v3/control-witness.js +3 -2
  104. package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
  105. package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
  106. package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
  107. package/dist/core/events/v3/fingerprint-keys.js +7 -6
  108. package/dist/core/events/v3/generated.d.ts +1 -1
  109. package/dist/core/events/v3/generated.js +1 -1
  110. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  111. package/dist/core/events/v3/live-feed.js +5 -4
  112. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  113. package/dist/core/events/v3/producers/command-recorder.js +8 -7
  114. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  115. package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
  116. package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
  117. package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
  118. package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
  119. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  120. package/dist/core/events/v3/producers/hook-base.js +10 -4
  121. package/dist/core/events/v3/producers/hook.d.ts +2 -0
  122. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  123. package/dist/core/events/v3/producers/hook.js +16 -1
  124. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  125. package/dist/core/events/v3/producers/intake.js +16 -6
  126. package/dist/core/events/v3/producers/recorder.d.ts +2 -4
  127. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  128. package/dist/core/events/v3/producers/recorder.js +109 -101
  129. package/dist/core/events/v3/recovery.d.ts.map +1 -1
  130. package/dist/core/events/v3/recovery.js +4 -3
  131. package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
  132. package/dist/core/events/v3/support-storage/inventory.js +2 -1
  133. package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
  134. package/dist/core/events/v3/support-storage/maintenance.js +4 -3
  135. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  136. package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
  137. package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
  138. package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
  139. package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
  140. package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
  141. package/dist/core/events/v3/tool-line-changes.js +135 -0
  142. package/dist/core/events/v3/writer.d.ts.map +1 -1
  143. package/dist/core/events/v3/writer.js +8 -7
  144. package/dist/core/governor/planning.d.ts.map +1 -1
  145. package/dist/core/governor/planning.js +6 -5
  146. package/dist/core/governor/service.d.ts.map +1 -1
  147. package/dist/core/governor/service.js +12 -11
  148. package/dist/core/governor/state.d.ts.map +1 -1
  149. package/dist/core/governor/state.js +7 -6
  150. package/dist/core/guard/evaluator.js +1 -0
  151. package/dist/core/guard/storage.d.ts.map +1 -1
  152. package/dist/core/guard/storage.js +3 -2
  153. package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
  154. package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
  155. package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
  156. package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
  157. package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
  158. package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
  159. package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
  160. package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
  161. package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
  162. package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
  163. package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
  164. package/dist/core/hooks/adapter/behaviors/index.js +20 -0
  165. package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
  166. package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
  167. package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
  168. package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
  169. package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
  170. package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
  171. package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
  172. package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
  173. package/dist/core/hooks/adapter/behaviors/types.js +1 -0
  174. package/dist/core/hooks/adapter/detect.d.ts +1 -1
  175. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  176. package/dist/core/hooks/adapter/detect.js +2 -3
  177. package/dist/core/hooks/adapter/events.d.ts +29 -1
  178. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  179. package/dist/core/hooks/adapter/events.js +31 -0
  180. package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
  181. package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
  182. package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
  183. package/dist/core/hooks/adapter/output.d.ts +7 -0
  184. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  185. package/dist/core/hooks/adapter/output.js +8 -1
  186. package/dist/core/hooks/adapter/wiring.d.ts +15 -1
  187. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  188. package/dist/core/hooks/adapter/wiring.js +74 -11
  189. package/dist/core/hooks/cli.js +635 -529
  190. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
  191. package/dist/core/hooks/effects/image-capture.js +2 -1
  192. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
  193. package/dist/core/hooks/prompt-context/contract.js +1 -0
  194. package/dist/core/hooks/resolve/transcript.d.ts +1 -1
  195. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  196. package/dist/core/hooks/resolve/transcript.js +35 -4
  197. package/dist/core/hooks/session-name-presence.d.ts +3 -1
  198. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  199. package/dist/core/hooks/session-name-presence.js +7 -2
  200. package/dist/core/hooks/shell-waiter.d.ts +2 -0
  201. package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
  202. package/dist/core/hooks/shell-waiter.js +173 -0
  203. package/dist/core/resources/service.d.ts.map +1 -1
  204. package/dist/core/resources/service.js +5 -4
  205. package/dist/core/semantic/review.d.ts.map +1 -1
  206. package/dist/core/semantic/review.js +5 -4
  207. package/dist/core/semantic/service.d.ts.map +1 -1
  208. package/dist/core/semantic/service.js +15 -14
  209. package/dist/core/semantic/storage.d.ts.map +1 -1
  210. package/dist/core/semantic/storage.js +5 -4
  211. package/dist/core/storage/atomic-json.d.ts.map +1 -1
  212. package/dist/core/storage/atomic-json.js +7 -3
  213. package/dist/core/storage/durable-history.d.ts.map +1 -1
  214. package/dist/core/storage/durable-history.js +6 -5
  215. package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
  216. package/dist/core/storage/maintenance-providers.js +2 -1
  217. package/dist/core/storage/maintenance.d.ts.map +1 -1
  218. package/dist/core/storage/maintenance.js +7 -6
  219. package/dist/core/storage/metrics.d.ts.map +1 -1
  220. package/dist/core/storage/metrics.js +5 -1
  221. package/dist/core/storage/modes.d.ts +17 -0
  222. package/dist/core/storage/modes.d.ts.map +1 -0
  223. package/dist/core/storage/modes.js +47 -0
  224. package/dist/core/storage/process-log.d.ts.map +1 -1
  225. package/dist/core/storage/process-log.js +3 -2
  226. package/dist/core/storage/receipts.d.ts.map +1 -1
  227. package/dist/core/storage/receipts.js +4 -3
  228. package/dist/core/storage/segments.d.ts.map +1 -1
  229. package/dist/core/storage/segments.js +7 -6
  230. package/dist/core/supervisor/hook-health.d.ts +2 -1
  231. package/dist/core/supervisor/hook-health.d.ts.map +1 -1
  232. package/dist/core/supervisor/hook-health.js +4 -3
  233. package/dist/core/supervisor/service.d.ts.map +1 -1
  234. package/dist/core/supervisor/service.js +7 -6
  235. package/dist/core/work/state.d.ts.map +1 -1
  236. package/dist/core/work/state.js +8 -7
  237. package/dist/core/workflow/adapters.d.ts +7 -0
  238. package/dist/core/workflow/adapters.d.ts.map +1 -1
  239. package/dist/core/workflow/adapters.js +9 -0
  240. package/dist/core/workflow/approvals.d.ts.map +1 -1
  241. package/dist/core/workflow/approvals.js +6 -5
  242. package/dist/core/workflow/billing.d.ts.map +1 -1
  243. package/dist/core/workflow/billing.js +33 -4
  244. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  245. package/dist/core/workflow/durable-record.js +4 -3
  246. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  247. package/dist/core/workflow/live-session-v3.js +2 -3
  248. package/dist/core/workflow/run-state.d.ts.map +1 -1
  249. package/dist/core/workflow/run-state.js +3 -2
  250. package/dist/core/workflow/spawn-opencode.d.ts +36 -0
  251. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
  252. package/dist/core/workflow/spawn-opencode.js +132 -0
  253. package/dist/core/workflow/transcript.d.ts.map +1 -1
  254. package/dist/core/workflow/transcript.js +3 -2
  255. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  256. package/dist/core/workflow/workspaces/leases.js +5 -4
  257. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  258. package/dist/core/workflow/workspaces/local-git.js +3 -2
  259. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
  260. package/dist/core/workflow/workspaces/paths.js +2 -1
  261. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  262. package/dist/core/workflow/workspaces/state.js +10 -9
  263. package/dist/lazy-commands.js +8 -1
  264. package/dist/lib/agent-browser/client.d.ts +9 -0
  265. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  266. package/dist/lib/agent-browser/client.js +13 -0
  267. package/dist/lib/browser/client.d.ts +35 -1
  268. package/dist/lib/browser/client.d.ts.map +1 -1
  269. package/dist/lib/browser/client.js +117 -8
  270. package/dist/lib/browser/identity.d.ts +25 -0
  271. package/dist/lib/browser/identity.d.ts.map +1 -0
  272. package/dist/lib/browser/identity.js +66 -0
  273. package/dist/lib/browser/index.d.ts +2 -1
  274. package/dist/lib/browser/index.d.ts.map +1 -1
  275. package/dist/lib/browser/index.js +2 -1
  276. package/dist/lib/browser/launch-args.d.ts +26 -0
  277. package/dist/lib/browser/launch-args.d.ts.map +1 -1
  278. package/dist/lib/browser/launch-args.js +41 -1
  279. package/dist/lib/browser/user-agent.d.ts +53 -0
  280. package/dist/lib/browser/user-agent.d.ts.map +1 -0
  281. package/dist/lib/browser/user-agent.js +79 -0
  282. package/dist/lib/docs-metadata-managed.d.ts +1 -0
  283. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  284. package/dist/lib/docs-metadata-managed.js +17 -1
  285. package/dist/lib/identities/assume.d.ts.map +1 -1
  286. package/dist/lib/identities/assume.js +4 -9
  287. package/dist/lib/instructions/apply.d.ts.map +1 -1
  288. package/dist/lib/instructions/apply.js +2 -0
  289. package/dist/lib/pace/index.d.ts +2 -0
  290. package/dist/lib/pace/index.d.ts.map +1 -0
  291. package/dist/lib/pace/index.js +1 -0
  292. package/dist/lib/pace/pace.d.ts +95 -0
  293. package/dist/lib/pace/pace.d.ts.map +1 -0
  294. package/dist/lib/pace/pace.js +313 -0
  295. package/dist/lib/tunnel/forward-headers.d.ts +11 -0
  296. package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
  297. package/dist/lib/tunnel/forward-headers.js +17 -0
  298. package/dist/lib/tunnel/gate.js +15 -10
  299. package/dist/lib/webview/index.d.ts +2 -0
  300. package/dist/lib/webview/index.d.ts.map +1 -0
  301. package/dist/lib/webview/index.js +1 -0
  302. package/dist/lib/webview/webview.d.ts +116 -0
  303. package/dist/lib/webview/webview.d.ts.map +1 -0
  304. package/dist/lib/webview/webview.js +289 -0
  305. package/opencode-plugin/package.json +19 -0
  306. package/opencode-plugin/src/index.ts +791 -0
  307. package/package.json +11 -4
  308. package/schemas/config.schema.json +13 -0
  309. package/schemas/event-v3.schema.json +164 -0
  310. package/src/commander.ts +7 -0
  311. package/src/commands/agents.ts +33 -18
  312. package/src/commands/browse-ai.ts +8 -0
  313. package/src/commands/browse.ts +189 -10
  314. package/src/commands/checkpoint.ts +7 -9
  315. package/src/commands/deinit.ts +9 -4
  316. package/src/commands/docs.ts +2 -0
  317. package/src/commands/doctor.ts +11 -2
  318. package/src/commands/fetch.ts +14 -0
  319. package/src/commands/init.ts +281 -119
  320. package/src/commands/instructions.ts +8 -4
  321. package/src/commands/ledger-v3.ts +2 -1
  322. package/src/commands/webview.ts +159 -0
  323. package/src/core/adapter.ts +102 -1
  324. package/src/core/adapters/attestation.ts +3 -2
  325. package/src/core/adapters/profiles.ts +42 -1
  326. package/src/core/adapters/registry.ts +37 -0
  327. package/src/core/agents/canonical-emit.ts +2 -5
  328. package/src/core/agents/cli.ts +18 -17
  329. package/src/core/agents/coord-client.ts +9 -3
  330. package/src/core/agents/rules/claim-conflict.ts +3 -8
  331. package/src/core/agents/rules/commit-conflict.ts +2 -7
  332. package/src/core/agents/rules/stop-hook.ts +3 -1
  333. package/src/core/agents/session-events.ts +120 -26
  334. package/src/core/agents/session-finalization-state-v3.ts +2 -1
  335. package/src/core/agents/session-finalizer-v3.ts +5 -4
  336. package/src/core/agents/session-name-display.ts +48 -0
  337. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  338. package/src/core/agents/state/heartbeat-writer.ts +24 -2
  339. package/src/core/agents/state/live-coordination-view.ts +1 -0
  340. package/src/core/agents/state/live-coordination-writer.ts +3 -0
  341. package/src/core/agents/state/stale-sweep.ts +2 -9
  342. package/src/core/agents/turn-changes.ts +97 -0
  343. package/src/core/artifacts/delivery-card.ts +2 -1
  344. package/src/core/artifacts/index.ts +4 -3
  345. package/src/core/config.ts +23 -0
  346. package/src/core/context/index.ts +2 -1
  347. package/src/core/conversations/archive.ts +4 -3
  348. package/src/core/diagnostics/bundle.ts +3 -2
  349. package/src/core/events/legacy-storage/canary.ts +5 -4
  350. package/src/core/events/legacy-storage/compression.ts +2 -1
  351. package/src/core/events/v3/adapter-id.ts +29 -1
  352. package/src/core/events/v3/authority-outbox.ts +2 -1
  353. package/src/core/events/v3/bootstrap.ts +5 -4
  354. package/src/core/events/v3/capabilities.ts +17 -0
  355. package/src/core/events/v3/contract.ts +9 -1
  356. package/src/core/events/v3/control-witness.ts +3 -2
  357. package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
  358. package/src/core/events/v3/fingerprint-keys.ts +7 -6
  359. package/src/core/events/v3/generated.ts +1 -1
  360. package/src/core/events/v3/live-feed.ts +5 -4
  361. package/src/core/events/v3/producers/command-recorder.ts +8 -6
  362. package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
  363. package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
  364. package/src/core/events/v3/producers/hook-base.ts +11 -4
  365. package/src/core/events/v3/producers/hook.ts +19 -1
  366. package/src/core/events/v3/producers/intake.ts +18 -6
  367. package/src/core/events/v3/producers/recorder.ts +123 -114
  368. package/src/core/events/v3/recovery.ts +4 -3
  369. package/src/core/events/v3/support-storage/inventory.ts +2 -1
  370. package/src/core/events/v3/support-storage/maintenance.ts +8 -3
  371. package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
  372. package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
  373. package/src/core/events/v3/tool-line-changes.ts +139 -0
  374. package/src/core/events/v3/writer.ts +8 -7
  375. package/src/core/governor/planning.ts +6 -5
  376. package/src/core/governor/service.ts +12 -11
  377. package/src/core/governor/state.ts +7 -6
  378. package/src/core/guard/evaluator.ts +1 -0
  379. package/src/core/guard/storage.ts +3 -2
  380. package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
  381. package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
  382. package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
  383. package/src/core/hooks/adapter/behaviors/index.ts +31 -0
  384. package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
  385. package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
  386. package/src/core/hooks/adapter/behaviors/types.ts +110 -0
  387. package/src/core/hooks/adapter/detect.ts +2 -3
  388. package/src/core/hooks/adapter/events.ts +53 -1
  389. package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
  390. package/src/core/hooks/adapter/output.ts +8 -1
  391. package/src/core/hooks/adapter/wiring.ts +85 -11
  392. package/src/core/hooks/cli.ts +733 -537
  393. package/src/core/hooks/effects/image-capture.ts +2 -1
  394. package/src/core/hooks/prompt-context/contract.ts +1 -0
  395. package/src/core/hooks/resolve/transcript.ts +42 -5
  396. package/src/core/hooks/session-name-presence.ts +10 -3
  397. package/src/core/hooks/shell-waiter.ts +166 -0
  398. package/src/core/resources/service.ts +5 -4
  399. package/src/core/semantic/review.ts +5 -4
  400. package/src/core/semantic/service.ts +15 -14
  401. package/src/core/semantic/storage.ts +5 -4
  402. package/src/core/storage/atomic-json.ts +7 -3
  403. package/src/core/storage/durable-history.ts +6 -5
  404. package/src/core/storage/maintenance-providers.ts +2 -1
  405. package/src/core/storage/maintenance.ts +7 -6
  406. package/src/core/storage/metrics.ts +5 -1
  407. package/src/core/storage/modes.ts +53 -0
  408. package/src/core/storage/process-log.ts +3 -2
  409. package/src/core/storage/receipts.ts +4 -3
  410. package/src/core/storage/segments.ts +7 -6
  411. package/src/core/supervisor/hook-health.ts +4 -4
  412. package/src/core/supervisor/service.ts +7 -6
  413. package/src/core/work/state.ts +8 -7
  414. package/src/core/workflow/adapters.ts +15 -0
  415. package/src/core/workflow/approvals.ts +6 -5
  416. package/src/core/workflow/billing.ts +34 -4
  417. package/src/core/workflow/durable-record.ts +4 -3
  418. package/src/core/workflow/live-session-v3.ts +2 -3
  419. package/src/core/workflow/run-state.ts +3 -2
  420. package/src/core/workflow/spawn-opencode.ts +150 -0
  421. package/src/core/workflow/transcript.ts +3 -2
  422. package/src/core/workflow/workspaces/leases.ts +5 -4
  423. package/src/core/workflow/workspaces/local-git.ts +3 -2
  424. package/src/core/workflow/workspaces/paths.ts +2 -1
  425. package/src/core/workflow/workspaces/state.ts +10 -9
  426. package/src/lazy-commands.ts +11 -1
  427. package/src/lib/agent-browser/client.ts +18 -0
  428. package/src/lib/browser/client.ts +147 -9
  429. package/src/lib/browser/identity.ts +64 -0
  430. package/src/lib/browser/index.ts +15 -1
  431. package/src/lib/browser/launch-args.ts +53 -1
  432. package/src/lib/browser/user-agent.ts +130 -0
  433. package/src/lib/docs-metadata-managed.ts +21 -0
  434. package/src/lib/identities/assume.ts +7 -7
  435. package/src/lib/instructions/apply.ts +2 -0
  436. package/src/lib/pace/index.ts +14 -0
  437. package/src/lib/pace/pace.ts +370 -0
  438. package/src/lib/tunnel/forward-headers.ts +16 -0
  439. package/src/lib/tunnel/gate.ts +18 -10
  440. package/src/lib/webview/index.ts +13 -0
  441. package/src/lib/webview/webview.ts +457 -0
@@ -16,6 +16,7 @@ import {
16
16
  } from "node:fs";
17
17
  import { hostname } from "node:os";
18
18
  import { basename, dirname, join, resolve } from "node:path";
19
+ import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
19
20
 
20
21
  const MAX_LEASE_BYTES = 8 * 1024;
21
22
  const MAX_CONTENTION_OBSERVATION_ATTEMPTS = 8;
@@ -202,11 +203,11 @@ function validateInput(input: AcquireNoClobberLeaseInput): void {
202
203
  }
203
204
 
204
205
  function ensureLeaseDirectory(leaseDir: string, onCreate?: () => void): void {
205
- mkdirSync(dirname(leaseDir), { recursive: true, mode: 0o700 });
206
+ mkdirSync(dirname(leaseDir), { recursive: true, mode: stateDirMode() });
206
207
  let created = false;
207
208
  onCreate?.();
208
209
  try {
209
- mkdirSync(leaseDir, { mode: 0o700 });
210
+ mkdirSync(leaseDir, { mode: stateDirMode() });
210
211
  created = true;
211
212
  } catch (error) {
212
213
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
@@ -226,7 +227,7 @@ function createRecoveryClaim(
226
227
  observed: { owner: NoClobberLeaseOwner; bytes: string },
227
228
  ): void {
228
229
  const temporary = join(leaseDir, `recovery-claim-${claimant.owner_id}`);
229
- mkdirSync(temporary, { mode: 0o700 });
230
+ mkdirSync(temporary, { mode: stateDirMode() });
230
231
  const claim: RecoveryClaim = {
231
232
  schema_version: 1,
232
233
  claimant,
@@ -385,7 +386,7 @@ function leaseHandle(
385
386
  }
386
387
 
387
388
  function writeExclusive(path: string, bytes: string): void {
388
- const fd = openSync(path, "wx", 0o600);
389
+ const fd = openSync(path, "wx", stateFileMode());
389
390
  try {
390
391
  writeFileSync(fd, bytes, "utf8");
391
392
  fsyncSync(fd);
@@ -8,6 +8,7 @@ import {
8
8
  writeFileSync,
9
9
  } from "node:fs";
10
10
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
11
+ import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
11
12
  import { WorkspaceAttestationError } from "./attestation-error.ts";
12
13
  import {
13
14
  git,
@@ -323,7 +324,7 @@ function claimedParentSegment(claim: WorkspaceClaim): string {
323
324
  function hideWorkspaceParentFromGit(parent: string): void {
324
325
  const ignoreFile = join(parent, ".gitignore");
325
326
  if (existsSync(ignoreFile)) return;
326
- writeFileSync(ignoreFile, "*\n", { mode: 0o600 });
327
+ writeFileSync(ignoreFile, "*\n", { mode: stateFileMode() });
327
328
  }
328
329
 
329
330
  function reconcileAllocation(
@@ -1908,7 +1909,7 @@ function capabilityDigestForClaim(claim: WorkspaceClaim): string {
1908
1909
 
1909
1910
  function acquireRepositoryLease(coordRoot: string, claim: WorkspaceClaim): () => void {
1910
1911
  const leaseDir = join(resolve(coordRoot), ".harnery", "workspaces", PROVIDER_ID, ".leases");
1911
- mkdirSync(leaseDir, { recursive: true, mode: 0o700 });
1912
+ mkdirSync(leaseDir, { recursive: true, mode: stateDirMode() });
1912
1913
  // Mutual exclusion keys on the Git common directory alone. `git worktree add`,
1913
1914
  // `prune`, and the shared-`config` migration all write to the common directory's
1914
1915
  // administrative area, and once worktrees and submodules are allowed, several
@@ -9,6 +9,7 @@ import {
9
9
  statSync,
10
10
  } from "node:fs";
11
11
  import { isAbsolute, join, relative, resolve } from "node:path";
12
+ import { stateDirMode } from "../../storage/modes.ts";
12
13
  import type { FilesystemIdentity, ValidatedFilesystemPath } from "./types.ts";
13
14
 
14
15
  const WINDOWS_AMBIGUOUS = /[<>:"|?*]|[ .]$/;
@@ -152,7 +153,7 @@ export function openContainedDirectory(
152
153
  );
153
154
  }
154
155
  } else {
155
- mkdirSync(candidate, { mode: 0o700 });
156
+ mkdirSync(candidate, { mode: stateDirMode() });
156
157
  }
157
158
  const childFd = openSync(
158
159
  candidate,
@@ -10,6 +10,7 @@ import {
10
10
  writeFileSync,
11
11
  } from "node:fs";
12
12
  import { dirname, join, resolve } from "node:path";
13
+ import { stateDirMode, stateFileMode } from "../../storage/modes.ts";
13
14
  import {
14
15
  canonicalJson,
15
16
  fsyncParentDirectory,
@@ -192,7 +193,7 @@ export function appendWorkspaceEvent(
192
193
  throw new Error("workspace provider event is invalid or too large");
193
194
  }
194
195
  const path = workspaceEventsPath(coordRoot, claim.provider_id, claim.binding_id);
195
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
196
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
196
197
  const existed = existsSync(path);
197
198
  const prior = readWorkspaceEvents(coordRoot, claim.provider_id, claim.binding_id);
198
199
  const basis = {
@@ -213,14 +214,14 @@ export function appendWorkspaceEvent(
213
214
  const line = `${canonicalJson(record)}\n`;
214
215
  if (Buffer.byteLength(line) > WORKSPACE_EVENT_BYTES)
215
216
  throw new Error("workspace provider event is too large");
216
- const fd = openSync(path, "a", 0o600);
217
+ const fd = openSync(path, "a", stateFileMode());
217
218
  try {
218
219
  writeFileSync(fd, line, "utf8");
219
220
  fsyncSync(fd);
220
221
  } finally {
221
222
  closeSync(fd);
222
223
  }
223
- chmodSync(path, 0o600);
224
+ chmodSync(path, stateFileMode());
224
225
  if (!existed) fsyncParentDirectory(path);
225
226
  return record;
226
227
  }
@@ -295,7 +296,7 @@ export function appendCleanupAttempt(
295
296
  input: Omit<WorkspaceCleanupAttempt, "seq" | "previous_sha256" | "record_sha256">,
296
297
  ): WorkspaceCleanupAttempt {
297
298
  const path = join(workflowRunDir(coordRoot, runId), "cleanup", "attempts.jsonl");
298
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
299
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
299
300
  const existed = existsSync(path);
300
301
  const prior = readCleanupAttempts(coordRoot, runId);
301
302
  const basis = {
@@ -311,14 +312,14 @@ export function appendCleanupAttempt(
311
312
  if (Buffer.byteLength(line) > WORKSPACE_EVENT_BYTES) {
312
313
  throw new Error("workspace cleanup attempt is too large");
313
314
  }
314
- const fd = openSync(path, "a", 0o600);
315
+ const fd = openSync(path, "a", stateFileMode());
315
316
  try {
316
317
  writeFileSync(fd, line, "utf8");
317
318
  fsyncSync(fd);
318
319
  } finally {
319
320
  closeSync(fd);
320
321
  }
321
- chmodSync(path, 0o600);
322
+ chmodSync(path, stateFileMode());
322
323
  if (!existed) fsyncParentDirectory(path);
323
324
  return attempt;
324
325
  }
@@ -329,7 +330,7 @@ export function appendIntegrationAttempt(
329
330
  input: Omit<IntegrationApplyAttempt, "seq" | "previous_sha256" | "record_sha256">,
330
331
  ): IntegrationApplyAttempt {
331
332
  const path = join(workflowRunDir(coordRoot, runId), "integration", "attempts.jsonl");
332
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
333
+ mkdirSync(dirname(path), { recursive: true, mode: stateDirMode() });
333
334
  const existed = existsSync(path);
334
335
  const prior = readIntegrationAttempts(coordRoot, runId);
335
336
  const basis = {
@@ -372,14 +373,14 @@ function appendChainedRecord(
372
373
  if (Buffer.byteLength(line) > WORKSPACE_EVENT_BYTES) {
373
374
  throw new Error(`${label} is too large`);
374
375
  }
375
- const fd = openSync(path, "a", 0o600);
376
+ const fd = openSync(path, "a", stateFileMode());
376
377
  try {
377
378
  writeFileSync(fd, line, "utf8");
378
379
  fsyncSync(fd);
379
380
  } finally {
380
381
  closeSync(fd);
381
382
  }
382
- chmodSync(path, 0o600);
383
+ chmodSync(path, stateFileMode());
383
384
  }
384
385
 
385
386
  function readChainedRecords<
@@ -256,7 +256,17 @@ function selectedCommand(
256
256
  if (token === "--") return undefined;
257
257
  if (token.startsWith("-")) {
258
258
  const option = findRootOption(program, token);
259
- if (option?.required || (option?.optional && !args[index + 1]?.startsWith("-"))) index++;
259
+ // `--format=json` carries its value inline, so the next token is the
260
+ // command, not the value. Skipping it here consumed the command name and
261
+ // the wrong bundle (or none) was materialized, which surfaced downstream
262
+ // as empty output or a bogus "too many arguments" error.
263
+ const inlineValue = token.includes("=");
264
+ if (
265
+ !inlineValue &&
266
+ (option?.required || (option?.optional && !args[index + 1]?.startsWith("-")))
267
+ ) {
268
+ index++;
269
+ }
260
270
  continue;
261
271
  }
262
272
  if (token === "help") return state.byName.get(args[index + 1] ?? "");
@@ -1,6 +1,7 @@
1
1
  import { type SpawnSyncOptions, spawnSync } from "node:child_process";
2
2
  import { writeFileSync } from "node:fs";
3
3
  import type { CookieJar, Cookie as JarCookie } from "../cookies/index.ts";
4
+ import { PaceGate, pacePolicyFromEnv } from "../pace/index.ts";
4
5
 
5
6
  /**
6
7
  * Thin wrapper over Vercel Labs' `agent-browser` Rust CLI.
@@ -34,6 +35,11 @@ export interface AgentBrowserOptions {
34
35
  * agent-browser session. Defaults to a tmp file derived from the jar.
35
36
  */
36
37
  stateFilePath?: string;
38
+ /**
39
+ * Human-pace gate applied before every `open`. Unset resolves the default
40
+ * from the `HARNERY_PACE*` environment; `null` disables pacing.
41
+ */
42
+ pace?: PaceGate | null;
37
43
  }
38
44
 
39
45
  export interface ExecResult {
@@ -48,11 +54,21 @@ const DEFAULT_TIMEOUT_MS = 60_000;
48
54
  export class AgentBrowser {
49
55
  private readonly opts: AgentBrowserOptions;
50
56
  private cookiesSeeded = false;
57
+ private paceGate: PaceGate | null | undefined;
51
58
 
52
59
  constructor(opts: AgentBrowserOptions = {}) {
53
60
  this.opts = opts;
54
61
  }
55
62
 
63
+ /** Block for the human-pace slot before a page load. Synchronous like the rest of this client. */
64
+ private paceBefore(url: string): void {
65
+ if (this.paceGate === undefined) {
66
+ this.paceGate =
67
+ this.opts.pace === undefined ? new PaceGate(pacePolicyFromEnv()) : this.opts.pace;
68
+ }
69
+ this.paceGate?.beforeSync(url);
70
+ }
71
+
56
72
  /**
57
73
  * Run an `agent-browser` subcommand. Returns a structured result.
58
74
  * Throws only on spawn failure (e.g., binary not found); non-zero exit
@@ -132,6 +148,7 @@ export class AgentBrowser {
132
148
 
133
149
  open(url: string, timeoutMs?: number): ExecResult {
134
150
  this.seedCookies();
151
+ this.paceBefore(url);
135
152
  return this.execOrThrow(["open", url], timeoutMs);
136
153
  }
137
154
 
@@ -183,6 +200,7 @@ export class AgentBrowser {
183
200
  // Naive shell-like splitting; agent-browser's own argv parser handles
184
201
  // the actual command; we just split on whitespace for the wrapper.
185
202
  const args = trimmed.split(/\s+/);
203
+ if (args[0] === "open" && args[1]) this.paceBefore(args[1]);
186
204
  results.push(this.exec(args));
187
205
  }
188
206
  return results;
@@ -16,6 +16,7 @@ import {
16
16
  CookieStoreParseError,
17
17
  type Cookie as JarCookie,
18
18
  } from "../cookies/index.ts";
19
+ import { PaceGate, pacePolicyFromEnv } from "../pace/index.ts";
19
20
  import { type AssertResult, type AssertSpec, buildAssertCheck } from "./asserts.js";
20
21
  import { stitchPngRows } from "./capture-fidelity.js";
21
22
  import {
@@ -33,6 +34,8 @@ import {
33
34
  type LayoutLintRequest,
34
35
  type LayoutLintResult,
35
36
  } from "./geometry.js";
37
+ import { browserIdentity, installDocumentIdentity } from "./identity.js";
38
+ import { AUTOMATION_DEFAULT_ARGS_TO_DROP, automationDisguiseArgs } from "./launch-args.js";
36
39
  import {
37
40
  buildClearLayoutAnnotationsScript,
38
41
  buildLayoutAnnotateScript,
@@ -91,7 +94,7 @@ export interface BrowserOptions {
91
94
  /** Cookie jar to seed/sync with. Pass `null` to skip jar entirely. */
92
95
  jar?: CookieJar | null;
93
96
  /** Viewport. Default 1280x800. */
94
- viewport?: { width: number; height: number };
97
+ viewport?: { width: number; height: number } | null;
95
98
  /**
96
99
  * Device scale factor (DPR) for the context. Unset keeps Playwright's
97
100
  * default of 1, so screenshots are CSS-pixel sized. A higher value renders
@@ -128,6 +131,13 @@ export interface BrowserOptions {
128
131
  * via this callback (e.g., a Cloudflare-bypass header for specific zones).
129
132
  */
130
133
  extraHeaders?: (url: string) => Record<string, string>;
134
+ /**
135
+ * Human-pace gate applied before every page load: `navigate`, `reload`, and
136
+ * the session tab verbs. Unset resolves the default from the `HARNERY_PACE*`
137
+ * environment (on, 3 to 9 seconds between loads of one site). `null`
138
+ * disables pacing for this client.
139
+ */
140
+ pace?: PaceGate | null;
131
141
  /**
132
142
  * Extra Chromium command-line flags, passed through to Playwright's
133
143
  * `launchPersistentContext` `args`. Used for environment-specific
@@ -136,6 +146,28 @@ export interface BrowserOptions {
136
146
  * defaults only.
137
147
  */
138
148
  launchArgs?: string[];
149
+ /**
150
+ * Playwright browser channel. `"chrome"` launches the operator's installed
151
+ * Google Chrome instead of the bundled "Chrome for Testing" build. Unset or
152
+ * `"chromium"` keeps the bundled browser.
153
+ */
154
+ channel?: "chrome" | "chrome-beta" | "msedge" | "chromium";
155
+ /**
156
+ * Drop Playwright's `--enable-automation` default, disable Blink's
157
+ * AutomationControlled feature, and run with the Chromium sandbox on (no
158
+ * `--no-sandbox`), so pages do not see `navigator.webdriver`, the automation
159
+ * infobar, or the unsupported-flag banner. Headed sessions set this; sites
160
+ * that screen for automation otherwise refuse clicks a person makes in the
161
+ * window.
162
+ */
163
+ hideAutomation?: boolean;
164
+ /**
165
+ * User agent to present. Set by `browse` from `lib/browser/user-agent`
166
+ * (never Linux, never HeadlessChrome). Applied both as the Playwright
167
+ * context override and as `--user-agent` so workers and frames agree.
168
+ * Unset keeps the browser's native string.
169
+ */
170
+ userAgent?: string;
139
171
  /** Authenticated browser proxy passed directly to Playwright. */
140
172
  proxy?: { server: string; username?: string; password?: string };
141
173
  }
@@ -306,6 +338,7 @@ export class Browser {
306
338
  private consoleEvents: ConsoleEvent[] = [];
307
339
  private pageErrors: PageErrorEvent[] = [];
308
340
  private failedRequests: FailedRequest[] = [];
341
+ private identityReady = new WeakMap<Page, Promise<void>>();
309
342
 
310
343
  constructor(private opts: BrowserOptions = {}) {
311
344
  this.profileDir = opts.profileDir ?? DEFAULT_PROFILE;
@@ -326,20 +359,54 @@ export class Browser {
326
359
  */
327
360
  private async openOnce(): Promise<void> {
328
361
  const jarCookies = this.opts.jar?.list() ?? [];
362
+ const launchIdentity = this.opts.userAgent ? browserIdentity(this.opts.userAgent) : undefined;
363
+ const launchArgs = [
364
+ ...new Set([
365
+ ...(this.opts.headed && this.opts.viewport == null ? ["--window-size=1536,864"] : []),
366
+ ...(this.opts.launchArgs ?? []),
367
+ ...(this.opts.hideAutomation ? automationDisguiseArgs() : []),
368
+ ...(this.opts.userAgent ? [`--user-agent=${this.opts.userAgent}`] : []),
369
+ ]),
370
+ ];
329
371
 
330
372
  // Hold a launch slot only across the spawn itself, not the browser's life.
331
373
  await acquireLaunchSlot();
332
374
  try {
333
375
  this.context = await chromium.launchPersistentContext(this.profileDir, {
334
376
  headless: !this.opts.headed,
335
- viewport: this.opts.viewport ?? { width: 1280, height: 800 },
377
+ // Service-worker-owned navigations bypass Playwright request routes.
378
+ ...(this.getPaceGate()?.policy.enabled ? { serviceWorkers: "block" as const } : {}),
379
+ viewport:
380
+ this.opts.viewport !== undefined
381
+ ? this.opts.viewport
382
+ : this.opts.headed
383
+ ? null
384
+ : { width: 1280, height: 800 },
336
385
  ...(this.opts.deviceScaleFactor !== undefined
337
386
  ? { deviceScaleFactor: this.opts.deviceScaleFactor }
338
387
  : {}),
339
388
  ...(this.opts.colorScheme ? { colorScheme: this.opts.colorScheme } : {}),
389
+ ...(this.opts.userAgent ? { userAgent: this.opts.userAgent } : {}),
390
+ ...(launchIdentity
391
+ ? {
392
+ extraHTTPHeaders: {
393
+ "sec-ch-ua-platform": JSON.stringify(launchIdentity.userAgentMetadata.platform),
394
+ "sec-ch-ua-mobile": "?0",
395
+ "sec-ch-ua": launchIdentity.userAgentMetadata.brands
396
+ .map(
397
+ ({ brand, version }) => `${JSON.stringify(brand)};v=${JSON.stringify(version)}`,
398
+ )
399
+ .join(", "),
400
+ },
401
+ }
402
+ : {}),
340
403
  ...(this.opts.launchTimeout !== undefined ? { timeout: this.opts.launchTimeout } : {}),
341
- ...(this.opts.launchArgs && this.opts.launchArgs.length > 0
342
- ? { args: this.opts.launchArgs }
404
+ ...(launchArgs.length > 0 ? { args: launchArgs } : {}),
405
+ ...(this.opts.channel && this.opts.channel !== "chromium"
406
+ ? { channel: this.opts.channel }
407
+ : {}),
408
+ ...(this.opts.hideAutomation
409
+ ? { ignoreDefaultArgs: [...AUTOMATION_DEFAULT_ARGS_TO_DROP], chromiumSandbox: true }
343
410
  : {}),
344
411
  ...(this.opts.proxy ? { proxy: this.opts.proxy } : {}),
345
412
  ...(this.opts.recordHarPath
@@ -350,18 +417,55 @@ export class Browser {
350
417
  releaseLaunchSlot();
351
418
  }
352
419
  this.context.setDefaultNavigationTimeout(this.opts.navigationTimeout ?? 30_000);
420
+ const identity = this.opts.userAgent
421
+ ? browserIdentity(this.opts.userAgent, this.context.browser()?.version())
422
+ : undefined;
423
+ if (identity) await this.context.addInitScript(installDocumentIdentity, identity);
353
424
 
354
425
  if (jarCookies.length > 0) {
355
426
  await this.context.addCookies(jarCookies.map(toPWCookie));
356
427
  }
357
428
 
358
- // Caller-injected extraHeaders callback (e.g., for Cloudflare-bypass
359
- // or custom auth headers). Per-request route handler so headers only
360
- // attach when the callback returns non-empty.
429
+ // Gate actual document requests, including links, forms, script navigation,
430
+ // and the first request of a popup. Method-level waits miss those loads.
431
+ // Redirect hops are part of the original load, not additional reservations.
361
432
  const headersCb = this.opts.extraHeaders;
362
- if (headersCb) {
433
+ if (this.getPaceGate()?.policy.enabled || headersCb || this.opts.userAgent) {
363
434
  await this.context.route("**/*", async (route, request) => {
364
- const extra = headersCb(request.url());
435
+ let mainDocument = false;
436
+ if (request.isNavigationRequest() && !request.redirectedFrom()) {
437
+ try {
438
+ mainDocument = request.frame() === request.frame().page().mainFrame();
439
+ } catch {
440
+ // A popup's first request can precede creation of its Page/Frame.
441
+ mainDocument = true;
442
+ }
443
+ }
444
+ if (mainDocument) {
445
+ try {
446
+ await this.getPaceGate()?.before(request.url());
447
+ } catch {
448
+ await route.abort("failed");
449
+ return;
450
+ }
451
+ }
452
+ const extra = { ...headersCb?.(request.url()) };
453
+ // Popup request headers are already computed before its CDP session is
454
+ // available. Keep negotiated hints consistent on that first request too.
455
+ if (identity) {
456
+ const metadata = identity.userAgentMetadata;
457
+ extra["sec-ch-ua-platform"] = JSON.stringify(metadata.platform);
458
+ extra["sec-ch-ua-mobile"] = "?0";
459
+ extra["sec-ch-ua"] = metadata.brands
460
+ .map(({ brand, version }) => `${JSON.stringify(brand)};v=${JSON.stringify(version)}`)
461
+ .join(", ");
462
+ for (const [header, value] of Object.entries({
463
+ "sec-ch-ua-platform-version": metadata.platformVersion,
464
+ "sec-ch-ua-arch": metadata.architecture,
465
+ "sec-ch-ua-bitness": metadata.bitness,
466
+ }))
467
+ if (header in request.headers()) extra[header] = JSON.stringify(value);
468
+ }
365
469
  if (Object.keys(extra).length === 0) return route.continue();
366
470
  const headers = { ...request.headers(), ...extra };
367
471
  return route.continue({ headers });
@@ -370,6 +474,7 @@ export class Browser {
370
474
 
371
475
  const pages = this.context.pages();
372
476
  const firstPage = pages[0] ?? (await this.context.newPage());
477
+ await Promise.all(this.context.pages().map((page) => this.preparePageIdentity(page)));
373
478
  for (const page of this.context.pages()) this.trackPage(page, page === firstPage);
374
479
  this.context.on("page", (page) => this.trackPage(page, true));
375
480
  }
@@ -491,6 +596,10 @@ export class Browser {
491
596
  type: "unknown",
492
597
  });
493
598
  this.attachDiagnosticListeners(page);
599
+ void this.preparePageIdentity(page).catch((error) => {
600
+ if (!page.isClosed())
601
+ this.pageErrors.push({ message: `Browser identity setup failed: ${String(error)}` });
602
+ });
494
603
  page.on("domcontentloaded", () => this.recordDocumentNavigation(page));
495
604
  page.once("close", () => {
496
605
  this.pageRecency.delete(page);
@@ -501,6 +610,23 @@ export class Browser {
501
610
  if (makeActive) this.setActivePage(page);
502
611
  }
503
612
 
613
+ private preparePageIdentity(page: Page): Promise<void> {
614
+ if (!this.opts.userAgent || !this.context) return Promise.resolve();
615
+ let ready = this.identityReady.get(page);
616
+ if (!ready) {
617
+ const context = this.context;
618
+ const userAgent = this.opts.userAgent;
619
+ ready = (async () => {
620
+ const identity = browserIdentity(userAgent, context.browser()?.version());
621
+ if (!identity) return;
622
+ const session = await context.newCDPSession(page);
623
+ await session.send("Emulation.setUserAgentOverride", identity);
624
+ })();
625
+ this.identityReady.set(page, ready);
626
+ }
627
+ return ready;
628
+ }
629
+
504
630
  private setActivePage(page: Page): void {
505
631
  if (page.isClosed()) return;
506
632
  this.page = page;
@@ -569,8 +695,19 @@ export class Browser {
569
695
  };
570
696
  }
571
697
 
698
+ private paceGate: PaceGate | null | undefined;
699
+
700
+ private getPaceGate(): PaceGate | null {
701
+ if (this.paceGate === undefined) {
702
+ this.paceGate =
703
+ this.opts.pace === undefined ? new PaceGate(pacePolicyFromEnv()) : this.opts.pace;
704
+ }
705
+ return this.paceGate;
706
+ }
707
+
572
708
  async navigate(url: string): Promise<NavigateResult> {
573
709
  const page = this.currentPage;
710
+ await this.preparePageIdentity(page);
574
711
  const response = await page.goto(url, { waitUntil: this.opts.waitUntil ?? "load" });
575
712
  return {
576
713
  url: page.url(),
@@ -1381,6 +1518,7 @@ export class Browser {
1381
1518
  const page = await this.context.newPage();
1382
1519
  this.trackPage(page, true);
1383
1520
  try {
1521
+ await this.preparePageIdentity(page);
1384
1522
  await page.goto(url, { waitUntil: this.opts.waitUntil ?? "load" });
1385
1523
  this.sessionRevision++;
1386
1524
  return await this.describeTab(page);
@@ -0,0 +1,64 @@
1
+ /** Keep Chromium's UA, navigator.platform and client hints in one profile. */
2
+ export function browserIdentity(userAgent: string, browserVersion?: string) {
3
+ const chrome = /(?:Chrome|Chromium)\/(\d+)\.([\d.]+)/.exec(userAgent);
4
+ const windows = userAgent.includes("Windows NT");
5
+ const mac = userAgent.includes("Macintosh");
6
+ // A custom non-Chromium or non-desktop UA is an explicit caller choice.
7
+ if (!chrome || (!windows && !mac)) return undefined;
8
+ const major = chrome[1]!;
9
+ const fullVersion = browserVersion?.startsWith(`${major}.`)
10
+ ? browserVersion
11
+ : `${major}.${chrome[2]}`;
12
+ const brand = userAgent.includes("Edg/") ? "Microsoft Edge" : "Google Chrome";
13
+ return {
14
+ userAgent,
15
+ platform: windows ? "Win32" : "MacIntel",
16
+ userAgentMetadata: {
17
+ brands: [
18
+ { brand: "Chromium", version: major },
19
+ { brand, version: major },
20
+ ],
21
+ fullVersionList: [
22
+ { brand: "Chromium", version: fullVersion },
23
+ { brand, version: fullVersion },
24
+ ],
25
+ platform: windows ? "Windows" : "macOS",
26
+ platformVersion: windows ? "10.0.0" : "10.15.7",
27
+ architecture: "x86",
28
+ bitness: "64",
29
+ model: "",
30
+ mobile: false,
31
+ wow64: false,
32
+ },
33
+ };
34
+ }
35
+
36
+ /** Initial popup documents exist before Playwright can attach a page session. */
37
+ export function installDocumentIdentity(identity: NonNullable<ReturnType<typeof browserIdentity>>) {
38
+ const metadata = identity.userAgentMetadata;
39
+ const basics = { brands: metadata.brands, mobile: metadata.mobile, platform: metadata.platform };
40
+ Object.defineProperty(Navigator.prototype, "platform", {
41
+ configurable: true,
42
+ get: () => identity.platform,
43
+ });
44
+ if (!("userAgentData" in navigator)) return;
45
+ const nativeData = (navigator as Navigator & { userAgentData: object }).userAgentData;
46
+ const data = Object.create(Object.getPrototypeOf(nativeData));
47
+ for (const [name, value] of Object.entries(basics)) {
48
+ Object.defineProperty(data, name, { enumerable: true, get: () => value });
49
+ }
50
+ Object.defineProperty(data, "toJSON", { value: () => ({ ...basics }) });
51
+ Object.defineProperty(data, "getHighEntropyValues", {
52
+ value: async (hints: string[]) => {
53
+ const result: Record<string, unknown> = { ...basics };
54
+ for (const hint of hints) {
55
+ if (Object.hasOwn(metadata, hint)) result[hint] = metadata[hint as keyof typeof metadata];
56
+ }
57
+ return result;
58
+ },
59
+ });
60
+ Object.defineProperty(Navigator.prototype, "userAgentData", {
61
+ configurable: true,
62
+ get: () => data,
63
+ });
64
+ }
@@ -76,7 +76,14 @@ export type {
76
76
  OverlapResult,
77
77
  } from "./geometry.js";
78
78
  export { buildLayoutLintCheck } from "./geometry.js";
79
- export { isWSL, wslHeadedLaunchArgs } from "./launch-args.js";
79
+ export {
80
+ AUTOMATION_DEFAULT_ARGS_TO_DROP,
81
+ automationDisguiseArgs,
82
+ installedChromeChannel,
83
+ installedChromePath,
84
+ isWSL,
85
+ wslHeadedLaunchArgs,
86
+ } from "./launch-args.js";
80
87
  export type {
81
88
  OverflowElement,
82
89
  OverflowResult,
@@ -237,6 +244,13 @@ export {
237
244
  snappedBandRects,
238
245
  type VisualAtom,
239
246
  } from "./tiling.js";
247
+ export {
248
+ chromeMajorFromExecutable,
249
+ hostFamily,
250
+ normalUserAgent,
251
+ readStoredUserAgent,
252
+ resolveUserAgent,
253
+ } from "./user-agent.js";
240
254
  export type {
241
255
  CheckVisibilityOptions,
242
256
  VisibilityResult,
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
 
3
3
  /**
4
4
  * Chromium launch-arg helpers for environment-specific workarounds.
@@ -31,3 +31,55 @@ export function isWSL(): boolean {
31
31
  export function wslHeadedLaunchArgs(): string[] {
32
32
  return isWSL() ? ["--disable-gpu"] : [];
33
33
  }
34
+
35
+ /**
36
+ * Playwright's bundled browser is "Chrome for Testing", launched with
37
+ * `--enable-automation`. Both are visible to the page: the User-Agent brand
38
+ * list carries "Chrome for Testing" and `navigator.webdriver` reads true, so
39
+ * sign-in flows on sites that screen for automation (X, some banks, some
40
+ * Cloudflare-fronted apps) silently refuse clicks in a headed window that a
41
+ * person is driving. Headed sessions therefore launch the operator's real
42
+ * Google Chrome when one is installed and drop the automation signals.
43
+ */
44
+
45
+ /** Default Chromium args Playwright adds that a headed session must not carry. */
46
+ export const AUTOMATION_DEFAULT_ARGS_TO_DROP: readonly string[] = ["--enable-automation"];
47
+
48
+ /** Launch flags that stop Blink from reporting the automation state to pages. */
49
+ export function automationDisguiseArgs(): string[] {
50
+ return ["--disable-blink-features=AutomationControlled"];
51
+ }
52
+
53
+ const CHROME_PATHS: Record<string, string[]> = {
54
+ linux: ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/opt/google/chrome/chrome"],
55
+ darwin: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
56
+ win32: [
57
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
58
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
59
+ ],
60
+ };
61
+
62
+ /**
63
+ * `"chrome"` when a stable Google Chrome install is present at a well-known
64
+ * path for this platform, else `undefined` (use Playwright's bundled
65
+ * Chromium). Playwright resolves the `chrome` channel to the same install, so
66
+ * the check only has to answer whether asking for it can succeed.
67
+ */
68
+ export function installedChromeChannel(
69
+ platform: NodeJS.Platform = process.platform,
70
+ exists: (path: string) => boolean = existsSync,
71
+ ): "chrome" | undefined {
72
+ return installedChromePath(platform, exists) ? "chrome" : undefined;
73
+ }
74
+
75
+ /**
76
+ * Filesystem path of the installed stable Google Chrome for this platform, or
77
+ * `undefined`. Used to spawn Chrome with nothing attached for sign-in flows
78
+ * whose verification vendors detect the DevTools protocol itself.
79
+ */
80
+ export function installedChromePath(
81
+ platform: NodeJS.Platform = process.platform,
82
+ exists: (path: string) => boolean = existsSync,
83
+ ): string | undefined {
84
+ return (CHROME_PATHS[platform] ?? []).find((candidate) => exists(candidate));
85
+ }