@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,311 @@
1
+ package ahp
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "net"
8
+ "net/url"
9
+ "os"
10
+ "path/filepath"
11
+ "strconv"
12
+ "sync"
13
+
14
+ sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
15
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
16
+
17
+ "github.com/MichaelKinsy/pigpen/ahp/internal/compose"
18
+ "github.com/MichaelKinsy/pigpen/ahp/internal/live"
19
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
20
+ "github.com/MichaelKinsy/pigpen/ahp/internal/settings"
21
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
22
+ "github.com/MichaelKinsy/pigpen/ahp/internal/ws"
23
+ )
24
+
25
+ // Version is reported to clients as the server version.
26
+ const Version = "0.1.0"
27
+
28
+ // runtime owns the (optional) listener of one PiG process.
29
+ type runtime struct {
30
+ live *live.Session
31
+
32
+ mu sync.Mutex
33
+ server *ws.Server
34
+ built *compose.Built
35
+ addr string
36
+ ctx sdk.Context
37
+ haveCtx bool
38
+ }
39
+
40
+ func newRuntime() *runtime { return &runtime{live: live.New()} }
41
+
42
+ func (rt *runtime) status() string {
43
+ rt.mu.Lock()
44
+ defer rt.mu.Unlock()
45
+ if rt.server == nil {
46
+ return "AHP listener is off. /ahp start opens it (settings: " + settingsPath(nil) + ")"
47
+ }
48
+ return rt.addr
49
+ }
50
+
51
+ // listenNotice is the message printed when the listener is up. The first address is the form VS Code takes
52
+ // (ws://<host>:<port>?tkn=<token>) and is followed by a space: VS Code's SSH launcher (chat.sshRemoteAgentHostCommand) and
53
+ // "Agents: Add Remote Agent Host..." both read the first ws:// URL in the text, and the launcher's token runs to the next
54
+ // space or '&'. The generic form (/?token=) follows for other AHP clients; the listener takes either. hostport is the
55
+ // listener's address. A listener without a token has one address and no token form.
56
+ func listenNotice(hostport, token string) string {
57
+ if token == "" {
58
+ return "AHP listening on ws://" + hostport
59
+ }
60
+ escaped := url.QueryEscape(token)
61
+ return "AHP listening on ws://" + hostport + "?tkn=" + escaped +
62
+ " - for VS Code, paste it into \"Agents: Add Remote Agent Host...\"; other AHP clients can also use ws://" + hostport + "/?token=" + escaped
63
+ }
64
+
65
+ // settingsPath is the settings file: the --ahp-settings flag, else <pig home>/ahp/settings.json.
66
+ func settingsPath(ctx *sdk.Context) string {
67
+ if ctx != nil {
68
+ if v, err := ctx.GetFlag(flagSettings); err == nil {
69
+ if path, ok := v.(string); ok && path != "" {
70
+ return path
71
+ }
72
+ }
73
+ }
74
+ if home := os.Getenv("PIG_HOME"); home != "" {
75
+ return filepath.Join(home, "ahp", "settings.json")
76
+ }
77
+ if dir, err := os.UserHomeDir(); err == nil {
78
+ return filepath.Join(dir, ".pig", "ahp", "settings.json")
79
+ }
80
+ return filepath.Join(".pig", "ahp", "settings.json")
81
+ }
82
+
83
+ // onSessionStart opens the listener when --ahp was given. PiG emits session_shutdown before a
84
+ // replacement session starts (and re-runs the extension factory), so a listener opened with
85
+ // /ahp start has already been closed here; only --ahp brings it back for the new session.
86
+ func (rt *runtime) onSessionStart(ctx sdk.Context, id, cwd, reason string) {
87
+ rt.mu.Lock()
88
+ rt.ctx, rt.haveCtx = ctx, true
89
+ rt.mu.Unlock()
90
+ requested := false
91
+ if v, err := ctx.GetFlag(flagStart); err == nil {
92
+ requested, _ = v.(bool)
93
+ }
94
+ if requested {
95
+ if addr, err := rt.start(ctx); err != nil {
96
+ ctx.Notify("AHP: "+err.Error(), "error")
97
+ } else {
98
+ ctx.Notify(addr, "info")
99
+ }
100
+ }
101
+ }
102
+
103
+ func (rt *runtime) onSessionShutdown(ctx sdk.Context, reason string) {
104
+ restarts := false
105
+ if v, err := ctx.GetFlag(flagStart); err == nil {
106
+ restarts, _ = v.(bool)
107
+ }
108
+ if notice := replacementNotice(reason, rt.stop(), restarts); notice != "" {
109
+ ctx.Notify(notice, "warning")
110
+ }
111
+ }
112
+
113
+ func (rt *runtime) onModelChanged() {
114
+ rt.mu.Lock()
115
+ built := rt.built
116
+ rt.mu.Unlock()
117
+ if built == nil {
118
+ return
119
+ }
120
+ built.Host.DispatchServerAction(wire.RootChannel, ahptypes.StateAction{Value: &ahptypes.RootAgentsChangedAction{
121
+ Type: ahptypes.ActionTypeRootAgentsChanged, Agents: []ahptypes.AgentInfo{pi.BuildAgentInfo(rt.live.Models())},
122
+ }})
123
+ }
124
+
125
+ func isLoopback(host string) bool {
126
+ if host == "localhost" {
127
+ return true
128
+ }
129
+ ip := net.ParseIP(host)
130
+ return ip != nil && ip.IsLoopback()
131
+ }
132
+
133
+ // start opens the listener and returns the notice to show (listenNotice). It is only ever called on the user's explicit request.
134
+ func (rt *runtime) start(ctx sdk.Context) (string, error) {
135
+ rt.mu.Lock()
136
+ if rt.server != nil {
137
+ addr := rt.addr
138
+ rt.mu.Unlock()
139
+ return addr, nil
140
+ }
141
+ rt.mu.Unlock()
142
+ if rt.live.ID() == "" {
143
+ return "", errors.New("no PiG session is running yet")
144
+ }
145
+ path := settingsPath(&ctx)
146
+ direct, err := settings.LoadDirect(path)
147
+ if err != nil {
148
+ return "", err
149
+ }
150
+ access, err := settings.LoadAccess(path)
151
+ if err != nil {
152
+ return "", err
153
+ }
154
+ token := ""
155
+ if direct.Token != nil {
156
+ token = *direct.Token
157
+ }
158
+ if err := requireTokenOffLoopback(path, direct.Host, token); err != nil {
159
+ return "", err
160
+ }
161
+
162
+ cwd := rt.live.WorkingDirectory()
163
+ sessionRoot := sessionRootOf(ctx)
164
+ opts := compose.Options{
165
+ ServerInfo: &ahptypes.Implementation{Name: "pigpen-ahp", Version: strPtr(Version)},
166
+ WorkingDirectory: cwd, SessionRoot: sessionRoot,
167
+ Models: rt.live.Models,
168
+ CreateBackend: rt.live.BackendFactory,
169
+ CreateSessionManager: rt.createStore,
170
+ DefaultSelection: rt.live.CurrentSelection,
171
+ DeleteFile: rt.deleteFile(access),
172
+ WrapStore: rt.wrapStore,
173
+ AdoptForeignTurns: true,
174
+ Log: func(format string, args ...any) { fmt.Fprintf(os.Stderr, "ahp: "+format+"\n", args...) },
175
+ }
176
+ opts.Filesystem, opts.Terminals = accessOptions(access, cwd)
177
+ built, err := compose.Build(opts)
178
+ if err != nil {
179
+ return "", err
180
+ }
181
+ if err := rt.registerLiveSession(built, cwd); err != nil {
182
+ built.Close()
183
+ return "", err
184
+ }
185
+ server, err := ws.Serve(built.Host, ws.Options{
186
+ Addr: net.JoinHostPort(direct.Host, strconv.Itoa(direct.Port)), Token: token, AllowedOrigins: access.AllowedOrigins,
187
+ })
188
+ if err != nil {
189
+ built.Close()
190
+ return "", err
191
+ }
192
+ addr := listenNotice(server.Addr().String(), token)
193
+ rt.mu.Lock()
194
+ rt.server, rt.built, rt.addr = server, built, addr
195
+ rt.mu.Unlock()
196
+ return addr, nil
197
+ }
198
+
199
+ // stop closes the listener and the services behind it; false when nothing was running.
200
+ func (rt *runtime) stop() bool {
201
+ rt.mu.Lock()
202
+ server, built := rt.server, rt.built
203
+ rt.server, rt.built, rt.addr = nil, nil, ""
204
+ rt.mu.Unlock()
205
+ if server == nil {
206
+ return false
207
+ }
208
+ _ = server.Close()
209
+ built.Close()
210
+ return true
211
+ }
212
+
213
+ func sessionRootOf(ctx sdk.Context) string {
214
+ if f, err := ctx.GetSessionFile(); err == nil && f != nil {
215
+ // <root>/<encoded cwd>/<file>.jsonl
216
+ return filepath.Dir(filepath.Dir(*f))
217
+ }
218
+ return ""
219
+ }
220
+
221
+ func strPtr(s string) *string { return &s }
222
+
223
+ // createStore is the storage for sessions created through the host: only the running session
224
+ // exists, so a client cannot make new ones.
225
+ func (rt *runtime) createStore(cwd, id string) (pi.SessionStore, error) {
226
+ if id == rt.live.ID() {
227
+ return rt.live.NewStore(), nil
228
+ }
229
+ return nil, errors.New("this host serves the running PiG session only; a client cannot create sessions")
230
+ }
231
+
232
+ func (rt *runtime) wrapStore(sessionID string, opened pi.SessionStore) pi.SessionStore {
233
+ if sessionID == rt.live.ID() {
234
+ return rt.live.NewStore()
235
+ }
236
+ return opened
237
+ }
238
+
239
+ // deleteFile refuses to delete anything unless the settings allow it, and never the running
240
+ // session's file.
241
+ func (rt *runtime) deleteFile(access settings.Access) func(string) (pi.SessionFileDeletionResult, error) {
242
+ return guardedDelete(access.AllowSessionDeletion, func() string { return rt.live.NewStore().File() }, pi.FileDeleter)
243
+ }
244
+
245
+ func guardedDelete(allow bool, liveFile func() string, remove func(string) (pi.SessionFileDeletionResult, error)) func(string) (pi.SessionFileDeletionResult, error) {
246
+ return func(path string) (pi.SessionFileDeletionResult, error) {
247
+ if !allow {
248
+ return pi.SessionFileDeletionResult{Error: "deleting sessions from a client is disabled (settings: allowSessionDeletion)"}, nil
249
+ }
250
+ if live := liveFile(); live != "" && filepath.Clean(live) == filepath.Clean(path) {
251
+ return pi.SessionFileDeletionResult{Error: "the running PiG session cannot be deleted from a client"}, nil
252
+ }
253
+ return remove(path)
254
+ }
255
+ }
256
+
257
+ // accessOptions turns the capability settings into the optional services of the composition:
258
+ // nil (not exposed) unless the settings enable them. A filesystem without roots is confined to
259
+ // the working directory; only an explicit unrestricted lifts that.
260
+ func accessOptions(access settings.Access, cwd string) (*compose.FilesystemOptions, *compose.TerminalOptions) {
261
+ var fs *compose.FilesystemOptions
262
+ var terminals *compose.TerminalOptions
263
+ if access.Filesystem.Enabled {
264
+ roots := access.Filesystem.Roots
265
+ if len(roots) == 0 && !access.Filesystem.Unrestricted {
266
+ roots = []string{cwd}
267
+ }
268
+ fs = &compose.FilesystemOptions{Roots: roots, Unrestricted: access.Filesystem.Unrestricted}
269
+ }
270
+ if access.Terminals.Enabled {
271
+ terminals = &compose.TerminalOptions{}
272
+ }
273
+ return fs, terminals
274
+ }
275
+
276
+ // replacementNotice is what the user is told when a running listener closes with its session
277
+ // (session_shutdown for new, resume, fork or reload). With --ahp the next session opens it
278
+ // again, so there is nothing to say; after /ahp start it stays closed until asked again.
279
+ func replacementNotice(reason string, running, restarts bool) string {
280
+ if !running || restarts || reason == "quit" {
281
+ return ""
282
+ }
283
+ return "AHP listener stopped: the session was replaced (" + reason + "). /ahp start serves the new session."
284
+ }
285
+
286
+ // requireTokenOffLoopback refuses to expose an unauthenticated listener beyond this machine.
287
+ func requireTokenOffLoopback(path, host, token string) error {
288
+ if !isLoopback(host) && token == "" {
289
+ return fmt.Errorf("%s: host %q is not loopback, so a token is required", path, host)
290
+ }
291
+ return nil
292
+ }
293
+
294
+ // registerLiveSession makes the running session available to clients: hydrated from its file
295
+ // when it has history on disk, created empty otherwise, then its agent attached so events are
296
+ // mirrored from the start.
297
+ func (rt *runtime) registerLiveSession(built *compose.Built, cwd string) error {
298
+ id := rt.live.ID()
299
+ session := wire.SessionURI(id)
300
+ ctx := context.Background()
301
+ hydrated, err := built.Sessions.Hydrator.Hydrate(ctx, session)
302
+ if err != nil {
303
+ return err
304
+ }
305
+ if !hydrated {
306
+ if err := built.Sessions.Registry.Create(ctx, ahptypes.CreateSessionParams{Channel: session, WorkingDirectories: []ahptypes.URI{wire.PathToFileURI(cwd)}}); err != nil {
307
+ return err
308
+ }
309
+ }
310
+ return built.Sessions.Registry.EnsureBackend(session)
311
+ }
@@ -0,0 +1,110 @@
1
+ package ahp
2
+
3
+ import (
4
+ "net/url"
5
+ "regexp"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/pi"
10
+ )
11
+
12
+ func TestLoopbackHosts(t *testing.T) {
13
+ for host, want := range map[string]bool{
14
+ "127.0.0.1": true, "::1": true, "localhost": true, "127.1.2.3": true,
15
+ "0.0.0.0": false, "::": false, "192.168.1.5": false, "example.com": false, "": false,
16
+ } {
17
+ if got := isLoopback(host); got != want {
18
+ t.Errorf("isLoopback(%q) = %v, want %v", host, got, want)
19
+ }
20
+ }
21
+ }
22
+
23
+ func TestRemoteHostNeedsAToken(t *testing.T) {
24
+ if err := requireTokenOffLoopback("s.json", "0.0.0.0", ""); err == nil || !strings.Contains(err.Error(), "token is required") {
25
+ t.Fatalf("a public host without a token was accepted: %v", err)
26
+ }
27
+ for _, c := range []struct{ host, token string }{{"0.0.0.0", "secret"}, {"127.0.0.1", ""}, {"localhost", ""}} {
28
+ if err := requireTokenOffLoopback("s.json", c.host, c.token); err != nil {
29
+ t.Errorf("%+v refused: %v", c, err)
30
+ }
31
+ }
32
+ }
33
+
34
+ func TestSessionDeletionIsGuarded(t *testing.T) {
35
+ var removed []string
36
+ remove := func(p string) (pi.SessionFileDeletionResult, error) {
37
+ removed = append(removed, p)
38
+ return pi.SessionFileDeletionResult{OK: true}, nil
39
+ }
40
+ live := func() string { return "/sessions/./live.jsonl" }
41
+
42
+ off := guardedDelete(false, live, remove)
43
+ if r, _ := off("/sessions/other.jsonl"); r.OK || r.Error == "" {
44
+ t.Fatalf("deletion is off by default, got %+v", r)
45
+ }
46
+ on := guardedDelete(true, live, remove)
47
+ if r, _ := on("/sessions/live.jsonl"); r.OK || !strings.Contains(r.Error, "running") {
48
+ t.Fatalf("the running session's file must never be deleted, got %+v", r)
49
+ }
50
+ if len(removed) != 0 {
51
+ t.Fatalf("removed %v before any allowed deletion", removed)
52
+ }
53
+ if r, _ := on("/sessions/other.jsonl"); !r.OK {
54
+ t.Fatalf("an allowed deletion failed: %+v", r)
55
+ }
56
+ if len(removed) != 1 || removed[0] != "/sessions/other.jsonl" {
57
+ t.Fatalf("removed = %v", removed)
58
+ }
59
+ if r, _ := guardedDelete(true, func() string { return "" }, remove)("/sessions/x.jsonl"); !r.OK {
60
+ t.Fatalf("with no live file everything else may go: %+v", r)
61
+ }
62
+ }
63
+
64
+ // VS Code's "Agents: Add Remote Agent Host..." takes ws://<host>:<port>?tkn=<token>; the notice prints that form first and the
65
+ // generic one after it, and prints no token form for a listener without a token.
66
+ func TestListenNoticePrintsTheVSCodeAddress(t *testing.T) {
67
+ got := listenNotice("127.0.0.1:7117", "s3cret")
68
+ for _, want := range []string{"AHP listening on ws://127.0.0.1:7117?tkn=s3cret ", "VS Code", "Agents: Add Remote Agent Host...", "ws://127.0.0.1:7117/?token=s3cret"} {
69
+ if !strings.Contains(got, want) {
70
+ t.Errorf("notice %q lacks %q", got, want)
71
+ }
72
+ }
73
+ if got := listenNotice("127.0.0.1:7117", ""); got != "AHP listening on ws://127.0.0.1:7117" {
74
+ t.Errorf("a tokenless notice = %q", got)
75
+ }
76
+ }
77
+
78
+ func TestListenNoticeEscapesAToken(t *testing.T) {
79
+ got := listenNotice("127.0.0.1:1", "a b&c")
80
+ if !strings.Contains(got, "?token=a+b%26c") || !strings.Contains(got, "?tkn=a+b%26c") {
81
+ t.Errorf("a token with reserved characters must be query-escaped: %q", got)
82
+ }
83
+ }
84
+
85
+ // VS Code's SSH launcher (chat.sshRemoteAgentHostCommand) scrapes the command's output with this expression and takes the
86
+ // first match: microsoft/vscode src/vs/platform/agentHost/node/sshRemoteAgentHostHelpers.ts (AGENT_HOST_WS_URL_RE,
87
+ // extractAgentHostWebSocketURL). The token it captures is used as is.
88
+ var vsCodeSSHListeningURL = regexp.MustCompile(`ws://(?:127\.0\.0\.1|localhost):(\d+)(?:\?tkn=([^\s&]+))?`)
89
+
90
+ func TestListenNoticeIsReadByVSCodeSSHLauncher(t *testing.T) {
91
+ m := vsCodeSSHListeningURL.FindStringSubmatch(listenNotice("127.0.0.1:7117", "s3cret"))
92
+ if m == nil || m[1] != "7117" || m[2] != "s3cret" {
93
+ t.Fatalf("VS Code's SSH launcher reads port and token %q from the notice, want 7117 and s3cret", m)
94
+ }
95
+ }
96
+
97
+ // "Agents: Add Remote Agent Host..." takes the first ws:// URL in what is pasted, drops trailing ),.;] and moves `tkn` out
98
+ // of the query into the connection token: parseRemoteAgentHostInput in microsoft/vscode
99
+ // src/vs/platform/agentHost/common/remoteAgentHostService.ts. Pasting the whole notice must give the right token.
100
+ func TestListenNoticeIsReadByAddRemoteAgentHost(t *testing.T) {
101
+ notice := listenNotice("127.0.0.1:7117", "s3cret")
102
+ candidate := strings.TrimRight(regexp.MustCompile(`(?i)(?:https?|wss?)://\S+`).FindString(notice), "),.;]")
103
+ u, err := url.Parse(candidate)
104
+ if err != nil {
105
+ t.Fatalf("%q: %v", candidate, err)
106
+ }
107
+ if got := u.Query().Get("tkn"); got != "s3cret" || u.Host != "127.0.0.1:7117" {
108
+ t.Fatalf("pasting the notice gives host %q and tkn %q, want 127.0.0.1:7117 and s3cret (from %q)", u.Host, got, candidate)
109
+ }
110
+ }