@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,1305 @@
1
+ // Package host is the AHP host: message routing, subscriptions, sequencing, broadcast and
2
+ // the authoritative channel state. It is a port of pi-ahp's src/core (host.ts, sequencer.ts,
3
+ // state-store.ts, connection.ts, client-workarounds.ts) and knows nothing about PiG.
4
+ //
5
+ // Concurrency: the host serializes its own state with one mutex. Handlers registered
6
+ // through Capabilities and listeners run outside it, so they may call back into the host.
7
+ package host
8
+
9
+ import (
10
+ "bytes"
11
+ "context"
12
+ "encoding/json"
13
+ "errors"
14
+ "fmt"
15
+ "sync"
16
+
17
+ "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
18
+
19
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
20
+ )
21
+
22
+ // Transport is one reliable, ordered, message-framed stream. Send must not block on a slow
23
+ // peer (the WebSocket transport queues and closes a peer that falls behind): the host
24
+ // broadcasts from inside its critical section.
25
+ type Transport interface {
26
+ Send(frame []byte) error
27
+ Close() error
28
+ }
29
+
30
+ // Options configure a Host.
31
+ type Options struct {
32
+ // ServerInfo is advertised on InitializeResult.serverInfo. Informational only.
33
+ ServerInfo *ahptypes.Implementation
34
+ // DefaultDirectory is the starting location for remote filesystem browsing, as a file: URI.
35
+ DefaultDirectory string
36
+ // CompletionTriggerCharacters make a client issue a completions request.
37
+ CompletionTriggerCharacters []string
38
+ // ReplayBufferCapacity bounds the replay buffer; 0 selects the default (1000).
39
+ ReplayBufferCapacity int
40
+ Log func(string)
41
+ }
42
+
43
+ // Capabilities are the optional protocol surfaces. A request for an absent surface receives an
44
+ // empty result where meaningful and MethodNotFound otherwise.
45
+ type Capabilities struct {
46
+ Catalogue SessionCatalogue
47
+ Sessions SessionLifecycle
48
+ Terminals TerminalHandler
49
+ Resources ResourceHandler
50
+ ResourceWatches ResourceWatchHandler
51
+ Completions CompletionHandler
52
+ SessionConfig SessionConfigHandler
53
+ TurnPaging TurnPagingHandler
54
+ Hydrator ChannelHydrator
55
+ }
56
+
57
+ // SessionCatalogue supplies the session list behind listSessions.
58
+ type SessionCatalogue interface {
59
+ List(ctx context.Context, limit *int64, cursor *string) (ahptypes.ListSessionsResult, error)
60
+ }
61
+
62
+ // SessionLifecycle handles createSession and disposeSession.
63
+ type SessionLifecycle interface {
64
+ Create(ctx context.Context, params ahptypes.CreateSessionParams) error
65
+ Dispose(ctx context.Context, channel string) error
66
+ }
67
+
68
+ // TerminalHandler handles createTerminal and disposeTerminal.
69
+ type TerminalHandler interface {
70
+ Create(ctx context.Context, params ahptypes.CreateTerminalParams, clientID string) error
71
+ Dispose(ctx context.Context, channel string) error
72
+ }
73
+
74
+ // ResourceHandler serves the host side of the connection-level resource* family.
75
+ type ResourceHandler interface {
76
+ Read(ctx context.Context, p ahptypes.ResourceReadParams) (ahptypes.ResourceReadResult, error)
77
+ Write(ctx context.Context, p ahptypes.ResourceWriteParams) error
78
+ List(ctx context.Context, uri string) (ahptypes.ResourceListResult, error)
79
+ Resolve(ctx context.Context, p ahptypes.ResourceResolveParams) (ahptypes.ResourceResolveResult, error)
80
+ Mkdir(ctx context.Context, p ahptypes.ResourceMkdirParams) error
81
+ Delete(ctx context.Context, p ahptypes.ResourceDeleteParams) error
82
+ Move(ctx context.Context, p ahptypes.ResourceMoveParams) error
83
+ Copy(ctx context.Context, p ahptypes.ResourceCopyParams) error
84
+ }
85
+
86
+ // ResourceWatchHandler opens filesystem watchers.
87
+ type ResourceWatchHandler interface {
88
+ Create(ctx context.Context, p ahptypes.CreateResourceWatchParams) (ahptypes.CreateResourceWatchResult, error)
89
+ }
90
+
91
+ // CompletionHandler serves inline completions for a chat's message input.
92
+ type CompletionHandler interface {
93
+ Complete(ctx context.Context, p ahptypes.CompletionsParams) (ahptypes.CompletionsResult, error)
94
+ }
95
+
96
+ // SessionConfigHandler serves the pre-creation session configuration exchange.
97
+ type SessionConfigHandler interface {
98
+ Resolve(ctx context.Context, p ahptypes.ResolveSessionConfigParams) (ahptypes.ResolveSessionConfigResult, error)
99
+ Completions(ctx context.Context, p ahptypes.SessionConfigCompletionsParams) (ahptypes.SessionConfigCompletionsResult, error)
100
+ }
101
+
102
+ // TurnPagingHandler loads older turns into a chat; the page is dispatched before it returns.
103
+ type TurnPagingHandler interface {
104
+ FetchTurns(ctx context.Context, p ahptypes.FetchTurnsParams) error
105
+ }
106
+
107
+ // ChannelHydrator materializes a channel that exists durably but is not in memory.
108
+ type ChannelHydrator interface {
109
+ Hydrate(ctx context.Context, channel string) (bool, error)
110
+ }
111
+
112
+ // ClientActionListener is a post-commit hook for actions a client dispatched.
113
+ type ClientActionListener func(channel string, action ahptypes.StateAction)
114
+
115
+ // CommittedActionListener observes every accepted action after it was reduced and broadcast.
116
+ type CommittedActionListener func(channel string, action ahptypes.StateAction)
117
+
118
+ // ClientActionValidator returns a reason to refuse a client action, or "" to accept it.
119
+ type ClientActionValidator func(channel string, action ClientAction, clientID string) string
120
+
121
+ // SubscriberCountListener is told when the number of clients subscribed to a channel changes.
122
+ type SubscriberCountListener func(channel string, count int)
123
+
124
+ // defaultReplayBufferCapacity is the local memory bound for replay. A client whose gap predates
125
+ // the buffer gets fresh snapshots instead, preserving correctness at the cost of more data.
126
+ const defaultReplayBufferCapacity = 1000
127
+
128
+ // ProviderScheme is the provider id of the agent this host advertises; VS Code derives its
129
+ // session URIs from it.
130
+ const ProviderScheme = "pi"
131
+
132
+ // Host is the AHP host.
133
+ type Host struct {
134
+ opts Options
135
+ store *Store
136
+
137
+ mu sync.Mutex // guards everything below except the event queue
138
+ seq int64
139
+ buf []ahptypes.ActionEnvelope
140
+ cap int
141
+ conns []*Conn // insertion order
142
+ // clientInfoByID: reconnect omits clientInfo; a present key with "" still records an id seen by this host process.
143
+ clientInfoByID map[string]string
144
+ caps Capabilities
145
+
146
+ listenMu sync.Mutex
147
+ actionListeners []*listener[ClientActionListener]
148
+ commitListeners []*listener[CommittedActionListener]
149
+ validators []*listener[ClientActionValidator]
150
+ countListeners []*listener[SubscriberCountListener]
151
+
152
+ evMu sync.Mutex
153
+ events []func()
154
+ draining bool
155
+ }
156
+
157
+ type listener[T any] struct{ fn T }
158
+
159
+ // New returns a host with no channels; install the root channel before clients connect.
160
+ func New(opts Options) *Host {
161
+ c := opts.ReplayBufferCapacity
162
+ if c <= 0 {
163
+ c = defaultReplayBufferCapacity
164
+ }
165
+ return &Host{opts: opts, store: newStore(), cap: c, clientInfoByID: map[string]string{}}
166
+ }
167
+
168
+ // Store returns the authoritative channel state.
169
+ func (h *Host) Store() *Store { return h.store }
170
+
171
+ // ServerSeq is the seq of the most recently emitted action envelope (0 before anything is emitted).
172
+ func (h *Host) ServerSeq() int64 {
173
+ h.mu.Lock()
174
+ defer h.mu.Unlock()
175
+ return h.seq
176
+ }
177
+
178
+ // Serve declares what this host serves; it merges into earlier declarations.
179
+ func (h *Host) Serve(c Capabilities) {
180
+ h.mu.Lock()
181
+ defer h.mu.Unlock()
182
+ if c.Catalogue != nil {
183
+ h.caps.Catalogue = c.Catalogue
184
+ }
185
+ if c.Sessions != nil {
186
+ h.caps.Sessions = c.Sessions
187
+ }
188
+ if c.Terminals != nil {
189
+ h.caps.Terminals = c.Terminals
190
+ }
191
+ if c.Resources != nil {
192
+ h.caps.Resources = c.Resources
193
+ }
194
+ if c.ResourceWatches != nil {
195
+ h.caps.ResourceWatches = c.ResourceWatches
196
+ }
197
+ if c.Completions != nil {
198
+ h.caps.Completions = c.Completions
199
+ }
200
+ if c.SessionConfig != nil {
201
+ h.caps.SessionConfig = c.SessionConfig
202
+ }
203
+ if c.TurnPaging != nil {
204
+ h.caps.TurnPaging = c.TurnPaging
205
+ }
206
+ if c.Hydrator != nil {
207
+ h.caps.Hydrator = c.Hydrator
208
+ }
209
+ }
210
+
211
+ func (h *Host) capabilities() Capabilities {
212
+ h.mu.Lock()
213
+ defer h.mu.Unlock()
214
+ return h.caps
215
+ }
216
+
217
+ func (h *Host) log(format string, args ...any) {
218
+ if h.opts.Log != nil {
219
+ h.opts.Log("[ahp-host] " + fmt.Sprintf(format, args...))
220
+ }
221
+ }
222
+
223
+ // InstallRootChannel registers ahp-root:// with the given agents.
224
+ func (h *Host) InstallRootChannel(agents []ahptypes.AgentInfo) {
225
+ if agents == nil {
226
+ agents = []ahptypes.AgentInfo{}
227
+ }
228
+ zero := int64(0)
229
+ _ = h.store.Create(wire.RootChannel, &ahptypes.RootState{Agents: agents, ActiveSessions: &zero})
230
+ }
231
+
232
+ // ── events (listeners run outside every lock, in commit order) ──────────
233
+
234
+ func (h *Host) emit(fn func()) {
235
+ h.evMu.Lock()
236
+ h.events = append(h.events, fn)
237
+ h.evMu.Unlock()
238
+ }
239
+
240
+ // drain runs queued listener events. A re-entrant call (a listener dispatching another action)
241
+ // only enqueues; the outer drain runs the derived event after the current one returns, so a
242
+ // derived action is always sequenced and observed after the action that caused it.
243
+ func (h *Host) drain() {
244
+ h.evMu.Lock()
245
+ if h.draining {
246
+ h.evMu.Unlock()
247
+ return
248
+ }
249
+ h.draining = true
250
+ for len(h.events) > 0 {
251
+ fn := h.events[0]
252
+ h.events[0] = nil
253
+ h.events = h.events[1:]
254
+ h.evMu.Unlock()
255
+ func() {
256
+ defer func() {
257
+ if r := recover(); r != nil {
258
+ h.log("listener panicked: %v", r)
259
+ }
260
+ }()
261
+ fn()
262
+ }()
263
+ h.evMu.Lock()
264
+ }
265
+ h.draining = false
266
+ h.evMu.Unlock()
267
+ }
268
+
269
+ func add[T any](h *Host, list *[]*listener[T], fn T) func() {
270
+ l := &listener[T]{fn: fn}
271
+ h.listenMu.Lock()
272
+ *list = append(*list, l)
273
+ h.listenMu.Unlock()
274
+ return func() {
275
+ h.listenMu.Lock()
276
+ defer h.listenMu.Unlock()
277
+ for i, x := range *list {
278
+ if x == l {
279
+ *list = append((*list)[:i:i], (*list)[i+1:]...)
280
+ return
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ func snapshotListeners[T any](h *Host, list []*listener[T]) []T {
287
+ h.listenMu.Lock()
288
+ defer h.listenMu.Unlock()
289
+ out := make([]T, len(list))
290
+ for i, l := range list {
291
+ out[i] = l.fn
292
+ }
293
+ return out
294
+ }
295
+
296
+ // OnClientAction registers a post-commit hook for client-dispatched actions. Actions are applied and
297
+ // broadcast before the hook runs. Rejected actions never reach it.
298
+ func (h *Host) OnClientAction(l ClientActionListener) func() { return add(h, &h.actionListeners, l) }
299
+
300
+ // OnActionCommitted observes every accepted action after its envelope has been broadcast.
301
+ func (h *Host) OnActionCommitted(l CommittedActionListener) func() {
302
+ return add(h, &h.commitListeners, l)
303
+ }
304
+
305
+ // AddClientActionValidator registers a pre-commit check, needed whenever accepting an action would
306
+ // leave the host unable to carry it out.
307
+ func (h *Host) AddClientActionValidator(v ClientActionValidator) func() {
308
+ return add(h, &h.validators, v)
309
+ }
310
+
311
+ // OnSubscriberCountChanged observes changes to a channel's subscriber count.
312
+ func (h *Host) OnSubscriberCountChanged(l SubscriberCountListener) func() {
313
+ return add(h, &h.countListeners, l)
314
+ }
315
+
316
+ // SubscriberCount is how many connected clients are subscribed to a channel.
317
+ func (h *Host) SubscriberCount(channel string) int {
318
+ h.mu.Lock()
319
+ defer h.mu.Unlock()
320
+ return h.subscriberCountLocked(channel)
321
+ }
322
+
323
+ func (h *Host) subscriberCountLocked(channel string) int {
324
+ n := 0
325
+ for _, c := range h.conns {
326
+ if c.subscribed(channel) {
327
+ n++
328
+ }
329
+ }
330
+ return n
331
+ }
332
+
333
+ // notifyCountLocked queues a subscriber-count notification (run by drain, outside the lock).
334
+ func (h *Host) notifyCountLocked(channel string) {
335
+ count := h.subscriberCountLocked(channel)
336
+ for _, fn := range snapshotListeners(h, h.countListeners) {
337
+ fn := fn
338
+ h.emit(func() { fn(channel, count) })
339
+ }
340
+ }
341
+
342
+ // DeleteChannel removes a channel and releases every connection subscribed to that identity.
343
+ func (h *Host) DeleteChannel(channel string) bool {
344
+ h.mu.Lock()
345
+ deleted := h.store.Delete(channel)
346
+ released := false
347
+ for _, c := range h.conns {
348
+ if c.unsubscribeLocked(channel) {
349
+ released = true
350
+ }
351
+ }
352
+ if released {
353
+ h.notifyCountLocked(channel)
354
+ }
355
+ h.mu.Unlock()
356
+ h.drain()
357
+ return deleted
358
+ }
359
+
360
+ // ── connections ─────────────────────────────────────────────────────────
361
+
362
+ // Conn is one accepted transport connection and its per-socket subscriptions.
363
+ type Conn struct {
364
+ h *Host
365
+ transport Transport
366
+ ctx context.Context
367
+ cancel context.CancelFunc
368
+
369
+ // guarded by h.mu
370
+ clientID string
371
+ subs []string
372
+ closed bool
373
+
374
+ workMu sync.Mutex // guards workarounds (used outside h.mu for incoming rewrites)
375
+ work *ClientWorkarounds
376
+ }
377
+
378
+ // Accept attaches a transport. The clientId is not known until initialize.
379
+ func (h *Host) Accept(t Transport) *Conn {
380
+ ctx, cancel := context.WithCancel(context.Background())
381
+ c := &Conn{h: h, transport: t, ctx: ctx, cancel: cancel, work: NewClientWorkarounds(ProviderScheme)}
382
+ h.mu.Lock()
383
+ h.conns = append(h.conns, c)
384
+ h.mu.Unlock()
385
+ return c
386
+ }
387
+
388
+ func (c *Conn) subscribed(channel string) bool {
389
+ for _, s := range c.subs {
390
+ if s == channel {
391
+ return true
392
+ }
393
+ }
394
+ return false
395
+ }
396
+
397
+ func (c *Conn) subscribeLocked(channel string) bool {
398
+ if c.subscribed(channel) {
399
+ return false
400
+ }
401
+ c.subs = append(c.subs, channel)
402
+ return true
403
+ }
404
+
405
+ func (c *Conn) unsubscribeLocked(channel string) bool {
406
+ for i, s := range c.subs {
407
+ if s == channel {
408
+ c.subs = append(c.subs[:i:i], c.subs[i+1:]...)
409
+ return true
410
+ }
411
+ }
412
+ return false
413
+ }
414
+
415
+ // Closed tells the host the transport went away; the connection's subscriptions are released just
416
+ // like explicit unsubscribes, since resources tied to them must not outlive the client.
417
+ func (c *Conn) Closed() {
418
+ h := c.h
419
+ h.mu.Lock()
420
+ if c.closed {
421
+ h.mu.Unlock()
422
+ return
423
+ }
424
+ c.closed = true
425
+ c.cancel()
426
+ h.removeConnLocked(c)
427
+ for _, channel := range c.subs {
428
+ h.notifyCountLocked(channel)
429
+ }
430
+ h.mu.Unlock()
431
+ h.drain()
432
+ }
433
+
434
+ func (h *Host) removeConnLocked(c *Conn) {
435
+ for i, x := range h.conns {
436
+ if x == c {
437
+ h.conns = append(h.conns[:i:i], h.conns[i+1:]...)
438
+ return
439
+ }
440
+ }
441
+ }
442
+
443
+ // sendLocked serializes and sends one message to this connection, applying its client workarounds.
444
+ func (c *Conn) sendLocked(msg any) {
445
+ if c.closed {
446
+ return
447
+ }
448
+ frame, err := json.Marshal(msg)
449
+ if err != nil {
450
+ c.h.log("cannot marshal outgoing message: %v", err)
451
+ return
452
+ }
453
+ c.workMu.Lock()
454
+ active := c.work.Active()
455
+ c.workMu.Unlock()
456
+ if active {
457
+ var generic map[string]any
458
+ dec := json.NewDecoder(bytes.NewReader(frame))
459
+ dec.UseNumber()
460
+ if err := dec.Decode(&generic); err == nil {
461
+ c.workMu.Lock()
462
+ generic = c.work.ApplyToOutgoing(generic)
463
+ c.workMu.Unlock()
464
+ if rewritten, err := json.Marshal(generic); err == nil {
465
+ frame = rewritten
466
+ }
467
+ }
468
+ }
469
+ if err := c.transport.Send(frame); err != nil {
470
+ c.h.log("send failed: %v", err)
471
+ }
472
+ }
473
+
474
+ // ── message routing ─────────────────────────────────────────────────────
475
+
476
+ type request struct {
477
+ id json.RawMessage
478
+ method string
479
+ params map[string]any
480
+ }
481
+
482
+ // Receive handles one inbound text frame. Requests that may block run on their own goroutine so the
483
+ // connection's read loop stays live (a ping must always be answered); everything else runs inline,
484
+ // which keeps the order of a client's messages.
485
+ func (c *Conn) Receive(frame []byte) {
486
+ var msg map[string]any
487
+ dec := json.NewDecoder(bytes.NewReader(frame))
488
+ dec.UseNumber()
489
+ if err := dec.Decode(&msg); err != nil {
490
+ // A frame we cannot parse has no id, so there is nobody to answer.
491
+ c.h.log("dropping unparseable frame: %v", err)
492
+ return
493
+ }
494
+ method, hasMethod := msg["method"].(string)
495
+ _, hasID := msg["id"]
496
+ idNumber, idIsNumber := msg["id"].(json.Number)
497
+ switch {
498
+ case hasMethod && hasID && idIsNumber:
499
+ id := json.RawMessage(idNumber.String())
500
+ c.handleRequest(id, method, msg)
501
+ case hasMethod && !hasID:
502
+ c.handleNotification(method, msg)
503
+ default:
504
+ text := string(frame)
505
+ if len(text) > 200 {
506
+ text = text[:200]
507
+ }
508
+ c.h.log("ignoring unroutable message: %s", text)
509
+ }
510
+ }
511
+
512
+ func (c *Conn) respond(id json.RawMessage, result any, err error) {
513
+ h := c.h
514
+ h.mu.Lock()
515
+ c.respondLocked(id, result, err)
516
+ h.mu.Unlock()
517
+ // Listener events queued while handling the request (subscriber counts) run once the response is sent.
518
+ h.drain()
519
+ }
520
+
521
+ func (c *Conn) respondLocked(id json.RawMessage, result any, err error) {
522
+ if err != nil {
523
+ var pe *wire.Error
524
+ if !errors.As(err, &pe) {
525
+ pe = &wire.Error{Code: wire.CodeInternalError, Message: err.Error()}
526
+ }
527
+ c.h.log("request failed: %s", pe.Message)
528
+ e := map[string]any{"code": pe.Code, "message": pe.Message}
529
+ if pe.Data != nil {
530
+ e["data"] = pe.Data
531
+ }
532
+ c.sendLocked(map[string]any{"jsonrpc": "2.0", "id": id, "error": e})
533
+ return
534
+ }
535
+ if result == nil {
536
+ result = json.RawMessage("null")
537
+ }
538
+ c.sendLocked(map[string]any{"jsonrpc": "2.0", "id": id, "result": result})
539
+ }
540
+
541
+ func isRootCommand(method string) bool {
542
+ switch method {
543
+ case "initialize", "ping", "reconnect", "listSessions", "resourceRead", "resourceWrite", "resourceList",
544
+ "resourceCopy", "resourceDelete", "resourceMove", "resourceResolve", "resourceMkdir", "resourceRequest",
545
+ "createResourceWatch", "authenticate", "resolveSessionConfig", "sessionConfigCompletions", "listAutomationTriggerDefinitions":
546
+ return true
547
+ }
548
+ return false
549
+ }
550
+
551
+ func (c *Conn) handleRequest(id json.RawMessage, method string, msg map[string]any) {
552
+ h := c.h
553
+ p, _ := msg["params"].(map[string]any)
554
+
555
+ // Handshake and pre-handshake policy.
556
+ h.mu.Lock()
557
+ initialized := c.clientID != ""
558
+ h.mu.Unlock()
559
+ handshake := method == "initialize" || method == "reconnect"
560
+ if handshake && initialized {
561
+ c.respond(id, nil, &wire.Error{Code: wire.CodeInvalidRequest, Message: "Connection is already initialized"})
562
+ return
563
+ }
564
+ if !handshake && method != "ping" && !initialized {
565
+ c.respond(id, nil, &wire.Error{Code: wire.CodeInvalidRequest, Message: "initialize or reconnect must be the first request"})
566
+ return
567
+ }
568
+ // Validate handshake shapes before any workaround observes them, so an invalid request cannot
569
+ // establish a URI dialect.
570
+ var initParams ahptypes.InitializeParams
571
+ var reconnectParams ahptypes.ReconnectParams
572
+ switch method {
573
+ case "initialize":
574
+ if err := decodeParams(p, &initParams); err != nil || initParams.ClientId == "" {
575
+ c.respond(id, nil, wire.InvalidParams(invalidMessage("initialize requires a clientId", err)))
576
+ return
577
+ }
578
+ case "reconnect":
579
+ // VS Code omits channel in reconnect (AHP 0.9 discriminant): repaired by the workaround
580
+ // below once VS Code is identified, so decode leniently first and validate after.
581
+ }
582
+ if err := c.applyWorkarounds(method, msg, p, initParams); err != nil {
583
+ c.respond(id, nil, err)
584
+ return
585
+ }
586
+ p, _ = msg["params"].(map[string]any)
587
+ if isRootCommand(method) {
588
+ if channel, _ := p["channel"].(string); channel != wire.RootChannel {
589
+ c.respond(id, nil, wire.InvalidParams(method+" requires channel "+wire.RootChannel))
590
+ return
591
+ }
592
+ }
593
+
594
+ switch method {
595
+ case "ping":
596
+ // Answered whether or not the client has completed initialize or holds any subscription,
597
+ // but like every connection-level command it must be routed on the root channel.
598
+ c.respond(id, nil, nil)
599
+ case "initialize":
600
+ // Re-decode: the workarounds may have rewritten initialSubscriptions.
601
+ initParams = ahptypes.InitializeParams{}
602
+ if err := decodeParams(p, &initParams); err != nil {
603
+ c.respond(id, nil, wire.InvalidParams(invalidMessage("invalid initialize params", err)))
604
+ return
605
+ }
606
+ if _, err := wire.NegotiateProtocolVersion(initParams.ProtocolVersions); err != nil {
607
+ c.respond(id, nil, err)
608
+ return
609
+ }
610
+ c.runMaybeBlocking(id, c.needsHydration(initParams.InitialSubscriptions), func() (any, error) { return c.initialize(initParams) })
611
+ case "reconnect":
612
+ if err := decodeParams(p, &reconnectParams); err != nil || reconnectParams.ClientId == "" || reconnectParams.LastSeenServerSeq < 0 {
613
+ c.respond(id, nil, wire.InvalidParams(invalidMessage("reconnect requires a clientId, a non-negative integer lastSeenServerSeq and a subscriptions array of URIs", err)))
614
+ return
615
+ }
616
+ c.runMaybeBlocking(id, c.needsHydration(reconnectParams.Subscriptions), func() (any, error) { return c.reconnect(reconnectParams) })
617
+ case "subscribe":
618
+ var params ahptypes.SubscribeParams
619
+ if err := decodeParams(p, &params); err != nil || params.Channel == "" {
620
+ c.respond(id, nil, wire.InvalidParams("subscribe requires a channel"))
621
+ return
622
+ }
623
+ c.runMaybeBlocking(id, c.needsHydration([]string{params.Channel}), func() (any, error) { return c.subscribe(params) })
624
+ default:
625
+ c.runBlocking(id, func() (any, error) { return c.dispatchOther(method, p) })
626
+ }
627
+ }
628
+
629
+ func invalidMessage(base string, err error) string {
630
+ if err == nil {
631
+ return base
632
+ }
633
+ return base + " (" + err.Error() + ")"
634
+ }
635
+
636
+ func decodeParams(p map[string]any, into any) error {
637
+ if p == nil {
638
+ return errors.New("params are required")
639
+ }
640
+ b, err := json.Marshal(p)
641
+ if err != nil {
642
+ return err
643
+ }
644
+ return json.Unmarshal(b, into)
645
+ }
646
+
647
+ // applyWorkarounds identifies the client and rewrites the request in place.
648
+ func (c *Conn) applyWorkarounds(method string, msg, p map[string]any, init ahptypes.InitializeParams) error {
649
+ h := c.h
650
+ c.workMu.Lock()
651
+ defer c.workMu.Unlock()
652
+ switch method {
653
+ case "initialize":
654
+ name := ""
655
+ if init.ClientInfo != nil {
656
+ name = init.ClientInfo.Name
657
+ }
658
+ if name == "" {
659
+ h.mu.Lock()
660
+ name = h.clientInfoByID[init.ClientId]
661
+ h.mu.Unlock()
662
+ }
663
+ c.work.Identify(name)
664
+ case "reconnect":
665
+ clientID, _ := p["clientId"].(string)
666
+ h.mu.Lock()
667
+ name := h.clientInfoByID[clientID]
668
+ h.mu.Unlock()
669
+ c.work.Identify(name)
670
+ }
671
+ if err := c.work.ApplyToIncoming(msg); err != nil {
672
+ return err
673
+ }
674
+ return nil
675
+ }
676
+
677
+ // needsHydration reports whether a handshake or subscribe names a channel only the hydrator can supply.
678
+ func (c *Conn) needsHydration(channels []string) bool {
679
+ caps := c.h.capabilities()
680
+ if caps.Hydrator == nil {
681
+ return false
682
+ }
683
+ for _, ch := range channels {
684
+ if ch != wire.RootChannel && !c.h.store.Has(ch) {
685
+ return true
686
+ }
687
+ }
688
+ return false
689
+ }
690
+
691
+ func (c *Conn) runMaybeBlocking(id json.RawMessage, blocking bool, fn func() (any, error)) {
692
+ if blocking {
693
+ c.runBlocking(id, fn)
694
+ return
695
+ }
696
+ result, err := fn()
697
+ c.respond(id, result, err)
698
+ }
699
+
700
+ func (c *Conn) runBlocking(id json.RawMessage, fn func() (any, error)) {
701
+ go func() {
702
+ result, err := func() (result any, err error) {
703
+ defer func() {
704
+ if r := recover(); r != nil {
705
+ err = fmt.Errorf("internal error: %v", r)
706
+ }
707
+ }()
708
+ return fn()
709
+ }()
710
+ c.respond(id, result, err)
711
+ }()
712
+ }
713
+
714
+ func (c *Conn) handleNotification(method string, msg map[string]any) {
715
+ h := c.h
716
+ h.mu.Lock()
717
+ initialized := c.clientID != ""
718
+ h.mu.Unlock()
719
+ if !initialized {
720
+ h.log("ignoring %s before initialize or reconnect", method)
721
+ return
722
+ }
723
+ c.workMu.Lock()
724
+ _ = c.work.ApplyToIncoming(msg)
725
+ c.workMu.Unlock()
726
+ p, _ := msg["params"].(map[string]any)
727
+ switch method {
728
+ case "unsubscribe":
729
+ channel, ok := p["channel"].(string)
730
+ if !ok {
731
+ return
732
+ }
733
+ h.mu.Lock()
734
+ if c.unsubscribeLocked(channel) {
735
+ h.notifyCountLocked(channel)
736
+ }
737
+ h.mu.Unlock()
738
+ h.drain()
739
+ case "dispatchAction":
740
+ c.dispatchClientAction(p)
741
+ default:
742
+ h.log("ignoring unknown notification: %s", method)
743
+ }
744
+ }
745
+
746
+ // ── handshake ───────────────────────────────────────────────────────────
747
+
748
+ func (c *Conn) hydrate(channels []string) {
749
+ caps := c.h.capabilities()
750
+ if caps.Hydrator == nil {
751
+ return
752
+ }
753
+ for _, ch := range channels {
754
+ if ch != wire.RootChannel && !c.h.store.Has(ch) {
755
+ if _, err := caps.Hydrator.Hydrate(c.ctx, ch); err != nil {
756
+ c.h.log("hydrate %s failed: %v", ch, err)
757
+ }
758
+ }
759
+ }
760
+ }
761
+
762
+ func (c *Conn) initialize(p ahptypes.InitializeParams) (any, error) {
763
+ version, err := wire.NegotiateProtocolVersion(p.ProtocolVersions)
764
+ if err != nil {
765
+ return nil, err
766
+ }
767
+ h := c.h
768
+ // Same lazy load as subscribe: a client reconnecting with its previously-open sessions must get them back.
769
+ c.hydrate(p.InitialSubscriptions)
770
+
771
+ h.mu.Lock()
772
+ defer h.mu.Unlock()
773
+ if c.clientID != "" {
774
+ return nil, &wire.Error{Code: wire.CodeInvalidRequest, Message: "Connection is already initialized"}
775
+ }
776
+ h.bindClientLocked(c, p.ClientId)
777
+ name := ""
778
+ if p.ClientInfo != nil {
779
+ name = p.ClientInfo.Name
780
+ }
781
+ if name != "" || !h.knownClientLocked(p.ClientId) {
782
+ h.clientInfoByID[p.ClientId] = name
783
+ }
784
+
785
+ snapshots := []ahptypes.Snapshot{}
786
+ for _, uri := range p.InitialSubscriptions {
787
+ if snap, ok := h.trySubscribeLocked(c, uri); ok {
788
+ snapshots = append(snapshots, snap)
789
+ }
790
+ }
791
+ result := ahptypes.InitializeResult{
792
+ ProtocolVersion: version,
793
+ ServerSeq: h.seq,
794
+ ServerInfo: h.opts.ServerInfo,
795
+ Snapshots: snapshots,
796
+ }
797
+ if h.opts.DefaultDirectory != "" {
798
+ d := h.opts.DefaultDirectory
799
+ result.DefaultDirectory = &d
800
+ }
801
+ if len(h.opts.CompletionTriggerCharacters) > 0 {
802
+ result.CompletionTriggerCharacters = append([]string(nil), h.opts.CompletionTriggerCharacters...)
803
+ }
804
+ return result, nil
805
+ }
806
+
807
+ func (h *Host) knownClientLocked(clientID string) bool {
808
+ _, ok := h.clientInfoByID[clientID]
809
+ return ok
810
+ }
811
+
812
+ // bindClientLocked replaces a half-open socket when the same clientId reconnects.
813
+ func (h *Host) bindClientLocked(c *Conn, clientID string) {
814
+ for _, previous := range append([]*Conn(nil), h.conns...) {
815
+ if previous != c && previous.clientID == clientID {
816
+ h.removeConnLocked(previous)
817
+ for _, channel := range previous.subs {
818
+ h.notifyCountLocked(channel)
819
+ }
820
+ previous.subs = nil
821
+ previous.closed = true
822
+ previous.cancel()
823
+ go previous.transport.Close()
824
+ }
825
+ }
826
+ c.clientID = clientID
827
+ }
828
+
829
+ func (c *Conn) reconnect(p ahptypes.ReconnectParams) (any, error) {
830
+ h := c.h
831
+ c.hydrate(p.Subscriptions)
832
+
833
+ h.mu.Lock()
834
+ defer h.mu.Unlock()
835
+ if c.clientID != "" {
836
+ return nil, &wire.Error{Code: wire.CodeInvalidRequest, Message: "Connection is already initialized"}
837
+ }
838
+ known := h.knownClientLocked(p.ClientId)
839
+ if !known {
840
+ h.clientInfoByID[p.ClientId] = ""
841
+ }
842
+ h.bindClientLocked(c, p.ClientId)
843
+
844
+ missing := []string{}
845
+ previous := c.subs
846
+ c.subs = nil
847
+ for _, uri := range p.Subscriptions {
848
+ if uri == wire.RootChannel || h.store.Has(uri) {
849
+ c.subscribeLocked(uri)
850
+ } else {
851
+ missing = append(missing, uri)
852
+ }
853
+ }
854
+ for _, channel := range previous {
855
+ if !c.subscribed(channel) {
856
+ h.notifyCountLocked(channel)
857
+ }
858
+ }
859
+ for _, channel := range c.subs {
860
+ if !contains(previous, channel) {
861
+ h.notifyCountLocked(channel)
862
+ }
863
+ }
864
+
865
+ if known && h.canReplayFromLocked(p.LastSeenServerSeq) {
866
+ return reconnectReplay{Type: "replay", Actions: h.replayFromLocked(p.LastSeenServerSeq, c.subs), Missing: missing}, nil
867
+ }
868
+ // Replay is unavailable after buffer eviction or in a fresh host process. Durable subscriptions
869
+ // were hydrated above; return current snapshots.
870
+ snapshots := []ahptypes.Snapshot{}
871
+ for _, uri := range c.subs {
872
+ if snap, ok := h.store.Snapshot(uri, h.seq); ok {
873
+ snapshots = append(snapshots, snap)
874
+ }
875
+ }
876
+ return reconnectSnapshot{Type: "snapshot", Snapshots: snapshots}, nil
877
+ }
878
+
879
+ // The vendored client's ReconnectReplayResult / ReconnectSnapshotResult carry no "type" discriminator
880
+ // on marshal (the union wrapper only marshals its value), which the wire format requires.
881
+ type reconnectReplay struct {
882
+ Type string `json:"type"`
883
+ Actions []ahptypes.ActionEnvelope `json:"actions"`
884
+ Missing []string `json:"missing"`
885
+ }
886
+
887
+ type reconnectSnapshot struct {
888
+ Type string `json:"type"`
889
+ Snapshots []ahptypes.Snapshot `json:"snapshots"`
890
+ }
891
+
892
+ func contains(list []string, s string) bool {
893
+ for _, x := range list {
894
+ if x == s {
895
+ return true
896
+ }
897
+ }
898
+ return false
899
+ }
900
+
901
+ // canReplayFromLocked reports whether the gap after lastSeen is fully covered by the buffer. A client
902
+ // that is already current trivially qualifies; otherwise the buffer must still hold lastSeen+1.
903
+ func (h *Host) canReplayFromLocked(lastSeen int64) bool {
904
+ if lastSeen >= h.seq {
905
+ return true
906
+ }
907
+ return len(h.buf) > 0 && h.buf[0].ServerSeq <= lastSeen+1
908
+ }
909
+
910
+ func (h *Host) replayFromLocked(lastSeen int64, channels []string) []ahptypes.ActionEnvelope {
911
+ out := []ahptypes.ActionEnvelope{}
912
+ for _, env := range h.buf {
913
+ if env.ServerSeq > lastSeen && contains(channels, env.Channel) {
914
+ out = append(out, env)
915
+ }
916
+ }
917
+ return out
918
+ }
919
+
920
+ // ── subscriptions ───────────────────────────────────────────────────────
921
+
922
+ func (c *Conn) subscribe(p ahptypes.SubscribeParams) (any, error) {
923
+ h := c.h
924
+ channel := p.Channel
925
+ if !h.store.Has(channel) {
926
+ // Not in memory does not mean it does not exist: a session from the catalogue lives on disk
927
+ // until someone opens it. The hydrator gets the first chance to resolve known aliases before
928
+ // the strict scheme check.
929
+ hydrated := false
930
+ if caps := h.capabilities(); caps.Hydrator != nil {
931
+ ok, err := caps.Hydrator.Hydrate(c.ctx, channel)
932
+ hydrated = err == nil && ok
933
+ }
934
+ if !hydrated && !h.store.Has(channel) {
935
+ if _, known := wire.KindOf(channel); !known {
936
+ return nil, wire.InvalidParams("Unsupported channel scheme: " + channel)
937
+ }
938
+ return nil, wire.NotFound(channel)
939
+ }
940
+ }
941
+ h.mu.Lock()
942
+ defer h.mu.Unlock()
943
+ if snap, ok := h.trySubscribeLocked(c, channel); ok {
944
+ return ahptypes.SubscribeResult{Snapshot: &snap}, nil
945
+ }
946
+ return ahptypes.SubscribeResult{}, nil
947
+ }
948
+
949
+ func (h *Host) trySubscribeLocked(c *Conn, channel string) (ahptypes.Snapshot, bool) {
950
+ // Membership in the store is the real test: a session opened at a non-standard URI has no
951
+ // recognised scheme but is a perfectly valid channel.
952
+ snap, ok := h.store.Snapshot(channel, h.seq)
953
+ if !ok {
954
+ h.log("ignoring subscription to unknown channel: %s", channel)
955
+ return ahptypes.Snapshot{}, false
956
+ }
957
+ if c.subscribeLocked(channel) {
958
+ h.notifyCountLocked(channel)
959
+ }
960
+ return snap, true
961
+ }
962
+
963
+ // ── actions ─────────────────────────────────────────────────────────────
964
+
965
+ func (c *Conn) dispatchClientAction(p map[string]any) {
966
+ h := c.h
967
+ channel, okChannel := p["channel"].(string)
968
+ clientSeqNum, okSeq := p["clientSeq"].(json.Number)
969
+ rawAction, okAction := p["action"].(map[string]any)
970
+ actionType, okType := "", false
971
+ if okAction {
972
+ actionType, okType = rawAction["type"].(string)
973
+ }
974
+ if !okChannel || !okSeq || !okType {
975
+ h.log("ignoring malformed dispatchAction")
976
+ return
977
+ }
978
+ clientSeq, err := clientSeqNum.Int64()
979
+ if err != nil {
980
+ h.log("ignoring malformed dispatchAction")
981
+ return
982
+ }
983
+ // Spec: an action naming a channel that does not exist is silently ignored — no echo, no rejection.
984
+ if !h.store.Has(channel) {
985
+ h.log("ignoring action for unknown channel: %s", channel)
986
+ return
987
+ }
988
+ rawJSON, _ := json.Marshal(rawAction)
989
+ var action ahptypes.StateAction
990
+ decodeErr := json.Unmarshal(rawJSON, &action)
991
+ if action.Value == nil {
992
+ action = ahptypes.StateAction{Value: &ahptypes.StateActionUnknown{Raw: rawJSON}}
993
+ }
994
+
995
+ c.h.mu.Lock()
996
+ clientID := c.clientID
997
+ c.h.mu.Unlock()
998
+ origin := &ahptypes.ActionOrigin{ClientId: clientID, ClientSeq: clientSeq}
999
+
1000
+ if !wire.IsClientDispatchable(actionType) {
1001
+ h.reject(channel, action, origin, "Action is not client-dispatchable: "+actionType)
1002
+ return
1003
+ }
1004
+ if kind, ok := h.store.KindOf(channel); ok && !wire.ActionBelongsToChannel(actionType, kind) {
1005
+ h.reject(channel, action, origin, actionType+" does not belong on a "+string(kind)+" channel")
1006
+ return
1007
+ }
1008
+ for _, validate := range snapshotListeners(h, h.validators) {
1009
+ if reason := validate(channel, ClientAction{Type: actionType, Raw: rawJSON, Action: action, DecodeErr: decodeErr}, clientID); reason != "" {
1010
+ h.reject(channel, action, origin, reason)
1011
+ return
1012
+ }
1013
+ }
1014
+ if decodeErr != nil {
1015
+ h.reject(channel, action, origin, "Malformed action: "+decodeErr.Error())
1016
+ return
1017
+ }
1018
+ h.commit(channel, action, origin)
1019
+ for _, fn := range snapshotListeners(h, h.actionListeners) {
1020
+ fn := fn
1021
+ h.emit(func() { fn(channel, action) })
1022
+ }
1023
+ h.drain()
1024
+ }
1025
+
1026
+ // DispatchServerAction applies a host-originated action and broadcasts it: the single write path
1027
+ // for everything the agent backend produces.
1028
+ func (h *Host) DispatchServerAction(channel string, action ahptypes.StateAction) {
1029
+ if !h.store.Has(channel) {
1030
+ h.log("dropping server action for unknown channel: %s", channel)
1031
+ return
1032
+ }
1033
+ h.commit(channel, action, nil)
1034
+ h.drain()
1035
+ }
1036
+
1037
+ func (h *Host) commit(channel string, action ahptypes.StateAction, origin *ahptypes.ActionOrigin) {
1038
+ h.mu.Lock()
1039
+ h.store.Apply(channel, action)
1040
+ h.seq++
1041
+ env := ahptypes.ActionEnvelope{Channel: channel, Action: action, ServerSeq: h.seq, Origin: origin}
1042
+ h.retainLocked(env)
1043
+ h.broadcastLocked(channel, "action", env)
1044
+ for _, fn := range snapshotListeners(h, h.commitListeners) {
1045
+ fn := fn
1046
+ h.emit(func() { fn(channel, action) })
1047
+ }
1048
+ h.mu.Unlock()
1049
+ }
1050
+
1051
+ // reject echoes a rejected action so the write-ahead client can roll it back.
1052
+ func (h *Host) reject(channel string, action ahptypes.StateAction, origin *ahptypes.ActionOrigin, reason string) {
1053
+ h.mu.Lock()
1054
+ h.seq++
1055
+ env := ahptypes.ActionEnvelope{Channel: channel, Action: action, ServerSeq: h.seq, Origin: origin, RejectionReason: &reason}
1056
+ h.retainLocked(env)
1057
+ h.broadcastLocked(channel, "action", env)
1058
+ h.mu.Unlock()
1059
+ }
1060
+
1061
+ func (h *Host) retainLocked(env ahptypes.ActionEnvelope) {
1062
+ h.buf = append(h.buf, env)
1063
+ if len(h.buf) > h.cap {
1064
+ h.buf[0] = ahptypes.ActionEnvelope{}
1065
+ h.buf = h.buf[1:]
1066
+ }
1067
+ }
1068
+
1069
+ func (h *Host) broadcastLocked(channel, method string, params any) {
1070
+ for _, c := range append([]*Conn(nil), h.conns...) {
1071
+ if c.subscribed(channel) {
1072
+ c.sendLocked(map[string]any{"jsonrpc": "2.0", "method": method, "params": params})
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ // Notify emits a protocol notification (root/sessionAdded, auth/required, ...) to the channel's
1078
+ // subscribers. Notifications are ephemeral: not stored and not replayed on reconnect.
1079
+ func (h *Host) Notify(channel, method string, params map[string]any) {
1080
+ full := map[string]any{"channel": channel}
1081
+ for k, v := range params {
1082
+ full[k] = v
1083
+ }
1084
+ h.mu.Lock()
1085
+ h.broadcastLocked(channel, method, full)
1086
+ h.mu.Unlock()
1087
+ }
1088
+
1089
+ // ClientAction is a client-dispatched action as a validator sees it: the raw JSON (so a validator
1090
+ // can report the precise malformed field), the typed action when it decoded, and the decode error.
1091
+ type ClientAction struct {
1092
+ Type string
1093
+ Raw json.RawMessage
1094
+ Action ahptypes.StateAction
1095
+ DecodeErr error
1096
+ }
1097
+
1098
+ // ── other commands ──────────────────────────────────────────────────────
1099
+
1100
+ func (c *Conn) dispatchOther(method string, p map[string]any) (any, error) {
1101
+ h := c.h
1102
+ caps := h.capabilities()
1103
+ ctx := c.ctx
1104
+ switch method {
1105
+ case "listSessions":
1106
+ var params ahptypes.ListSessionsParams
1107
+ _ = decodeParams(p, &params)
1108
+ if caps.Catalogue == nil {
1109
+ return ahptypes.ListSessionsResult{Items: []ahptypes.SessionSummary{}}, nil
1110
+ }
1111
+ res, err := caps.Catalogue.List(ctx, params.Limit, params.Cursor)
1112
+ if res.Items == nil {
1113
+ res.Items = []ahptypes.SessionSummary{}
1114
+ }
1115
+ return res, err
1116
+ case "createSession":
1117
+ if caps.Sessions == nil {
1118
+ return nil, wire.MethodNotFound("createSession")
1119
+ }
1120
+ var params ahptypes.CreateSessionParams
1121
+ if err := decodeParams(p, &params); err != nil {
1122
+ return nil, wire.InvalidParams(invalidMessage("invalid createSession params", err))
1123
+ }
1124
+ return nil, caps.Sessions.Create(ctx, params)
1125
+ case "disposeSession":
1126
+ if caps.Sessions == nil {
1127
+ return nil, wire.MethodNotFound("disposeSession")
1128
+ }
1129
+ channel, ok := p["channel"].(string)
1130
+ if !ok || channel == "" {
1131
+ return nil, wire.InvalidParams("disposeSession requires a channel")
1132
+ }
1133
+ if err := h.assertCompatibleChannel(channel, wire.KindSession, "disposeSession"); err != nil {
1134
+ return nil, err
1135
+ }
1136
+ return nil, caps.Sessions.Dispose(ctx, channel)
1137
+ case "createTerminal":
1138
+ if caps.Terminals == nil {
1139
+ return nil, wire.MethodNotFound("createTerminal")
1140
+ }
1141
+ var params ahptypes.CreateTerminalParams
1142
+ if err := decodeParams(p, &params); err != nil {
1143
+ return nil, wire.InvalidParams(invalidMessage("invalid createTerminal params", err))
1144
+ }
1145
+ h.mu.Lock()
1146
+ clientID := c.clientID
1147
+ h.mu.Unlock()
1148
+ return nil, caps.Terminals.Create(ctx, params, clientID)
1149
+ case "disposeTerminal":
1150
+ if caps.Terminals == nil {
1151
+ return nil, wire.MethodNotFound("disposeTerminal")
1152
+ }
1153
+ channel, ok := p["channel"].(string)
1154
+ if !ok || channel == "" {
1155
+ return nil, wire.InvalidParams("disposeTerminal requires a channel")
1156
+ }
1157
+ if err := h.assertCompatibleChannel(channel, wire.KindTerminal, "disposeTerminal"); err != nil {
1158
+ return nil, err
1159
+ }
1160
+ return nil, caps.Terminals.Dispose(ctx, channel)
1161
+ case "resourceRead", "resourceWrite", "resourceList", "resourceResolve", "resourceMkdir", "resourceDelete", "resourceMove", "resourceCopy":
1162
+ return c.resource(ctx, caps, method, p)
1163
+ case "resolveSessionConfig":
1164
+ // A client calls this before createSession; answering MethodNotFound stops it from getting as far as creating one.
1165
+ if caps.SessionConfig == nil {
1166
+ return ahptypes.ResolveSessionConfigResult{Schema: ahptypes.SessionConfigSchema{Type: "object", Properties: map[string]ahptypes.SessionConfigPropertySchema{}}, Values: map[string]json.RawMessage{}}, nil
1167
+ }
1168
+ var params ahptypes.ResolveSessionConfigParams
1169
+ if err := decodeParams(p, &params); err != nil {
1170
+ return nil, wire.InvalidParams(invalidMessage("invalid resolveSessionConfig params", err))
1171
+ }
1172
+ return caps.SessionConfig.Resolve(ctx, params)
1173
+ case "sessionConfigCompletions":
1174
+ if caps.SessionConfig == nil {
1175
+ return ahptypes.SessionConfigCompletionsResult{Items: []ahptypes.SessionConfigValueItem{}}, nil
1176
+ }
1177
+ var params ahptypes.SessionConfigCompletionsParams
1178
+ if err := decodeParams(p, &params); err != nil {
1179
+ return nil, wire.InvalidParams(invalidMessage("invalid sessionConfigCompletions params", err))
1180
+ }
1181
+ return caps.SessionConfig.Completions(ctx, params)
1182
+ case "fetchTurns":
1183
+ // The result carries no turns: the host must dispatch chat/turnsLoaded before responding, so the
1184
+ // client's state already holds the page by the time this returns.
1185
+ if caps.TurnPaging == nil {
1186
+ return map[string]any{}, nil
1187
+ }
1188
+ if err := h.assertChatChannel(p, "fetchTurns"); err != nil {
1189
+ return nil, err
1190
+ }
1191
+ var params ahptypes.FetchTurnsParams
1192
+ if err := decodeParams(p, &params); err != nil {
1193
+ return nil, wire.InvalidParams(invalidMessage("invalid fetchTurns params", err))
1194
+ }
1195
+ if err := caps.TurnPaging.FetchTurns(ctx, params); err != nil {
1196
+ return nil, err
1197
+ }
1198
+ return map[string]any{}, nil
1199
+ case "completions":
1200
+ // Best-effort by contract: a client debounces keystrokes into this, so an unconfigured host answers with nothing rather than an error.
1201
+ if caps.Completions == nil {
1202
+ return ahptypes.CompletionsResult{Items: []ahptypes.CompletionItem{}}, nil
1203
+ }
1204
+ if err := h.assertChatChannel(p, "completions"); err != nil {
1205
+ return nil, err
1206
+ }
1207
+ var params ahptypes.CompletionsParams
1208
+ if err := decodeParams(p, &params); err != nil {
1209
+ return nil, wire.InvalidParams(invalidMessage("invalid completions params", err))
1210
+ }
1211
+ return caps.Completions.Complete(ctx, params)
1212
+ case "createResourceWatch":
1213
+ if caps.ResourceWatches == nil {
1214
+ return nil, wire.MethodNotFound("createResourceWatch")
1215
+ }
1216
+ var params ahptypes.CreateResourceWatchParams
1217
+ if err := decodeParams(p, &params); err != nil {
1218
+ return nil, wire.InvalidParams(invalidMessage("invalid createResourceWatch params", err))
1219
+ }
1220
+ return caps.ResourceWatches.Create(ctx, params)
1221
+ case "resourceRequest":
1222
+ // No per-resource grants are tracked: a client that reaches this endpoint already holds the
1223
+ // token and can start a session, so a grant ledger here would imply a boundary that does not
1224
+ // exist. The receiver may still refuse individual operations.
1225
+ return map[string]any{}, nil
1226
+ }
1227
+ return nil, wire.MethodNotFound(method)
1228
+ }
1229
+
1230
+ func (h *Host) assertCompatibleChannel(channel string, expected wire.ChannelKind, method string) error {
1231
+ if actual, ok := h.store.KindOf(channel); ok && actual != expected {
1232
+ return wire.InvalidParams(method + " cannot target a " + string(actual) + " channel")
1233
+ }
1234
+ return nil
1235
+ }
1236
+
1237
+ func (h *Host) assertChatChannel(p map[string]any, method string) error {
1238
+ channel, ok := p["channel"].(string)
1239
+ if !ok || channel == "" {
1240
+ return wire.InvalidParams(method + " requires a channel")
1241
+ }
1242
+ kind, known := h.store.KindOf(channel)
1243
+ if !known {
1244
+ kind, known = wire.KindOf(channel)
1245
+ }
1246
+ if !known || kind != wire.KindChat {
1247
+ return wire.InvalidParams(method + " requires a chat channel")
1248
+ }
1249
+ return nil
1250
+ }
1251
+
1252
+ func (c *Conn) resource(ctx context.Context, caps Capabilities, method string, p map[string]any) (any, error) {
1253
+ if caps.Resources == nil {
1254
+ return nil, wire.MethodNotFound("resource*")
1255
+ }
1256
+ r := caps.Resources
1257
+ switch method {
1258
+ case "resourceRead":
1259
+ var params ahptypes.ResourceReadParams
1260
+ if err := decodeParams(p, &params); err != nil {
1261
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceRead params", err))
1262
+ }
1263
+ return r.Read(ctx, params)
1264
+ case "resourceWrite":
1265
+ var params ahptypes.ResourceWriteParams
1266
+ if err := decodeParams(p, &params); err != nil {
1267
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceWrite params", err))
1268
+ }
1269
+ return map[string]any{}, r.Write(ctx, params)
1270
+ case "resourceList":
1271
+ uri, _ := p["uri"].(string)
1272
+ return r.List(ctx, uri)
1273
+ case "resourceResolve":
1274
+ var params ahptypes.ResourceResolveParams
1275
+ if err := decodeParams(p, &params); err != nil {
1276
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceResolve params", err))
1277
+ }
1278
+ return r.Resolve(ctx, params)
1279
+ case "resourceMkdir":
1280
+ var params ahptypes.ResourceMkdirParams
1281
+ if err := decodeParams(p, &params); err != nil {
1282
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceMkdir params", err))
1283
+ }
1284
+ return map[string]any{}, r.Mkdir(ctx, params)
1285
+ case "resourceDelete":
1286
+ var params ahptypes.ResourceDeleteParams
1287
+ if err := decodeParams(p, &params); err != nil {
1288
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceDelete params", err))
1289
+ }
1290
+ return map[string]any{}, r.Delete(ctx, params)
1291
+ case "resourceMove":
1292
+ var params ahptypes.ResourceMoveParams
1293
+ if err := decodeParams(p, &params); err != nil {
1294
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceMove params", err))
1295
+ }
1296
+ return map[string]any{}, r.Move(ctx, params)
1297
+ case "resourceCopy":
1298
+ var params ahptypes.ResourceCopyParams
1299
+ if err := decodeParams(p, &params); err != nil {
1300
+ return nil, wire.InvalidParams(invalidMessage("invalid resourceCopy params", err))
1301
+ }
1302
+ return map[string]any{}, r.Copy(ctx, params)
1303
+ }
1304
+ return nil, wire.MethodNotFound(method)
1305
+ }