@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,495 @@
1
+ package svc
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/base64"
7
+ "errors"
8
+ "io"
9
+ "io/fs"
10
+ "os"
11
+ "path/filepath"
12
+ "sort"
13
+ "strings"
14
+ "sync"
15
+ "syscall"
16
+ "time"
17
+ "unicode/utf8"
18
+
19
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
20
+ ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
21
+ )
22
+
23
+ // ResourceOptions configures a ResourceService.
24
+ type ResourceOptions struct {
25
+ // Paths maps and confines URIs. A nil policy means unrestricted, which the extension only uses
26
+ // when the user explicitly asked for it.
27
+ Paths *PathPolicy
28
+ // ReadVirtual answers reads of non-file resources (nil result means "not mine").
29
+ ReadVirtual func(ahptypes.ResourceReadParams) (*ahptypes.ResourceReadResult, error)
30
+ }
31
+
32
+ // ResourceService is the filesystem behind the resource* commands (port of resource-service.ts).
33
+ // It implements host.ResourceHandler.
34
+ type ResourceService struct {
35
+ paths *PathPolicy
36
+ readVirtual func(ahptypes.ResourceReadParams) (*ahptypes.ResourceReadResult, error)
37
+
38
+ mu sync.Mutex
39
+ locks map[string]*pathLock
40
+ }
41
+
42
+ type pathLock struct {
43
+ mu sync.Mutex
44
+ refs int
45
+ }
46
+
47
+ // NewResourceService creates the service.
48
+ func NewResourceService(opts ResourceOptions) *ResourceService {
49
+ paths := opts.Paths
50
+ if paths == nil {
51
+ paths = &PathPolicy{}
52
+ }
53
+ return &ResourceService{paths: paths, readVirtual: opts.ReadVirtual, locks: map[string]*pathLock{}}
54
+ }
55
+
56
+ func pathErr(code int, msg string) *wire.Error { return wire.Coded(code, msg) }
57
+
58
+ // translate maps a filesystem error onto the protocol error a client can branch on.
59
+ func translate(err error, uri string) error {
60
+ var we *wire.Error
61
+ if errors.As(err, &we) {
62
+ return err
63
+ }
64
+ switch {
65
+ case errors.Is(err, fs.ErrNotExist):
66
+ return wire.NotFound(uri)
67
+ case errors.Is(err, fs.ErrPermission):
68
+ return pathErr(wire.CodePermissionDenied, "Permission denied: "+uri)
69
+ case errors.Is(err, fs.ErrExist):
70
+ return pathErr(wire.CodeAlreadyExists, "Already exists: "+uri)
71
+ case errors.Is(err, syscall.ENOTEMPTY):
72
+ return pathErr(wire.CodeConflict, "Directory not empty: "+uri)
73
+ case errors.Is(err, syscall.EISDIR):
74
+ return wire.InvalidParams("Is a directory: " + uri)
75
+ case errors.Is(err, syscall.ENOTDIR):
76
+ return wire.InvalidParams("Not a directory: " + uri)
77
+ }
78
+ return pathErr(wire.CodePermissionDenied, err.Error())
79
+ }
80
+
81
+ func isUTF8Text(data []byte) bool { return utf8.Valid(data) && !bytes.Contains(data, []byte{0}) }
82
+
83
+ func strPtr(s string) *string { return &s }
84
+
85
+ // Read implements host.ResourceHandler.
86
+ func (s *ResourceService) Read(_ context.Context, p ahptypes.ResourceReadParams) (ahptypes.ResourceReadResult, error) {
87
+ if s.readVirtual != nil {
88
+ virtual, err := s.readVirtual(p)
89
+ if err != nil {
90
+ return ahptypes.ResourceReadResult{}, err
91
+ }
92
+ if virtual != nil {
93
+ return *virtual, nil
94
+ }
95
+ }
96
+ path, err := s.paths.PathFor(p.Uri)
97
+ if err != nil {
98
+ return ahptypes.ResourceReadResult{}, err
99
+ }
100
+ data, err := os.ReadFile(path)
101
+ if err != nil {
102
+ return ahptypes.ResourceReadResult{}, translate(err, p.Uri)
103
+ }
104
+ // Never claim UTF-8 when decoding would replace invalid bytes. Unknown extensions still
105
+ // remain editable when their contents are valid text.
106
+ text := isUTF8Text(data)
107
+ result := ahptypes.ResourceReadResult{Encoding: ahptypes.ContentEncodingBase64}
108
+ if text && (p.Encoding == nil || *p.Encoding != ahptypes.ContentEncodingBase64) {
109
+ result.Encoding = ahptypes.ContentEncodingUtf8
110
+ result.Data = string(data)
111
+ } else {
112
+ result.Data = base64.StdEncoding.EncodeToString(data)
113
+ }
114
+ if ct := contentTypeOf(path, text); ct != "" {
115
+ result.ContentType = strPtr(ct)
116
+ }
117
+ return result, nil
118
+ }
119
+
120
+ func (s *ResourceService) lockPath(path string) func() {
121
+ s.mu.Lock()
122
+ l := s.locks[path]
123
+ if l == nil {
124
+ l = &pathLock{}
125
+ s.locks[path] = l
126
+ }
127
+ l.refs++
128
+ s.mu.Unlock()
129
+ l.mu.Lock()
130
+ return func() {
131
+ l.mu.Unlock()
132
+ s.mu.Lock()
133
+ if l.refs--; l.refs == 0 {
134
+ delete(s.locks, path)
135
+ }
136
+ s.mu.Unlock()
137
+ }
138
+ }
139
+
140
+ // Write implements host.ResourceHandler.
141
+ func (s *ResourceService) Write(_ context.Context, p ahptypes.ResourceWriteParams) error {
142
+ path, err := s.paths.PathFor(p.Uri)
143
+ if err != nil {
144
+ return err
145
+ }
146
+ var data []byte
147
+ switch p.Encoding {
148
+ case ahptypes.ContentEncodingBase64:
149
+ data, err = base64.StdEncoding.DecodeString(p.Data)
150
+ if err != nil {
151
+ // Node's Buffer.from(…, "base64") is lenient; accept unpadded input too.
152
+ if data, err = base64.RawStdEncoding.DecodeString(strings.TrimRight(p.Data, "=")); err != nil {
153
+ return wire.InvalidParams("Invalid base64 data: " + p.Uri)
154
+ }
155
+ }
156
+ case ahptypes.ContentEncodingUtf8:
157
+ data = []byte(p.Data)
158
+ default:
159
+ return wire.InvalidParams("Unsupported content encoding: " + string(p.Encoding))
160
+ }
161
+ mode := ahptypes.ResourceWriteModeTruncate
162
+ if p.Mode != nil {
163
+ mode = *p.Mode
164
+ }
165
+ if mode != ahptypes.ResourceWriteModeTruncate && mode != ahptypes.ResourceWriteModeAppend && mode != ahptypes.ResourceWriteModeInsert {
166
+ return wire.InvalidParams("Unsupported write mode: " + string(mode))
167
+ }
168
+ var position int64
169
+ if p.Position != nil {
170
+ position = *p.Position
171
+ }
172
+ if position < 0 {
173
+ return wire.InvalidParams("Write position must be a non-negative integer")
174
+ }
175
+ unlock := s.lockPath(path)
176
+ defer unlock()
177
+ if err := writeLocked(path, p, data, mode, position); err != nil {
178
+ return translate(err, p.Uri)
179
+ }
180
+ return nil
181
+ }
182
+
183
+ func writeLocked(path string, p ahptypes.ResourceWriteParams, data []byte, mode ahptypes.ResourceWriteMode, position int64) error {
184
+ createOnly := p.CreateOnly != nil && *p.CreateOnly
185
+ if createOnly && p.IfMatch == nil {
186
+ f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
187
+ if err != nil {
188
+ return err
189
+ }
190
+ return closeAfter(f, data)
191
+ }
192
+ if p.IfMatch != nil {
193
+ current := ""
194
+ exists := false
195
+ if info, err := os.Stat(path); err == nil {
196
+ current, exists = etagOf(info), true
197
+ } else if !errors.Is(err, fs.ErrNotExist) {
198
+ return err
199
+ }
200
+ if createOnly && exists {
201
+ return pathErr(wire.CodeAlreadyExists, "Already exists: "+p.Uri)
202
+ }
203
+ if !exists || *p.IfMatch != current {
204
+ return pathErr(wire.CodeConflict, "ifMatch precondition failed: "+p.Uri)
205
+ }
206
+ }
207
+ if position == 0 && mode != ahptypes.ResourceWriteModeInsert {
208
+ flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
209
+ if mode == ahptypes.ResourceWriteModeAppend {
210
+ flag = os.O_WRONLY | os.O_CREATE | os.O_APPEND
211
+ }
212
+ f, err := os.OpenFile(path, flag, 0o666)
213
+ if err != nil {
214
+ return err
215
+ }
216
+ return closeAfter(f, data)
217
+ }
218
+ existing, err := os.ReadFile(path)
219
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
220
+ return err
221
+ }
222
+ length := int64(len(existing))
223
+ var offset int64
224
+ if mode == ahptypes.ResourceWriteModeAppend {
225
+ offset = length - position
226
+ if offset < 0 {
227
+ offset = 0
228
+ }
229
+ } else {
230
+ offset = position
231
+ if offset > length {
232
+ offset = length
233
+ }
234
+ }
235
+ updated := append([]byte{}, existing[:offset]...)
236
+ updated = append(updated, data...)
237
+ if mode != ahptypes.ResourceWriteModeTruncate {
238
+ updated = append(updated, existing[offset:]...)
239
+ }
240
+ f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
241
+ if err != nil {
242
+ return err
243
+ }
244
+ return closeAfter(f, updated)
245
+ }
246
+
247
+ func closeAfter(f *os.File, data []byte) error {
248
+ _, err := f.Write(data)
249
+ if cerr := f.Close(); err == nil {
250
+ err = cerr
251
+ }
252
+ return err
253
+ }
254
+
255
+ // List implements host.ResourceHandler: directories first, then names in locale order.
256
+ func (s *ResourceService) List(_ context.Context, uri string) (ahptypes.ResourceListResult, error) {
257
+ path, err := s.paths.PathFor(uri)
258
+ if err != nil {
259
+ return ahptypes.ResourceListResult{}, err
260
+ }
261
+ found, err := os.ReadDir(path)
262
+ if err != nil {
263
+ return ahptypes.ResourceListResult{}, translate(err, uri)
264
+ }
265
+ entries := make([]ahptypes.DirectoryEntry, 0, len(found))
266
+ for _, entry := range found {
267
+ kind := "file"
268
+ if s.isDirectoryEntry(path, entry) {
269
+ kind = "directory"
270
+ }
271
+ entries = append(entries, ahptypes.DirectoryEntry{Name: entry.Name(), Type: kind})
272
+ }
273
+ sort.SliceStable(entries, func(i, j int) bool {
274
+ a, b := entries[i], entries[j]
275
+ if a.Type == b.Type {
276
+ return localeLess(a.Name, b.Name)
277
+ }
278
+ return a.Type == "directory"
279
+ })
280
+ return ahptypes.ResourceListResult{Entries: entries}, nil
281
+ }
282
+
283
+ func localeLess(a, b string) bool {
284
+ la, lb := strings.ToLower(a), strings.ToLower(b)
285
+ if la != lb {
286
+ return la < lb
287
+ }
288
+ return a < b
289
+ }
290
+
291
+ func (s *ResourceService) isDirectoryEntry(parent string, entry fs.DirEntry) bool {
292
+ if entry.Type()&fs.ModeSymlink == 0 {
293
+ return entry.IsDir()
294
+ }
295
+ path := filepath.Join(parent, entry.Name())
296
+ // DirectoryEntry has no symlink kind; an unresolved or forbidden link is safest as a
297
+ // non-navigable file entry.
298
+ if _, err := s.paths.PathFor(wire.PathToFileURI(path)); err != nil {
299
+ return false
300
+ }
301
+ info, err := os.Stat(path)
302
+ return err == nil && info.IsDir()
303
+ }
304
+
305
+ func isoMillis(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
306
+
307
+ // Resolve implements host.ResourceHandler.
308
+ func (s *ResourceService) Resolve(_ context.Context, p ahptypes.ResourceResolveParams) (ahptypes.ResourceResolveResult, error) {
309
+ path, err := s.paths.PathFor(p.Uri)
310
+ if err != nil {
311
+ return ahptypes.ResourceResolveResult{}, err
312
+ }
313
+ follow := p.FollowSymlinks == nil || *p.FollowSymlinks
314
+ var info os.FileInfo
315
+ realPath := path
316
+ if follow {
317
+ info, err = os.Stat(path)
318
+ if err == nil {
319
+ realPath, err = filepath.EvalSymlinks(path)
320
+ }
321
+ } else {
322
+ info, err = os.Lstat(path)
323
+ }
324
+ if err != nil {
325
+ return ahptypes.ResourceResolveResult{}, translate(err, p.Uri)
326
+ }
327
+ kind := ahptypes.ResourceTypeFile
328
+ switch {
329
+ case info.IsDir():
330
+ kind = ahptypes.ResourceTypeDirectory
331
+ case info.Mode()&fs.ModeSymlink != 0:
332
+ kind = ahptypes.ResourceTypeSymlink
333
+ }
334
+ size := info.Size()
335
+ result := ahptypes.ResourceResolveResult{
336
+ Uri: wire.PathToFileURI(realPath),
337
+ Type: kind,
338
+ Size: &size,
339
+ Mtime: strPtr(isoMillis(info.ModTime())),
340
+ Ctime: strPtr(isoMillis(ctimeOf(info))),
341
+ Etag: strPtr(etagOf(info)),
342
+ }
343
+ if kind == ahptypes.ResourceTypeFile {
344
+ if ct := contentTypeOf(path, false); ct != "" {
345
+ result.ContentType = strPtr(ct)
346
+ }
347
+ }
348
+ return result, nil
349
+ }
350
+
351
+ // Mkdir implements host.ResourceHandler (mkdir -p).
352
+ func (s *ResourceService) Mkdir(_ context.Context, p ahptypes.ResourceMkdirParams) error {
353
+ path, err := s.paths.PathFor(p.Uri)
354
+ if err != nil {
355
+ return err
356
+ }
357
+ if err := os.MkdirAll(path, 0o777); err != nil {
358
+ return translate(err, p.Uri)
359
+ }
360
+ return nil
361
+ }
362
+
363
+ // Delete implements host.ResourceHandler. A directory is only removed when recursion is asked
364
+ // for; the refusal is PermissionDenied so a client can tell it from "already exists".
365
+ func (s *ResourceService) Delete(_ context.Context, p ahptypes.ResourceDeleteParams) error {
366
+ path, err := s.paths.PathFor(p.Uri)
367
+ if err != nil {
368
+ return err
369
+ }
370
+ info, err := os.Lstat(path)
371
+ if err != nil {
372
+ return translate(err, p.Uri)
373
+ }
374
+ recursive := p.Recursive != nil && *p.Recursive
375
+ if info.IsDir() {
376
+ if !recursive {
377
+ return pathErr(wire.CodePermissionDenied, "Path is a directory: rm returned EISDIR (rm '"+path+"')")
378
+ }
379
+ if err := os.RemoveAll(path); err != nil {
380
+ return translate(err, p.Uri)
381
+ }
382
+ return nil
383
+ }
384
+ if err := os.Remove(path); err != nil {
385
+ return translate(err, p.Uri)
386
+ }
387
+ return nil
388
+ }
389
+
390
+ func (s *ResourceService) guardDestination(path, uri string, failIfExists *bool) error {
391
+ if failIfExists == nil || !*failIfExists {
392
+ return nil
393
+ }
394
+ if _, err := os.Lstat(path); err != nil {
395
+ if errors.Is(err, fs.ErrNotExist) {
396
+ return nil
397
+ }
398
+ return translate(err, uri)
399
+ }
400
+ return pathErr(wire.CodeAlreadyExists, "Already exists: "+uri)
401
+ }
402
+
403
+ // Move implements host.ResourceHandler.
404
+ func (s *ResourceService) Move(_ context.Context, p ahptypes.ResourceMoveParams) error {
405
+ source, err := s.paths.PathFor(p.Source)
406
+ if err != nil {
407
+ return err
408
+ }
409
+ destination, err := s.paths.PathFor(p.Destination)
410
+ if err != nil {
411
+ return err
412
+ }
413
+ if err := s.guardDestination(destination, p.Destination, p.FailIfExists); err != nil {
414
+ return err
415
+ }
416
+ if err := os.Rename(source, destination); err != nil {
417
+ return translate(err, p.Source)
418
+ }
419
+ return nil
420
+ }
421
+
422
+ // Copy implements host.ResourceHandler; symlinks inside a copied directory stay links.
423
+ func (s *ResourceService) Copy(_ context.Context, p ahptypes.ResourceCopyParams) error {
424
+ source, err := s.paths.PathFor(p.Source)
425
+ if err != nil {
426
+ return err
427
+ }
428
+ destination, err := s.paths.PathFor(p.Destination)
429
+ if err != nil {
430
+ return err
431
+ }
432
+ if err := s.guardDestination(destination, p.Destination, p.FailIfExists); err != nil {
433
+ return err
434
+ }
435
+ info, err := os.Lstat(source)
436
+ if err != nil {
437
+ return translate(err, p.Source)
438
+ }
439
+ if info.IsDir() {
440
+ err = copyTree(source, destination)
441
+ } else {
442
+ err = copyFile(source, destination, info.Mode().Perm())
443
+ }
444
+ if err != nil {
445
+ return translate(err, p.Source)
446
+ }
447
+ return nil
448
+ }
449
+
450
+ func copyFile(source, destination string, perm fs.FileMode) error {
451
+ in, err := os.Open(source)
452
+ if err != nil {
453
+ return err
454
+ }
455
+ defer in.Close()
456
+ out, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
457
+ if err != nil {
458
+ return err
459
+ }
460
+ if _, err := io.Copy(out, in); err != nil {
461
+ out.Close()
462
+ return err
463
+ }
464
+ return out.Close()
465
+ }
466
+
467
+ func copyTree(source, destination string) error {
468
+ return filepath.WalkDir(source, func(path string, d fs.DirEntry, err error) error {
469
+ if err != nil {
470
+ return err
471
+ }
472
+ rel, err := filepath.Rel(source, path)
473
+ if err != nil {
474
+ return err
475
+ }
476
+ target := filepath.Join(destination, rel)
477
+ info, err := d.Info()
478
+ if err != nil {
479
+ return err
480
+ }
481
+ switch {
482
+ case d.IsDir():
483
+ return os.MkdirAll(target, info.Mode().Perm()|0o700)
484
+ case d.Type()&fs.ModeSymlink != 0:
485
+ link, err := os.Readlink(path)
486
+ if err != nil {
487
+ return err
488
+ }
489
+ _ = os.Remove(target)
490
+ return os.Symlink(link, target)
491
+ default:
492
+ return copyFile(path, target, info.Mode().Perm())
493
+ }
494
+ })
495
+ }