@pi-in-go/pigpen-ahp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,674 @@
1
+ // Package ws is a minimal RFC 6455 WebSocket transport for the AHP host: a server (HTTP upgrade
2
+ // with token and Origin checks, text frames, fragmentation, ping/pong, close, size limits, a
3
+ // bounded outbound queue) and a small client used by tests. It replaces pi-ahp's
4
+ // src/transport/websocket.ts, which uses the `ws` package, so the extension needs no third-party
5
+ // module. AHP sends each JSON-RPC message as one text frame.
6
+ package ws
7
+
8
+ import (
9
+ "bufio"
10
+ "context"
11
+ "crypto/sha1"
12
+ "crypto/subtle"
13
+ "encoding/base64"
14
+ "encoding/binary"
15
+ "errors"
16
+ "fmt"
17
+ "io"
18
+ "net"
19
+ "net/http"
20
+ "net/url"
21
+ "strings"
22
+ "sync"
23
+ "time"
24
+ "unicode/utf8"
25
+
26
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
27
+ )
28
+
29
+ const guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
30
+
31
+ const (
32
+ opContinuation = 0x0
33
+ opText = 0x1
34
+ opBinary = 0x2
35
+ opClose = 0x8
36
+ opPing = 0x9
37
+ opPong = 0xA
38
+ )
39
+
40
+ // Close codes (RFC 6455 section 7.4).
41
+ const (
42
+ closeNormal = 1000
43
+ closeGoingAway = 1001
44
+ closeProtocolError = 1002
45
+ closeInvalidData = 1007
46
+ closeTooBig = 1009
47
+ )
48
+
49
+ // Options configure a Server.
50
+ type Options struct {
51
+ // Addr is the listen address, "127.0.0.1:0" for an ephemeral loopback port.
52
+ Addr string
53
+ // Token, when non-empty, must be presented as ?token= or ?tkn= (VS Code's spelling).
54
+ Token string
55
+ // AllowedOrigins lists browser Origins that may connect. A request with an Origin header that is
56
+ // not listed is refused; a request without one (native clients) is allowed.
57
+ AllowedOrigins []string
58
+ // MaxMessageBytes bounds one message (0: 16 MiB).
59
+ MaxMessageBytes int
60
+ // MaxQueuedFrames bounds the outbound queue of one peer (0: 4096); a peer that falls behind is closed.
61
+ MaxQueuedFrames int
62
+ // PingInterval is the keep-alive interval (0: 30 s); a peer that says nothing for two intervals is closed.
63
+ PingInterval time.Duration
64
+ Log func(string)
65
+ }
66
+
67
+ func (o *Options) defaults() {
68
+ if o.MaxMessageBytes <= 0 {
69
+ o.MaxMessageBytes = 16 << 20
70
+ }
71
+ if o.MaxQueuedFrames <= 0 {
72
+ o.MaxQueuedFrames = 4096
73
+ }
74
+ if o.PingInterval <= 0 {
75
+ o.PingInterval = 30 * time.Second
76
+ }
77
+ }
78
+
79
+ // AcceptKey computes Sec-WebSocket-Accept for a client key.
80
+ func AcceptKey(key string) string {
81
+ sum := sha1.Sum([]byte(key + guid))
82
+ return base64.StdEncoding.EncodeToString(sum[:])
83
+ }
84
+
85
+ // Server accepts WebSocket connections and hands each to the host.
86
+ type Server struct {
87
+ h *host.Host
88
+ opts Options
89
+ ln net.Listener
90
+ srv *http.Server
91
+
92
+ mu sync.Mutex
93
+ conns map[*serverConn]struct{}
94
+ closed bool
95
+ wg sync.WaitGroup
96
+ }
97
+
98
+ // Serve starts listening and wires each connection into the host.
99
+ func Serve(h *host.Host, opts Options) (*Server, error) {
100
+ opts.defaults()
101
+ ln, err := net.Listen("tcp", opts.Addr)
102
+ if err != nil {
103
+ return nil, err
104
+ }
105
+ s := &Server{h: h, opts: opts, ln: ln, conns: map[*serverConn]struct{}{}}
106
+ s.srv = &http.Server{Handler: http.HandlerFunc(s.handle), ReadHeaderTimeout: 10 * time.Second}
107
+ s.wg.Add(1)
108
+ go func() {
109
+ defer s.wg.Done()
110
+ _ = s.srv.Serve(ln)
111
+ }()
112
+ return s, nil
113
+ }
114
+
115
+ // Addr is the bound address.
116
+ func (s *Server) Addr() net.Addr { return s.ln.Addr() }
117
+
118
+ // Port is the bound port.
119
+ func (s *Server) Port() int { return s.ln.Addr().(*net.TCPAddr).Port }
120
+
121
+ // Close stops listening and closes every connection.
122
+ func (s *Server) Close() error {
123
+ s.mu.Lock()
124
+ if s.closed {
125
+ s.mu.Unlock()
126
+ return nil
127
+ }
128
+ s.closed = true
129
+ conns := make([]*serverConn, 0, len(s.conns))
130
+ for c := range s.conns {
131
+ conns = append(conns, c)
132
+ }
133
+ s.mu.Unlock()
134
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
135
+ defer cancel()
136
+ _ = s.srv.Shutdown(ctx) // hijacked connections are not tracked by http.Server
137
+ _ = s.ln.Close()
138
+ for _, c := range conns {
139
+ c.shutdown(closeGoingAway, "server closing")
140
+ }
141
+ s.wg.Wait()
142
+ return nil
143
+ }
144
+
145
+ func (s *Server) logf(format string, args ...any) {
146
+ if s.opts.Log != nil {
147
+ s.opts.Log(fmt.Sprintf(format, args...))
148
+ }
149
+ }
150
+
151
+ func headerHasToken(h http.Header, name, token string) bool {
152
+ for _, v := range h.Values(name) {
153
+ for _, part := range strings.Split(v, ",") {
154
+ if strings.EqualFold(strings.TrimSpace(part), token) {
155
+ return true
156
+ }
157
+ }
158
+ }
159
+ return false
160
+ }
161
+
162
+ func (s *Server) originAllowed(origin string) bool {
163
+ for _, allowed := range s.opts.AllowedOrigins {
164
+ if strings.EqualFold(allowed, origin) {
165
+ return true
166
+ }
167
+ }
168
+ return false
169
+ }
170
+
171
+ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
172
+ if !headerHasToken(r.Header, "Upgrade", "websocket") {
173
+ w.Header().Set("Content-Type", "text/plain")
174
+ w.WriteHeader(http.StatusUpgradeRequired)
175
+ _, _ = io.WriteString(w, "Upgrade required")
176
+ return
177
+ }
178
+ if s.opts.Token != "" {
179
+ q := r.URL.Query()
180
+ // VS Code calls a manually configured connection token `tkn`; generic AHP examples use
181
+ // `token`, so a protected direct listener accepts both.
182
+ presented, ok := "", false
183
+ if vals, has := q["token"]; has {
184
+ presented, ok = vals[0], true
185
+ } else if vals, has := q["tkn"]; has {
186
+ presented, ok = vals[0], true
187
+ }
188
+ if !ok || subtle.ConstantTimeCompare([]byte(presented), []byte(s.opts.Token)) != 1 {
189
+ // 403, as VS Code's agent host (the AHP reference host) answers a missing or wrong tkn; pi-ahp writes 401.
190
+ // A 401 would also owe a WWW-Authenticate challenge, which a query token has no scheme for.
191
+ s.logf("rejecting upgrade: bad token")
192
+ w.WriteHeader(http.StatusForbidden)
193
+ return
194
+ }
195
+ }
196
+ if origin := r.Header.Get("Origin"); origin != "" && !s.originAllowed(origin) {
197
+ s.logf("rejecting upgrade: origin %q is not allowed", origin)
198
+ w.WriteHeader(http.StatusForbidden)
199
+ return
200
+ }
201
+ key := r.Header.Get("Sec-WebSocket-Key")
202
+ if r.Method != http.MethodGet || !headerHasToken(r.Header, "Connection", "upgrade") ||
203
+ r.Header.Get("Sec-WebSocket-Version") != "13" || !validKey(key) {
204
+ w.Header().Set("Sec-WebSocket-Version", "13")
205
+ w.WriteHeader(http.StatusBadRequest)
206
+ return
207
+ }
208
+ hj, ok := w.(http.Hijacker)
209
+ if !ok {
210
+ w.WriteHeader(http.StatusInternalServerError)
211
+ return
212
+ }
213
+ conn, brw, err := hj.Hijack()
214
+ if err != nil {
215
+ return
216
+ }
217
+ if _, err := fmt.Fprintf(brw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", AcceptKey(key)); err != nil || brw.Flush() != nil {
218
+ conn.Close()
219
+ return
220
+ }
221
+ s.mu.Lock()
222
+ if s.closed {
223
+ s.mu.Unlock()
224
+ conn.Close()
225
+ return
226
+ }
227
+ sc := newServerConn(s, conn, brw.Reader)
228
+ s.conns[sc] = struct{}{}
229
+ s.wg.Add(1)
230
+ s.mu.Unlock()
231
+ sc.hostConn = s.h.Accept(sc)
232
+ go sc.run()
233
+ }
234
+
235
+ func validKey(key string) bool {
236
+ raw, err := base64.StdEncoding.DecodeString(key)
237
+ return err == nil && len(raw) == 16
238
+ }
239
+
240
+ // ── server connection ───────────────────────────────────────────────────
241
+
242
+ type outFrame struct {
243
+ opcode byte
244
+ payload []byte
245
+ close bool // the connection ends after this frame
246
+ }
247
+
248
+ type serverConn struct {
249
+ s *Server
250
+ conn net.Conn
251
+ br *bufio.Reader
252
+ hostConn *host.Conn
253
+
254
+ out chan outFrame // data frames
255
+ ctrl chan outFrame // control frames, sent ahead of data
256
+ done chan struct{}
257
+ stop chan struct{} // the reader is finished: flush control frames, then end the writer
258
+ doneOnce sync.Once
259
+ closing sync.Once
260
+ }
261
+
262
+ func newServerConn(s *Server, conn net.Conn, br *bufio.Reader) *serverConn {
263
+ return &serverConn{
264
+ s: s, conn: conn, br: br,
265
+ out: make(chan outFrame, s.opts.MaxQueuedFrames),
266
+ ctrl: make(chan outFrame, 16),
267
+ done: make(chan struct{}),
268
+ stop: make(chan struct{}),
269
+ }
270
+ }
271
+
272
+ // Send implements host.Transport. It never blocks: a peer whose queue is full is closed, which is
273
+ // safe because AHP clients recover through reconnect.
274
+ func (c *serverConn) Send(frame []byte) error {
275
+ select {
276
+ case <-c.done:
277
+ return errors.New("connection closed")
278
+ default:
279
+ }
280
+ select {
281
+ case c.out <- outFrame{opcode: opText, payload: frame}:
282
+ return nil
283
+ default:
284
+ c.s.logf("closing a peer that stopped reading (queue of %d frames full)", cap(c.out))
285
+ go c.shutdown(closeGoingAway, "too slow")
286
+ return errors.New("outbound queue full")
287
+ }
288
+ }
289
+
290
+ // Close implements host.Transport.
291
+ func (c *serverConn) Close() error {
292
+ go c.shutdown(closeNormal, "")
293
+ return nil
294
+ }
295
+
296
+ func closePayload(code uint16, reason string) []byte {
297
+ p := make([]byte, 2, 2+len(reason))
298
+ binary.BigEndian.PutUint16(p, code)
299
+ if len(reason) > 123 {
300
+ reason = reason[:123]
301
+ }
302
+ return append(p, reason...)
303
+ }
304
+
305
+ // shutdown sends a close frame, gives the peer a moment to answer, then drops the connection.
306
+ func (c *serverConn) shutdown(code uint16, reason string) {
307
+ c.closing.Do(func() {
308
+ select {
309
+ case c.ctrl <- outFrame{opcode: opClose, payload: closePayload(code, reason), close: true}:
310
+ case <-c.done:
311
+ return
312
+ }
313
+ select {
314
+ case <-c.done:
315
+ case <-time.After(time.Second):
316
+ c.finish()
317
+ }
318
+ })
319
+ }
320
+
321
+ func (c *serverConn) finish() {
322
+ c.doneOnce.Do(func() {
323
+ close(c.done)
324
+ _ = c.conn.Close()
325
+ })
326
+ }
327
+
328
+ func (c *serverConn) run() {
329
+ defer c.s.wg.Done()
330
+ writerDone := make(chan struct{})
331
+ go func() {
332
+ defer close(writerDone)
333
+ c.writeLoop()
334
+ }()
335
+ c.readLoop()
336
+ close(c.stop)
337
+ select {
338
+ case <-writerDone:
339
+ case <-time.After(2 * time.Second):
340
+ }
341
+ c.finish()
342
+ <-writerDone
343
+ c.s.mu.Lock()
344
+ delete(c.s.conns, c)
345
+ c.s.mu.Unlock()
346
+ c.hostConn.Closed()
347
+ }
348
+
349
+ func (c *serverConn) writeLoop() {
350
+ ping := time.NewTicker(c.s.opts.PingInterval)
351
+ defer ping.Stop()
352
+ write := func(f outFrame) bool {
353
+ _ = c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
354
+ if err := writeFrame(c.conn, f.opcode, f.payload, false); err != nil {
355
+ c.finish()
356
+ return false
357
+ }
358
+ if f.close {
359
+ // Closing handshake initiated by us: leave the peer time to echo; the reader ends on EOF.
360
+ return true
361
+ }
362
+ return true
363
+ }
364
+ for {
365
+ // Control frames first.
366
+ select {
367
+ case f := <-c.ctrl:
368
+ if !write(f) {
369
+ return
370
+ }
371
+ continue
372
+ default:
373
+ }
374
+ select {
375
+ case f := <-c.ctrl:
376
+ if !write(f) {
377
+ return
378
+ }
379
+ case f := <-c.out:
380
+ if !write(f) {
381
+ return
382
+ }
383
+ case <-ping.C:
384
+ if !write(outFrame{opcode: opPing}) {
385
+ return
386
+ }
387
+ case <-c.stop:
388
+ // Flush what the reader queued (the echo of a close, a protocol-error close), then end.
389
+ for {
390
+ select {
391
+ case f := <-c.ctrl:
392
+ if !write(f) {
393
+ return
394
+ }
395
+ default:
396
+ return
397
+ }
398
+ }
399
+ case <-c.done:
400
+ return
401
+ }
402
+ }
403
+ }
404
+
405
+ type frameHeader struct {
406
+ fin bool
407
+ rsv byte
408
+ opcode byte
409
+ masked bool
410
+ length uint64
411
+ maskKey [4]byte
412
+ }
413
+
414
+ func readHeader(r io.Reader) (frameHeader, error) {
415
+ var h frameHeader
416
+ var b [2]byte
417
+ if _, err := io.ReadFull(r, b[:]); err != nil {
418
+ return h, err
419
+ }
420
+ h.fin = b[0]&0x80 != 0
421
+ h.rsv = (b[0] >> 4) & 0x7
422
+ h.opcode = b[0] & 0x0f
423
+ h.masked = b[1]&0x80 != 0
424
+ h.length = uint64(b[1] & 0x7f)
425
+ switch h.length {
426
+ case 126:
427
+ var e [2]byte
428
+ if _, err := io.ReadFull(r, e[:]); err != nil {
429
+ return h, err
430
+ }
431
+ h.length = uint64(binary.BigEndian.Uint16(e[:]))
432
+ case 127:
433
+ var e [8]byte
434
+ if _, err := io.ReadFull(r, e[:]); err != nil {
435
+ return h, err
436
+ }
437
+ h.length = binary.BigEndian.Uint64(e[:])
438
+ if h.length>>63 != 0 {
439
+ return h, errors.New("frame length has the high bit set")
440
+ }
441
+ }
442
+ if h.masked {
443
+ if _, err := io.ReadFull(r, h.maskKey[:]); err != nil {
444
+ return h, err
445
+ }
446
+ }
447
+ return h, nil
448
+ }
449
+
450
+ func writeFrame(w io.Writer, opcode byte, payload []byte, mask bool) error {
451
+ hdr := []byte{0x80 | opcode}
452
+ maskBit := byte(0)
453
+ if mask {
454
+ maskBit = 0x80
455
+ }
456
+ switch n := len(payload); {
457
+ case n < 126:
458
+ hdr = append(hdr, maskBit|byte(n))
459
+ case n <= 0xffff:
460
+ hdr = append(hdr, maskBit|126, byte(n>>8), byte(n))
461
+ default:
462
+ var l [8]byte
463
+ binary.BigEndian.PutUint64(l[:], uint64(n))
464
+ hdr = append(hdr, maskBit|127)
465
+ hdr = append(hdr, l[:]...)
466
+ }
467
+ body := payload
468
+ if mask {
469
+ key := [4]byte{byte(time.Now().UnixNano()), 0x37, 0xfa, 0x21}
470
+ hdr = append(hdr, key[:]...)
471
+ body = make([]byte, len(payload))
472
+ for i, b := range payload {
473
+ body[i] = b ^ key[i%4]
474
+ }
475
+ }
476
+ _, err := w.Write(append(hdr, body...))
477
+ return err
478
+ }
479
+
480
+ // readMessage reads one full data message (reassembling fragments) and handles control frames
481
+ // through onControl. requireMask is true on the server side.
482
+ func readMessage(r *bufio.Reader, limit int, requireMask bool, onControl func(op byte, payload []byte) error) (opcode byte, msg []byte, code uint16, err error) {
483
+ var buf []byte
484
+ var first byte
485
+ inMessage := false
486
+ for {
487
+ h, err := readHeader(r)
488
+ if err != nil {
489
+ return 0, nil, 0, err
490
+ }
491
+ if h.rsv != 0 {
492
+ return 0, nil, closeProtocolError, errors.New("reserved bits set")
493
+ }
494
+ if requireMask && !h.masked {
495
+ return 0, nil, closeProtocolError, errors.New("client frames must be masked")
496
+ }
497
+ isControl := h.opcode&0x8 != 0
498
+ if isControl && (!h.fin || h.length > 125) {
499
+ return 0, nil, closeProtocolError, errors.New("invalid control frame")
500
+ }
501
+ switch h.opcode {
502
+ case opContinuation:
503
+ if !inMessage {
504
+ return 0, nil, closeProtocolError, errors.New("unexpected continuation frame")
505
+ }
506
+ case opText, opBinary:
507
+ if inMessage {
508
+ return 0, nil, closeProtocolError, errors.New("data frame inside a fragmented message")
509
+ }
510
+ case opClose, opPing, opPong:
511
+ default:
512
+ return 0, nil, closeProtocolError, fmt.Errorf("reserved opcode %d", h.opcode)
513
+ }
514
+ if !isControl && h.length > uint64(limit-len(buf)) {
515
+ return 0, nil, closeTooBig, errors.New("message too big")
516
+ }
517
+ payload := make([]byte, h.length)
518
+ if _, err := io.ReadFull(r, payload); err != nil {
519
+ return 0, nil, 0, err
520
+ }
521
+ if h.masked {
522
+ for i := range payload {
523
+ payload[i] ^= h.maskKey[i%4]
524
+ }
525
+ }
526
+ if isControl {
527
+ if err := onControl(h.opcode, payload); err != nil {
528
+ return h.opcode, nil, 0, err
529
+ }
530
+ continue
531
+ }
532
+ if !inMessage {
533
+ first = h.opcode
534
+ inMessage = true
535
+ }
536
+ buf = append(buf, payload...)
537
+ if h.fin {
538
+ if first == opText && !utf8.Valid(buf) {
539
+ return 0, nil, closeInvalidData, errors.New("invalid UTF-8 in a text message")
540
+ }
541
+ return first, buf, 0, nil
542
+ }
543
+ }
544
+ }
545
+
546
+ var errPeerClosed = errors.New("peer closed the connection")
547
+
548
+ func (c *serverConn) readLoop() {
549
+ idle := 2 * c.s.opts.PingInterval
550
+ for {
551
+ _ = c.conn.SetReadDeadline(time.Now().Add(idle))
552
+ _, msg, code, err := readMessage(c.br, c.s.opts.MaxMessageBytes, true, func(op byte, payload []byte) error {
553
+ _ = c.conn.SetReadDeadline(time.Now().Add(idle))
554
+ switch op {
555
+ case opPing:
556
+ select {
557
+ case c.ctrl <- outFrame{opcode: opPong, payload: payload}:
558
+ default:
559
+ }
560
+ case opClose:
561
+ echo := closePayload(closeNormal, "")
562
+ if len(payload) >= 2 {
563
+ echo = payload[:2]
564
+ }
565
+ c.closing.Do(func() {}) // the peer initiated: do not start our own handshake
566
+ select {
567
+ case c.ctrl <- outFrame{opcode: opClose, payload: echo, close: true}:
568
+ default:
569
+ }
570
+ return errPeerClosed
571
+ }
572
+ return nil
573
+ })
574
+ if err != nil {
575
+ if code != 0 {
576
+ c.closing.Do(func() {})
577
+ select {
578
+ case c.ctrl <- outFrame{opcode: opClose, payload: closePayload(code, ""), close: true}:
579
+ default:
580
+ }
581
+ }
582
+ return
583
+ }
584
+ c.hostConn.Receive(msg)
585
+ }
586
+ }
587
+
588
+ // ── client (tests and interop) ──────────────────────────────────────────
589
+
590
+ // Client is a WebSocket client connection.
591
+ type Client struct {
592
+ conn net.Conn
593
+ br *bufio.Reader
594
+ wmu sync.Mutex
595
+ }
596
+
597
+ // Dial connects to a ws:// URL. On a refused upgrade it returns the HTTP response.
598
+ func Dial(rawurl string, header http.Header) (*Client, *http.Response, error) {
599
+ u, err := url.Parse(rawurl)
600
+ if err != nil {
601
+ return nil, nil, err
602
+ }
603
+ if u.Scheme != "ws" {
604
+ return nil, nil, fmt.Errorf("ws: unsupported scheme %q", u.Scheme)
605
+ }
606
+ conn, err := net.DialTimeout("tcp", u.Host, 5*time.Second)
607
+ if err != nil {
608
+ return nil, nil, err
609
+ }
610
+ rawKey := make([]byte, 16)
611
+ for i := range rawKey {
612
+ rawKey[i] = byte(time.Now().UnixNano()>>uint(i)) ^ byte(i*31+7)
613
+ }
614
+ key := base64.StdEncoding.EncodeToString(rawKey)
615
+ path := u.RequestURI()
616
+ var sb strings.Builder
617
+ fmt.Fprintf(&sb, "GET %s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n", path, u.Host, key)
618
+ for k, vs := range header {
619
+ for _, v := range vs {
620
+ fmt.Fprintf(&sb, "%s: %s\r\n", k, v)
621
+ }
622
+ }
623
+ sb.WriteString("\r\n")
624
+ if _, err := io.WriteString(conn, sb.String()); err != nil {
625
+ conn.Close()
626
+ return nil, nil, err
627
+ }
628
+ br := bufio.NewReader(conn)
629
+ resp, err := http.ReadResponse(br, &http.Request{Method: http.MethodGet})
630
+ if err != nil {
631
+ conn.Close()
632
+ return nil, nil, err
633
+ }
634
+ if resp.StatusCode != http.StatusSwitchingProtocols || resp.Header.Get("Sec-WebSocket-Accept") != AcceptKey(key) {
635
+ conn.Close()
636
+ return nil, resp, fmt.Errorf("ws: upgrade refused: %s", resp.Status)
637
+ }
638
+ return &Client{conn: conn, br: br}, resp, nil
639
+ }
640
+
641
+ // WriteText sends one text message.
642
+ func (c *Client) WriteText(msg []byte) error {
643
+ c.wmu.Lock()
644
+ defer c.wmu.Unlock()
645
+ return writeFrame(c.conn, opText, msg, true)
646
+ }
647
+
648
+ // ReadMessage returns the next data message (text or binary), answering pings.
649
+ func (c *Client) ReadMessage(timeout time.Duration) ([]byte, error) {
650
+ _ = c.conn.SetReadDeadline(time.Now().Add(timeout))
651
+ _, msg, _, err := readMessage(c.br, 64<<20, false, func(op byte, payload []byte) error {
652
+ switch op {
653
+ case opPing:
654
+ c.wmu.Lock()
655
+ defer c.wmu.Unlock()
656
+ return writeFrame(c.conn, opPong, payload, true)
657
+ case opClose:
658
+ c.wmu.Lock()
659
+ _ = writeFrame(c.conn, opClose, payload, true)
660
+ c.wmu.Unlock()
661
+ return io.EOF
662
+ }
663
+ return nil
664
+ })
665
+ return msg, err
666
+ }
667
+
668
+ // Close sends a close frame and closes the connection.
669
+ func (c *Client) Close() error {
670
+ c.wmu.Lock()
671
+ _ = writeFrame(c.conn, opClose, closePayload(closeNormal, ""), true)
672
+ c.wmu.Unlock()
673
+ return c.conn.Close()
674
+ }