@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,218 @@
1
+ // Package wire holds the protocol-level helpers of the AHP host: channel URI classification,
2
+ // protocol errors, version negotiation, the client-dispatchable action table and file: URI
3
+ // conversion. It has no I/O. Ports pi-ahp src/core/channels.ts, uri.ts and src/protocol/*.
4
+ package wire
5
+
6
+ import (
7
+ "errors"
8
+ "fmt"
9
+ "net/url"
10
+ "path/filepath"
11
+ "regexp"
12
+ "runtime"
13
+ "strings"
14
+ )
15
+
16
+ // ResourceWatchScheme prefixes every resource watch channel URI ("<scheme>/<id>").
17
+ const ResourceWatchScheme = "ahp-resource-watch:"
18
+
19
+ // RootChannel is the singleton root channel.
20
+ const RootChannel = "ahp-root://"
21
+
22
+ // ChannelKind classifies a channel URI by the reducer that serves it.
23
+ type ChannelKind string
24
+
25
+ const (
26
+ KindRoot ChannelKind = "root"
27
+ KindSession ChannelKind = "session"
28
+ KindChat ChannelKind = "chat"
29
+ KindTerminal ChannelKind = "terminal"
30
+ KindChangeset ChannelKind = "changeset"
31
+ KindResourceWatch ChannelKind = "resourceWatch"
32
+ )
33
+
34
+ // JSON-RPC and AHP error codes (pinned spec: errors.generated.go).
35
+ const (
36
+ CodeParseError = -32700
37
+ CodeInvalidRequest = -32600
38
+ CodeMethodNotFound = -32601
39
+ CodeInvalidParams = -32602
40
+ CodeInternalError = -32603
41
+ CodeSessionNotFound = -32001
42
+ CodeProviderNotFound = -32002
43
+ CodeSessionAlreadyExists = -32003
44
+ CodeTurnInProgress = -32004
45
+ CodeUnsupportedProtocolVersion = -32005
46
+ CodeAuthRequired = -32007
47
+ CodeNotFound = -32008
48
+ CodePermissionDenied = -32009
49
+ CodeAlreadyExists = -32010
50
+ // CodeConflict is the code of the npm 0.9.0 package pi-ahp was written against (errors.ts,
51
+ // AhpErrorCodes.Conflict); the pinned spec commit has no such code, see PORT.md.
52
+ CodeConflict = -32011
53
+ )
54
+
55
+ // ProtocolVersion is the one AHP version this host speaks (pinned: 0.9.0).
56
+ const ProtocolVersion = "0.9.0"
57
+
58
+ // Error is a protocol error: handlers return it and the router turns it into a JSON-RPC error response.
59
+ type Error struct {
60
+ Code int
61
+ Message string
62
+ Data any
63
+ }
64
+
65
+ func (e *Error) Error() string { return e.Message }
66
+
67
+ const (
68
+ sessionScheme = "ahp-session:"
69
+ chatScheme = "ahp-chat:"
70
+ terminalScheme = "ahp-terminal:"
71
+ changesetScheme = "ahp-changeset:"
72
+ resourceWatchScheme = "ahp-resource-watch:"
73
+ )
74
+
75
+ // KindOf classifies a channel URI, false for a scheme this host does not serve. Deliberately
76
+ // strict: it drives subscribe, where guessing wrong would create a channel with the wrong reducer.
77
+ func KindOf(uri string) (ChannelKind, bool) {
78
+ switch {
79
+ case uri == RootChannel:
80
+ return KindRoot, true
81
+ case strings.HasPrefix(uri, sessionScheme+"/"):
82
+ return KindSession, true
83
+ case strings.HasPrefix(uri, chatScheme+"/"):
84
+ return KindChat, true
85
+ case strings.HasPrefix(uri, terminalScheme+"/"):
86
+ return KindTerminal, true
87
+ case strings.HasPrefix(uri, changesetScheme+"/"):
88
+ return KindChangeset, true
89
+ case strings.HasPrefix(uri, resourceWatchScheme+"/"):
90
+ return KindResourceWatch, true
91
+ }
92
+ return "", false
93
+ }
94
+
95
+ // ActionBelongsToChannel reports whether an action type's namespace belongs to a channel kind.
96
+ func ActionBelongsToChannel(actionType string, kind ChannelKind) bool {
97
+ return strings.HasPrefix(actionType, string(kind)+"/")
98
+ }
99
+
100
+ // IsChatChannel reports whether the URI is `ahp-chat:/…`.
101
+ func IsChatChannel(uri string) bool { return strings.HasPrefix(uri, chatScheme+"/") }
102
+
103
+ // SessionURI returns `ahp-session:/<id>`.
104
+ func SessionURI(id string) string { return sessionScheme + "/" + id }
105
+
106
+ // ChatURI returns `ahp-chat:/<id>`.
107
+ func ChatURI(id string) string { return chatScheme + "/" + id }
108
+
109
+ // ChatIDFromURI extracts `<id>` from `ahp-chat:/<id>`.
110
+ func ChatIDFromURI(uri string) (string, bool) {
111
+ if !IsChatChannel(uri) {
112
+ return "", false
113
+ }
114
+ id := uri[len(chatScheme)+1:]
115
+ return id, id != ""
116
+ }
117
+
118
+ var singleSegment = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9+.-]*):/([^/?#]+)$`)
119
+
120
+ // SessionIDFromURI identifies a session from its canonical scheme or from explicit provider
121
+ // aliases given as bare scheme names. Shape alone never makes an unknown URI a session.
122
+ func SessionIDFromURI(uri string, providerAliases ...string) (string, bool) {
123
+ m := singleSegment.FindStringSubmatch(uri)
124
+ if m == nil {
125
+ return "", false
126
+ }
127
+ scheme := strings.ToLower(m[1])
128
+ if scheme == "ahp-session" {
129
+ return m[2], true
130
+ }
131
+ for _, alias := range providerAliases {
132
+ if scheme == strings.ToLower(alias) {
133
+ return m[2], true
134
+ }
135
+ }
136
+ return "", false
137
+ }
138
+
139
+ func newError(code int, message string, data any) *Error {
140
+ return &Error{Code: code, Message: message, Data: data}
141
+ }
142
+
143
+ // InvalidParams and the other constructors mirror pi-ahp's ProtocolError statics.
144
+ // Coded builds a protocol error with an explicit code.
145
+ func Coded(code int, message string) *Error { return newError(code, message, nil) }
146
+
147
+ func InvalidParams(message string) *Error { return newError(CodeInvalidParams, message, nil) }
148
+ func MethodNotFound(method string) *Error {
149
+ return newError(CodeMethodNotFound, "Unknown method: "+method, nil)
150
+ }
151
+ func SessionNotFound(channel string) *Error {
152
+ return newError(CodeSessionNotFound, "Session not found: "+channel, nil)
153
+ }
154
+ func SessionAlreadyExists(channel string) *Error {
155
+ return newError(CodeSessionAlreadyExists, "Session already exists: "+channel, nil)
156
+ }
157
+ func ProviderNotFound(provider string) *Error {
158
+ return newError(CodeProviderNotFound, "No agent for provider: "+provider, nil)
159
+ }
160
+ func NotFound(uri string) *Error { return newError(CodeNotFound, "Not found: "+uri, nil) }
161
+
162
+ // HostSupportedVersions is what this host can speak. Generated wire models do not down-convert
163
+ // required fields across minor versions, so it is the current version only.
164
+ func HostSupportedVersions() []string { return []string{ProtocolVersion} }
165
+
166
+ // NegotiateProtocolVersion picks the client's most-preferred version this host also speaks.
167
+ func NegotiateProtocolVersion(offered []string) (string, error) {
168
+ if len(offered) == 0 {
169
+ return "", InvalidParams("initialize requires a non-empty protocolVersions array of strings")
170
+ }
171
+ for _, candidate := range offered {
172
+ for _, supported := range HostSupportedVersions() {
173
+ if candidate == supported {
174
+ return candidate, nil
175
+ }
176
+ }
177
+ }
178
+ return "", newError(CodeUnsupportedProtocolVersion,
179
+ "No mutually supported protocol version. Offered: "+strings.Join(offered, ", "),
180
+ map[string]any{"supportedVersions": HostSupportedVersions()})
181
+ }
182
+
183
+ // IsClientDispatchable reports whether a client may dispatch an action of this type.
184
+ func IsClientDispatchable(actionType string) bool { return clientDispatchable[actionType] }
185
+
186
+ // FileURIToPath converts a file: URI to a filesystem path. A value that is not a file: URI is
187
+ // returned unchanged: clients occasionally send a bare path where the protocol asks for a URI.
188
+ // A URI naming a remote host is an error (Node's fileURLToPath does the same).
189
+ func FileURIToPath(uri string) (string, error) {
190
+ if !strings.HasPrefix(uri, "file://") {
191
+ return uri, nil
192
+ }
193
+ u, err := url.Parse(uri)
194
+ if err != nil {
195
+ return "", fmt.Errorf("invalid file URL %q: %w", uri, err)
196
+ }
197
+ if u.Host != "" && u.Host != "localhost" {
198
+ return "", fmt.Errorf("file URL host must be \"localhost\" or empty on %s: %q", runtime.GOOS, uri)
199
+ }
200
+ p := u.Path
201
+ if runtime.GOOS == "windows" {
202
+ p = strings.TrimPrefix(p, "/")
203
+ return filepath.FromSlash(p), nil
204
+ }
205
+ if strings.Contains(u.EscapedPath(), "%2F") || strings.Contains(u.EscapedPath(), "%2f") {
206
+ return "", errors.New("file URL path must not include encoded / characters")
207
+ }
208
+ return p, nil
209
+ }
210
+
211
+ // PathToFileURI converts an absolute path to a file: URI.
212
+ func PathToFileURI(path string) string {
213
+ p := filepath.ToSlash(path)
214
+ if !strings.HasPrefix(p, "/") {
215
+ p = "/" + p
216
+ }
217
+ return (&url.URL{Scheme: "file", Path: p}).String()
218
+ }
@@ -0,0 +1,123 @@
1
+ package wire_test
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+
7
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
8
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
9
+ )
10
+
11
+ // Twins of upstream test/uri.test.ts (src/core/uri.ts).
12
+ func TestFileURIConversion(t *testing.T) {
13
+ twin.Run(t, "uri", "decodes percent-escapes", func(t *testing.T) {
14
+ got, err := wire.FileURIToPath("file:///tmp/a%20b")
15
+ if err != nil || got != "/tmp/a b" {
16
+ t.Fatalf("got %q, %v; want /tmp/a b", got, err)
17
+ }
18
+ })
19
+ twin.Run(t, "uri", "round-trips a path", func(t *testing.T) {
20
+ got, err := wire.FileURIToPath(wire.PathToFileURI("/tmp/a b/c"))
21
+ if err != nil || got != "/tmp/a b/c" {
22
+ t.Fatalf("got %q, %v; want /tmp/a b/c", got, err)
23
+ }
24
+ })
25
+ twin.Run(t, "uri", "passes a bare path through", func(t *testing.T) {
26
+ // Clients occasionally send a path where the protocol asks for a URI.
27
+ got, err := wire.FileURIToPath("/tmp/plain")
28
+ if err != nil || got != "/tmp/plain" {
29
+ t.Fatalf("got %q, %v; want /tmp/plain", got, err)
30
+ }
31
+ })
32
+ twin.Run(t, "uri", "refuses a URI naming a remote host", func(t *testing.T) {
33
+ if got, err := wire.FileURIToPath("file://host/share/x"); err == nil {
34
+ t.Fatalf("expected an error, got %q", got)
35
+ }
36
+ })
37
+ }
38
+
39
+ // Channel helpers (src/core/channels.ts); no upstream test file, so these are additional Go cases.
40
+ func TestChannelKinds(t *testing.T) {
41
+ cases := map[string]wire.ChannelKind{
42
+ "ahp-root://": wire.KindRoot,
43
+ "ahp-session:/abc": wire.KindSession,
44
+ "ahp-chat:/abc": wire.KindChat,
45
+ "ahp-terminal:/t1": wire.KindTerminal,
46
+ "ahp-changeset:/c": wire.KindChangeset,
47
+ "ahp-resource-watch:/w": wire.KindResourceWatch,
48
+ }
49
+ for uri, want := range cases {
50
+ if got, ok := wire.KindOf(uri); !ok || got != want {
51
+ t.Errorf("KindOf(%q) = %v, %v; want %v", uri, got, ok, want)
52
+ }
53
+ }
54
+ for _, uri := range []string{"ahp-unknown:/x", "pi:/abc", "", "ahp-session:"} {
55
+ if got, ok := wire.KindOf(uri); ok {
56
+ t.Errorf("KindOf(%q) = %v, want unsupported", uri, got)
57
+ }
58
+ }
59
+ if !wire.ActionBelongsToChannel("chat/delta", wire.KindChat) || wire.ActionBelongsToChannel("chat/delta", wire.KindSession) {
60
+ t.Error("action namespace must match the channel kind")
61
+ }
62
+ if wire.ActionBelongsToChannel("resourceWatch/changed", wire.KindResourceWatch) != true {
63
+ t.Error("resourceWatch/ belongs to resourceWatch")
64
+ }
65
+ if wire.SessionURI("x") != "ahp-session:/x" || wire.ChatURI("x") != "ahp-chat:/x" {
66
+ t.Error("uri constructors")
67
+ }
68
+ if id, ok := wire.ChatIDFromURI("ahp-chat:/abc"); !ok || id != "abc" {
69
+ t.Errorf("ChatIDFromURI = %q, %v", id, ok)
70
+ }
71
+ if _, ok := wire.ChatIDFromURI("ahp-session:/abc"); ok {
72
+ t.Error("a session URI is not a chat")
73
+ }
74
+ if id, ok := wire.SessionIDFromURI("ahp-session:/abc"); !ok || id != "abc" {
75
+ t.Errorf("SessionIDFromURI canonical = %q, %v", id, ok)
76
+ }
77
+ if id, ok := wire.SessionIDFromURI("pi:/abc", "pi"); !ok || id != "abc" {
78
+ t.Errorf("SessionIDFromURI alias = %q, %v", id, ok)
79
+ }
80
+ if _, ok := wire.SessionIDFromURI("pi:/abc"); ok {
81
+ t.Error("shape alone never makes an unknown URI a session")
82
+ }
83
+ if _, ok := wire.SessionIDFromURI("ahp-session:/a/b"); ok {
84
+ t.Error("a session URI has a single path segment")
85
+ }
86
+ }
87
+
88
+ func TestNegotiateProtocolVersion(t *testing.T) {
89
+ got, err := wire.NegotiateProtocolVersion([]string{"0.9.0", "0.8.0"})
90
+ if err != nil || got != "0.9.0" {
91
+ t.Fatalf("got %q, %v", got, err)
92
+ }
93
+ got, err = wire.NegotiateProtocolVersion([]string{"99.0.0", "0.9.0"})
94
+ if err != nil || got != "0.9.0" {
95
+ t.Fatalf("unknown versions are skipped: got %q, %v", got, err)
96
+ }
97
+ _, err = wire.NegotiateProtocolVersion([]string{"0.8.0"})
98
+ var pe *wire.Error
99
+ if !asError(err, &pe) || pe.Code != wire.CodeUnsupportedProtocolVersion {
100
+ t.Fatalf("want UnsupportedProtocolVersion, got %v", err)
101
+ }
102
+ if !reflect.DeepEqual(pe.Data, map[string]any{"supportedVersions": []string{"0.9.0"}}) {
103
+ t.Fatalf("error data = %#v", pe.Data)
104
+ }
105
+ if _, err = wire.NegotiateProtocolVersion(nil); err == nil {
106
+ t.Fatal("an empty offer is invalid")
107
+ }
108
+ }
109
+
110
+ // The table is generated from the pinned spec's @clientDispatchable annotations
111
+ // (types/action-origin.generated.ts, IS_CLIENT_DISPATCHABLE); this checks a sample of both kinds.
112
+ func TestClientDispatchable(t *testing.T) {
113
+ for _, typ := range []string{"root/configChanged", "session/titleChanged", "chat/turnStarted", "chat/turnCancelled", "chat/truncated", "terminal/input", "terminal/cleared"} {
114
+ if !wire.IsClientDispatchable(typ) {
115
+ t.Errorf("%s must be client-dispatchable", typ)
116
+ }
117
+ }
118
+ for _, typ := range []string{"root/agentsChanged", "session/ready", "chat/delta", "chat/turnComplete", "terminal/data", "resourceWatch/changed", "nope/unknown"} {
119
+ if wire.IsClientDispatchable(typ) {
120
+ t.Errorf("%s must be server-only", typ)
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,7 @@
1
+ package ws_test
2
+
3
+ import "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
4
+
5
+ func activeSessions(n int64) ahptypes.StateAction {
6
+ return ahptypes.StateAction{Value: &ahptypes.RootActiveSessionsChangedAction{Type: ahptypes.ActionTypeRootActiveSessionsChanged, ActiveSessions: n}}
7
+ }
@@ -0,0 +1,198 @@
1
+ package ws_test
2
+
3
+ import (
4
+ "bufio"
5
+ "encoding/json"
6
+ "io"
7
+ "net"
8
+ "net/http"
9
+ "strings"
10
+ "testing"
11
+ "time"
12
+
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
14
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
15
+ "github.com/MichaelKinsy/pigpen/ahp/internal/ws"
16
+ )
17
+
18
+ // VS Code's Agents window connects to a WebSocket AHP host given as ws://<host>:<port>?tkn=<token>
19
+ // (command "Agents: Add Remote Agent Host...", or the chat.remoteAgentHosts setting). These tests send what VS Code sends, as
20
+ // read from microsoft/vscode main; nothing here was run against a live VS Code. Files are under src/vs/platform/agentHost/.
21
+
22
+ // vsCodeInitialize is VS Code's first message, field for field (browser/agentHostProtocolClient.ts, connect): request id 1
23
+ // (_nextRequestId starts at 1), the root channel, every protocol version it can speak newest first with 0.8.0 left out
24
+ // (CLIENT_SUPPORTED_PROTOCOL_VERSIONS, from common/state/protocol/version/registry.ts SUPPORTED_PROTOCOL_VERSIONS), a UUID
25
+ // client id, the Agents window's clientInfo (common/agentHostClientInfo.ts), the _meta of toAgentHostClientMeta
26
+ // (common/agentHostTelemetry.ts) for a direct WebSocket, and the root as the one initial subscription.
27
+ func vsCodeInitialize(t *testing.T) []byte { return vsCodeInitializeOffering(t, vsCodeOffers["main"]) }
28
+
29
+ // vsCodeOffers is the protocolVersions list each VS Code sends: 1.140.0 speaks 0.9.0 itself (PROTOCOL_VERSION), release/1.141
30
+ // and main speak 0.10.0 and still offer 0.9.0.
31
+ var vsCodeOffers = map[string][]string{
32
+ "1.140.0": {"0.9.0", "0.7.0", "0.6.0", "0.5.2", "0.5.1"},
33
+ "main": {"0.10.0", "0.9.0", "0.7.0", "0.6.0", "0.5.2", "0.5.1"},
34
+ }
35
+
36
+ func vsCodeInitializeOffering(t *testing.T, versions []string) []byte {
37
+ t.Helper()
38
+ req, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{
39
+ "channel": wire.RootChannel,
40
+ "protocolVersions": []string{"0.10.0", "0.9.0", "0.7.0", "0.6.0", "0.5.2", "0.5.1"},
41
+ "clientId": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
42
+ "clientInfo": map[string]any{"name": "vscode-agents-window", "title": "VS Code Agents Window"},
43
+ "_meta": map[string]any{"vscode.telemetryLevel": "all", "vscode.clientConnectionKind": "direct_websocket"},
44
+ "initialSubscriptions": []string{wire.RootChannel},
45
+ }})
46
+ if err != nil {
47
+ t.Fatal(err)
48
+ }
49
+ return req
50
+ }
51
+
52
+ // vsCodeRootConfig is one of the notifications VS Code sends right after initialize (_forwardClientConfig, which dispatches
53
+ // root/configChanged with client sequence 0 for each forwarded setting: _dispatchRootConfig).
54
+ func vsCodeRootConfig(t *testing.T, config map[string]any) []byte {
55
+ t.Helper()
56
+ msg, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "method": "dispatchAction", "params": map[string]any{
57
+ "channel": wire.RootChannel, "clientSeq": 0, "action": map[string]any{"type": "root/configChanged", "config": config},
58
+ }})
59
+ if err != nil {
60
+ t.Fatal(err)
61
+ }
62
+ return msg
63
+ }
64
+
65
+ // rawAt upgrades by hand at the request target VS Code's address produces, so a test can see opcodes and the status line.
66
+ func rawAt(t *testing.T, s *ws.Server, target string) (net.Conn, *bufio.Reader, *http.Response) {
67
+ t.Helper()
68
+ conn, err := net.Dial("tcp", "127.0.0.1:"+itoa(s.Port()))
69
+ if err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ t.Cleanup(func() { conn.Close() })
73
+ io.WriteString(conn, "GET "+target+" HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n")
74
+ br := bufio.NewReader(conn)
75
+ resp, err := http.ReadResponse(br, nil)
76
+ if err != nil {
77
+ t.Fatal(err)
78
+ }
79
+ return conn, br, resp
80
+ }
81
+
82
+ func TestVSCodeHandshakeNegotiatesTheSupportedVersionOverTextFrames(t *testing.T) {
83
+ _, s := serve(t, ws.Options{Token: "s3cret"})
84
+ conn, br, resp := rawAt(t, s, "/?tkn=s3cret")
85
+ if resp.StatusCode != http.StatusSwitchingProtocols {
86
+ t.Fatalf("upgrade with the right tkn = %d", resp.StatusCode)
87
+ }
88
+ if _, err := conn.Write(maskedFrame(true, 1, vsCodeInitialize(t))); err != nil {
89
+ t.Fatal(err)
90
+ }
91
+
92
+ conn.SetReadDeadline(time.Now().Add(2 * time.Second))
93
+ op, payload := readFrame(t, br)
94
+ if op != 1 {
95
+ t.Fatalf("the reply was opcode %d; VS Code drops every non-text frame and closes the connection (4002) after ten (browser/webSocketClientTransport.ts)", op)
96
+ }
97
+ var reply struct {
98
+ Result struct {
99
+ ProtocolVersion string `json:"protocolVersion"`
100
+ Snapshots []struct {
101
+ Resource string `json:"resource"`
102
+ State struct {
103
+ Agents []struct {
104
+ Provider string `json:"provider"`
105
+ } `json:"agents"`
106
+ } `json:"state"`
107
+ } `json:"snapshots"`
108
+ } `json:"result"`
109
+ }
110
+ if err := json.Unmarshal(payload, &reply); err != nil {
111
+ t.Fatalf("%s: %v", payload, err)
112
+ }
113
+ if reply.Result.ProtocolVersion != "0.9.0" {
114
+ t.Fatalf("VS Code's offer (0.10.0, 0.9.0, 0.7.0, 0.6.0, 0.5.2, 0.5.1) negotiated %q, want the one version this host speaks, 0.9.0", reply.Result.ProtocolVersion)
115
+ }
116
+ if len(reply.Result.Snapshots) != 1 || reply.Result.Snapshots[0].Resource != wire.RootChannel {
117
+ t.Fatalf("the initial root subscription returned %+v", reply.Result.Snapshots)
118
+ }
119
+ for _, agent := range reply.Result.Snapshots[0].State.Agents {
120
+ if agent.Provider == "" || strings.Contains(agent.Provider, "-") {
121
+ t.Errorf("agent provider id %q must be non-empty and dash-free", agent.Provider)
122
+ }
123
+ }
124
+
125
+ // Then, as VS Code does, forward settings as root/configChanged notifications and keep using the connection (VS Code's
126
+ // keep-alive ping is {"channel":"ahp-root://"}): the host must neither drop it nor answer with anything but text frames.
127
+ for _, config := range []map[string]any{{"telemetryLevel": "all"}, {"terminalAutoApproveEnabled": true}} {
128
+ if _, err := conn.Write(maskedFrame(true, 1, vsCodeRootConfig(t, config))); err != nil {
129
+ t.Fatal(err)
130
+ }
131
+ }
132
+ if _, err := conn.Write(maskedFrame(true, 1, []byte(`{"jsonrpc":"2.0","id":2,"method":"ping","params":{"channel":"ahp-root://"}}`))); err != nil {
133
+ t.Fatal(err)
134
+ }
135
+ conn.SetReadDeadline(time.Now().Add(2 * time.Second))
136
+ for {
137
+ op, payload := readFrame(t, br)
138
+ if op != 1 {
139
+ t.Fatalf("a later frame was opcode %d, want text", op)
140
+ }
141
+ var msg struct {
142
+ ID *int `json:"id"`
143
+ Error json.RawMessage `json:"error"`
144
+ }
145
+ if err := json.Unmarshal(payload, &msg); err != nil {
146
+ t.Fatalf("%s: %v", payload, err)
147
+ }
148
+ if msg.ID != nil && *msg.ID == 2 {
149
+ if msg.Error != nil {
150
+ t.Fatalf("ping after the config notifications failed: %s", payload)
151
+ }
152
+ break
153
+ }
154
+ }
155
+ }
156
+
157
+ func TestVSCodeAddressFormIsAccepted(t *testing.T) {
158
+ _, s := serve(t, ws.Options{Token: "s3cret"})
159
+ for release, versions := range vsCodeOffers {
160
+ // The address VS Code is given has no slash before the query.
161
+ c, _, err := ws.Dial("ws://127.0.0.1:"+itoa(s.Port())+"?tkn=s3cret", nil)
162
+ if err != nil {
163
+ t.Fatal(err)
164
+ }
165
+ if err := c.WriteText(vsCodeInitializeOffering(t, versions)); err != nil {
166
+ t.Fatal(err)
167
+ }
168
+ msg, err := c.ReadMessage(2 * time.Second)
169
+ c.Close()
170
+ if err != nil || !strings.Contains(string(msg), `"protocolVersion":"0.9.0"`) {
171
+ t.Fatalf("VS Code %s: reply %s (%v)", release, msg, err)
172
+ }
173
+ }
174
+ }
175
+
176
+ func TestVSCodeWrongOrMissingTknIsRefusedAtTheUpgrade(t *testing.T) {
177
+ _, s := serve(t, ws.Options{Token: "s3cret"})
178
+ for _, target := range []string{"/?tkn=wrong", "/?tkn=", "/", "/?TKN=s3cret"} {
179
+ _, _, resp := rawAt(t, s, target)
180
+ // 403 Forbidden with no upgrade, as VS Code's own agent host answers a missing or wrong tkn
181
+ // (microsoft/vscode src/vs/platform/agentHost/node/webSocketTransport.ts, verifyClient: cb(false, 403, 'Forbidden')).
182
+ // The AHP spec leaves endpoint access to the transport (docs/specification/transport.md, "Authentication"), and
183
+ // VS Code's client cannot see the status: a browser WebSocket reports any refused upgrade as one error.
184
+ if resp.StatusCode != http.StatusForbidden {
185
+ t.Errorf("GET %s = %d, want 403 and no upgrade", target, resp.StatusCode)
186
+ }
187
+ }
188
+ }
189
+
190
+ func TestProviderIDIsDashFree(t *testing.T) {
191
+ // AHP session URIs are <provider>:/<id>; VS Code parses the scheme, so a dash in the provider breaks them.
192
+ if pi.Provider == "" || strings.Contains(pi.Provider, "-") {
193
+ t.Fatalf("provider id %q must be non-empty and dash-free", pi.Provider)
194
+ }
195
+ if got := pi.BuildAgentInfo(nil).Provider; got != pi.Provider {
196
+ t.Fatalf("the agent advertises provider %q, not %q", got, pi.Provider)
197
+ }
198
+ }