@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,52 @@
1
+ package settings
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "reflect"
7
+ "testing"
8
+ )
9
+
10
+ // Additions: the capability keys are the port's own (upstream has a bare port/token/host file).
11
+
12
+ func writeSettings(t *testing.T, body string) string {
13
+ t.Helper()
14
+ path := filepath.Join(t.TempDir(), "settings.json")
15
+ if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
16
+ t.Fatal(err)
17
+ }
18
+ return path
19
+ }
20
+
21
+ func TestAccessDefaultsToNothing(t *testing.T) {
22
+ // a freshly created file (port and token only) grants no capability
23
+ path := filepath.Join(t.TempDir(), "ahp", "settings.json")
24
+ if _, err := LoadDirect(path); err != nil {
25
+ t.Fatal(err)
26
+ }
27
+ a, err := LoadAccess(path)
28
+ if err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ if a.Filesystem.Enabled || a.Terminals.Enabled || a.AllowSessionDeletion || len(a.AllowedOrigins) != 0 {
32
+ t.Fatalf("%+v", a)
33
+ }
34
+ }
35
+
36
+ func TestAccessReadsCapabilities(t *testing.T) {
37
+ a, err := LoadAccess(writeSettings(t, `{"port": 1, "allowedOrigins": ["https://vscode.dev"], "filesystem": {"enabled": true, "roots": ["/work"]}, "terminals": {"enabled": true}, "allowSessionDeletion": true}`))
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ if !a.Filesystem.Enabled || !reflect.DeepEqual(a.Filesystem.Roots, []string{"/work"}) || a.Filesystem.Unrestricted || !a.Terminals.Enabled || !a.AllowSessionDeletion || !reflect.DeepEqual(a.AllowedOrigins, []string{"https://vscode.dev"}) {
42
+ t.Fatalf("%+v", a)
43
+ }
44
+ }
45
+
46
+ func TestAccessRejectsWrongTypes(t *testing.T) {
47
+ for _, body := range []string{`{"filesystem": true}`, `{"terminals": {"enabled": "yes"}}`, `{"allowedOrigins": "x"}`, `{"filesystem": {"roots": "/work"}}`} {
48
+ if _, err := LoadAccess(writeSettings(t, body)); err == nil {
49
+ t.Errorf("%s must be refused", body)
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,117 @@
1
+ // Package settings reads and writes the listener settings file (port of pi-ahp's
2
+ // src/host/direct-settings.ts).
3
+ //
4
+ // Unlike the standalone upstream server, this extension never opens a listener on its own: the
5
+ // file is only loaded (and, if absent, created) after the user explicitly asked for one.
6
+ package settings
7
+
8
+ import (
9
+ "crypto/rand"
10
+ "encoding/base64"
11
+ "encoding/json"
12
+ "fmt"
13
+ "math"
14
+ "net"
15
+ "os"
16
+ "path/filepath"
17
+ )
18
+
19
+ // DefaultHost is where the listener binds unless told otherwise: loopback only.
20
+ const DefaultHost = "127.0.0.1"
21
+
22
+ // Direct is the endpoint configuration of the direct listener.
23
+ type Direct struct {
24
+ Port int `json:"port"`
25
+ Token *string `json:"token"`
26
+ Host string `json:"host"`
27
+ }
28
+
29
+ // Error is a problem with the settings file, naming the file.
30
+ type Error struct{ Path, Detail string }
31
+
32
+ func (e *Error) Error() string { return e.Path + ": " + e.Detail }
33
+
34
+ func freePort() (int, error) {
35
+ l, err := net.Listen("tcp", net.JoinHostPort(DefaultHost, "0"))
36
+ if err != nil {
37
+ return 0, err
38
+ }
39
+ defer l.Close()
40
+ return l.Addr().(*net.TCPAddr).Port, nil
41
+ }
42
+
43
+ func newToken() (string, error) {
44
+ var b [32]byte
45
+ if _, err := rand.Read(b[:]); err != nil {
46
+ return "", err
47
+ }
48
+ return base64.RawURLEncoding.EncodeToString(b[:]), nil
49
+ }
50
+
51
+ // LoadDirect reads the settings at path. A missing file is created with a free loopback port and
52
+ // a fresh token (only a missing file mints a token); an existing file is taken literally, with
53
+ // only the optional fields defaulted, and a malformed one is an error rather than an invitation
54
+ // to invent replacements.
55
+ func LoadDirect(path string) (Direct, error) {
56
+ raw, err := os.ReadFile(path)
57
+ if os.IsNotExist(err) {
58
+ port, err := freePort()
59
+ if err != nil {
60
+ return Direct{}, err
61
+ }
62
+ token, err := newToken()
63
+ if err != nil {
64
+ return Direct{}, err
65
+ }
66
+ settings := Direct{Port: port, Token: &token, Host: DefaultHost}
67
+ return settings, write(settings, path)
68
+ }
69
+ if err != nil {
70
+ return Direct{}, err
71
+ }
72
+
73
+ var stored map[string]json.RawMessage
74
+ if err := json.Unmarshal(raw, &stored); err != nil || stored == nil {
75
+ detail := "not an object"
76
+ if err != nil {
77
+ detail = err.Error()
78
+ }
79
+ return Direct{}, &Error{path, fmt.Sprintf("not valid JSON (%s)", detail)}
80
+ }
81
+ rawPort, hasPort := stored["port"]
82
+ if !hasPort {
83
+ return Direct{}, &Error{path, "no `port`. Set one, or delete the file to have it chosen."}
84
+ }
85
+ var portValue float64
86
+ if err := json.Unmarshal(rawPort, &portValue); err != nil || portValue != math.Trunc(portValue) || portValue < 1 || portValue > 65535 {
87
+ return Direct{}, &Error{path, fmt.Sprintf("`port` must be an integer 1-65535, got %s", rawPort)}
88
+ }
89
+ out := Direct{Port: int(portValue), Host: DefaultHost}
90
+ if rawToken, ok := stored["token"]; ok && string(rawToken) != "null" {
91
+ var token string
92
+ if err := json.Unmarshal(rawToken, &token); err != nil {
93
+ return Direct{}, &Error{path, fmt.Sprintf("`token` must be a string or null, got %s", rawToken)}
94
+ }
95
+ out.Token = &token // absent means absent
96
+ }
97
+ if rawHost, ok := stored["host"]; ok {
98
+ var host string
99
+ if err := json.Unmarshal(rawHost, &host); err != nil {
100
+ return Direct{}, &Error{path, fmt.Sprintf("`host` must be a string, got %s", rawHost)}
101
+ }
102
+ out.Host = host
103
+ }
104
+ return out, nil
105
+ }
106
+
107
+ func write(s Direct, path string) error {
108
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
109
+ return err
110
+ }
111
+ raw, err := json.MarshalIndent(s, "", " ")
112
+ if err != nil {
113
+ return err
114
+ }
115
+ // Owner-only: the token is a bearer credential for the endpoint.
116
+ return os.WriteFile(path, append(raw, '\n'), 0o600)
117
+ }
@@ -0,0 +1,104 @@
1
+ package settings_test
2
+
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "os"
7
+ "path/filepath"
8
+ "reflect"
9
+ "regexp"
10
+ "runtime"
11
+ "testing"
12
+
13
+ "github.com/MichaelKinsy/pigpen/ahp/internal/settings"
14
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
15
+ )
16
+
17
+ // Twins of upstream test/direct-settings.test.ts.
18
+
19
+ func fixture(t *testing.T) (path string, write func(any)) {
20
+ path = filepath.Join(t.TempDir(), "nested", "settings.json")
21
+ return path, func(v any) {
22
+ t.Helper()
23
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ var raw []byte
27
+ if s, ok := v.(string); ok {
28
+ raw = []byte(s)
29
+ } else {
30
+ raw, _ = json.Marshal(v)
31
+ }
32
+ if err := os.WriteFile(path, raw, 0o644); err != nil {
33
+ t.Fatal(err)
34
+ }
35
+ }
36
+ }
37
+
38
+ func TestDirectListenerSettings(t *testing.T) {
39
+ twin.Run(t, "direct-settings", "creates a private, reusable endpoint configuration on first use", func(t *testing.T) {
40
+ path, _ := fixture(t)
41
+ s, err := settings.LoadDirect(path)
42
+ if err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ if s.Host != "127.0.0.1" || s.Port < 1 || s.Port > 65535 {
46
+ t.Fatalf("settings = %+v", s)
47
+ }
48
+ if s.Token == nil || !regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`).MatchString(*s.Token) {
49
+ t.Fatalf("token = %v", s.Token)
50
+ }
51
+ raw, _ := os.ReadFile(path)
52
+ var onDisk settings.Direct
53
+ if err := json.Unmarshal(raw, &onDisk); err != nil || !reflect.DeepEqual(onDisk, s) {
54
+ t.Fatalf("on disk %+v (%v), returned %+v", onDisk, err, s)
55
+ }
56
+ if runtime.GOOS != "windows" {
57
+ if info, _ := os.Stat(path); info.Mode().Perm() != 0o600 {
58
+ t.Fatalf("mode = %v; the token is a bearer credential", info.Mode().Perm())
59
+ }
60
+ }
61
+ again, err := settings.LoadDirect(path)
62
+ if err != nil || !reflect.DeepEqual(again, s) {
63
+ t.Fatalf("second load %+v (%v)", again, err)
64
+ }
65
+ })
66
+
67
+ twin.Run(t, "direct-settings", "takes an existing file literally and defaults only optional fields", func(t *testing.T) {
68
+ path, write := fixture(t)
69
+ write(map[string]any{"port": 12345})
70
+ got, err := settings.LoadDirect(path)
71
+ if err != nil || got.Port != 12345 || got.Token != nil || got.Host != "127.0.0.1" {
72
+ t.Fatalf("got %+v (%v)", got, err)
73
+ }
74
+ write(map[string]any{"port": 23456, "token": nil, "host": "0.0.0.0"})
75
+ got, err = settings.LoadDirect(path)
76
+ if err != nil || got.Port != 23456 || got.Token != nil || got.Host != "0.0.0.0" {
77
+ t.Fatalf("got %+v (%v)", got, err)
78
+ }
79
+ })
80
+
81
+ twin.Run(t, "direct-settings", "rejects malformed or incomplete files instead of inventing replacements", func(t *testing.T) {
82
+ path, write := fixture(t)
83
+ cases := []struct {
84
+ value any
85
+ message string
86
+ }{
87
+ {"not json", `not valid JSON`},
88
+ {map[string]any{}, "no `port`"},
89
+ {map[string]any{"port": 0}, `integer 1-65535`},
90
+ {map[string]any{"port": 65536}, `integer 1-65535`},
91
+ {map[string]any{"port": 1.5}, `integer 1-65535`},
92
+ {map[string]any{"port": 12345, "token": 42}, `string or null`},
93
+ {map[string]any{"port": 12345, "host": 42}, `host.*string`},
94
+ }
95
+ for _, c := range cases {
96
+ write(c.value)
97
+ _, err := settings.LoadDirect(path)
98
+ var se *settings.Error
99
+ if !errors.As(err, &se) || !regexp.MustCompile(c.message).MatchString(se.Error()) {
100
+ t.Fatalf("%v: err = %v, want /%s/", c.value, err, c.message)
101
+ }
102
+ }
103
+ })
104
+ }
@@ -0,0 +1,15 @@
1
+ //go:build !unix
2
+
3
+ package svc
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "time"
9
+ )
10
+
11
+ func etagOf(info os.FileInfo) string {
12
+ return fmt.Sprintf("0-0-%d-%d", info.Size(), info.ModTime().UnixNano())
13
+ }
14
+
15
+ func ctimeOf(info os.FileInfo) time.Time { return info.ModTime() }
@@ -0,0 +1,34 @@
1
+ //go:build unix
2
+
3
+ package svc
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "strconv"
9
+ "syscall"
10
+ "time"
11
+ )
12
+
13
+ func msOf(sec, nsec int64) string {
14
+ return strconv.FormatFloat(float64(sec)*1e3+float64(nsec)/1e6, 'f', -1, 64)
15
+ }
16
+
17
+ // etagOf is an opaque change token: device, inode, size and both timestamps, so a write, a
18
+ // replacement or a metadata change all invalidate it.
19
+ func etagOf(info os.FileInfo) string {
20
+ if st, ok := info.Sys().(*syscall.Stat_t); ok {
21
+ mt, ct := statTimes(st)
22
+ return fmt.Sprintf("%d-%d-%d-%s-%s", st.Dev, st.Ino, st.Size, msOf(mt[0], mt[1]), msOf(ct[0], ct[1]))
23
+ }
24
+ return fmt.Sprintf("0-0-%d-%d", info.Size(), info.ModTime().UnixNano())
25
+ }
26
+
27
+ // ctimeOf is the inode change time (the birth time is not portable).
28
+ func ctimeOf(info os.FileInfo) time.Time {
29
+ if st, ok := info.Sys().(*syscall.Stat_t); ok {
30
+ _, ct := statTimes(st)
31
+ return time.Unix(ct[0], ct[1])
32
+ }
33
+ return info.ModTime()
34
+ }
@@ -0,0 +1,197 @@
1
+ package svc
2
+
3
+ import "strings"
4
+
5
+ // matchesGlob reports whether a slash-separated relative path matches a glob pattern with the
6
+ // semantics of Node's path.matchesGlob (minimatch): `*` and `?` stay inside one segment, `**`
7
+ // spans whole segments, `[...]` is a character class, `{a,b}` alternates, and wildcards never
8
+ // match a segment starting with a dot unless the pattern names the dot itself. A trailing `**`
9
+ // needs at least one segment (so `a/**` does not match `a`).
10
+ func matchesGlob(path, pattern string) bool {
11
+ segments := strings.Split(path, "/")
12
+ for _, alt := range expandBraces(pattern) {
13
+ if matchSegments(strings.Split(alt, "/"), segments) {
14
+ return true
15
+ }
16
+ }
17
+ return false
18
+ }
19
+
20
+ func expandBraces(pattern string) []string {
21
+ open := -1
22
+ depth := 0
23
+ for i := 0; i < len(pattern); i++ {
24
+ switch pattern[i] {
25
+ case '\\':
26
+ i++
27
+ case '{':
28
+ if depth == 0 {
29
+ open = i
30
+ }
31
+ depth++
32
+ case '}':
33
+ if depth == 0 {
34
+ continue
35
+ }
36
+ depth--
37
+ if depth == 0 {
38
+ body := pattern[open+1 : i]
39
+ var parts []string
40
+ level, start := 0, 0
41
+ for j := 0; j < len(body); j++ {
42
+ switch body[j] {
43
+ case '\\':
44
+ j++
45
+ case '{':
46
+ level++
47
+ case '}':
48
+ level--
49
+ case ',':
50
+ if level == 0 {
51
+ parts = append(parts, body[start:j])
52
+ start = j + 1
53
+ }
54
+ }
55
+ }
56
+ parts = append(parts, body[start:])
57
+ if len(parts) == 1 {
58
+ // {a} is literal in minimatch, like a brace without a comma.
59
+ return []string{pattern}
60
+ }
61
+ var out []string
62
+ for _, p := range parts {
63
+ out = append(out, expandBraces(pattern[:open]+p+pattern[i+1:])...)
64
+ }
65
+ return out
66
+ }
67
+ }
68
+ }
69
+ return []string{pattern}
70
+ }
71
+
72
+ func matchSegments(pattern, path []string) bool {
73
+ if len(pattern) == 0 {
74
+ return len(path) == 0
75
+ }
76
+ if pattern[0] == "**" {
77
+ if len(pattern) == 1 {
78
+ if len(path) == 0 {
79
+ return false
80
+ }
81
+ for _, s := range path {
82
+ if strings.HasPrefix(s, ".") {
83
+ return false
84
+ }
85
+ }
86
+ return true
87
+ }
88
+ for skip := 0; skip <= len(path); skip++ {
89
+ if matchSegments(pattern[1:], path[skip:]) {
90
+ return true
91
+ }
92
+ if skip < len(path) && strings.HasPrefix(path[skip], ".") {
93
+ break // ** never swallows a dot segment
94
+ }
95
+ }
96
+ return false
97
+ }
98
+ if len(path) == 0 {
99
+ return false
100
+ }
101
+ if strings.HasPrefix(path[0], ".") && !strings.HasPrefix(pattern[0], ".") {
102
+ return false
103
+ }
104
+ return matchSegment(pattern[0], path[0]) && matchSegments(pattern[1:], path[1:])
105
+ }
106
+
107
+ func matchSegment(pattern, name string) bool {
108
+ return matchRunes([]rune(pattern), []rune(name))
109
+ }
110
+
111
+ func matchRunes(p, s []rune) bool {
112
+ for len(p) > 0 {
113
+ switch p[0] {
114
+ case '*':
115
+ for len(p) > 0 && p[0] == '*' {
116
+ p = p[1:]
117
+ }
118
+ if len(p) == 0 {
119
+ return true
120
+ }
121
+ for i := 0; i <= len(s); i++ {
122
+ if matchRunes(p, s[i:]) {
123
+ return true
124
+ }
125
+ }
126
+ return false
127
+ case '?':
128
+ if len(s) == 0 {
129
+ return false
130
+ }
131
+ p, s = p[1:], s[1:]
132
+ case '[':
133
+ if len(s) == 0 {
134
+ return false
135
+ }
136
+ ok, rest, valid := matchClass(p, s[0])
137
+ if !valid {
138
+ // An unterminated class is a literal '['.
139
+ if s[0] != '[' {
140
+ return false
141
+ }
142
+ p, s = p[1:], s[1:]
143
+ continue
144
+ }
145
+ if !ok {
146
+ return false
147
+ }
148
+ p, s = rest, s[1:]
149
+ case '\\':
150
+ if len(p) > 1 {
151
+ p = p[1:]
152
+ }
153
+ fallthrough
154
+ default:
155
+ if len(s) == 0 || s[0] != p[0] {
156
+ return false
157
+ }
158
+ p, s = p[1:], s[1:]
159
+ }
160
+ }
161
+ return len(s) == 0
162
+ }
163
+
164
+ // matchClass matches r against the class at the start of p; valid is false when it never closes.
165
+ func matchClass(p []rune, r rune) (ok bool, rest []rune, valid bool) {
166
+ i := 1
167
+ negate := false
168
+ if i < len(p) && (p[i] == '!' || p[i] == '^') {
169
+ negate = true
170
+ i++
171
+ }
172
+ first := true
173
+ matched := false
174
+ for ; i < len(p); i++ {
175
+ if p[i] == ']' && !first {
176
+ return matched != negate, p[i+1:], true
177
+ }
178
+ first = false
179
+ lo := p[i]
180
+ if p[i] == '\\' && i+1 < len(p) {
181
+ i++
182
+ lo = p[i]
183
+ }
184
+ if i+2 < len(p) && p[i+1] == '-' && p[i+2] != ']' {
185
+ hi := p[i+2]
186
+ if lo <= r && r <= hi {
187
+ matched = true
188
+ }
189
+ i += 2
190
+ continue
191
+ }
192
+ if r == lo {
193
+ matched = true
194
+ }
195
+ }
196
+ return false, nil, false
197
+ }
@@ -0,0 +1,37 @@
1
+ package svc
2
+
3
+ import (
4
+ "path/filepath"
5
+ "strings"
6
+ )
7
+
8
+ // mimeTypes is the subset of the `mime` npm package's table (mime-db) that matters for source
9
+ // trees: the same answers for the extensions people actually open. An extension that is not here
10
+ // has no content type (text files fall back to text/plain at the call site).
11
+ var mimeTypes = map[string]string{
12
+ "txt": "text/plain", "text": "text/plain", "log": "text/plain", "conf": "text/plain", "ini": "text/plain",
13
+ "md": "text/markdown", "markdown": "text/markdown", "html": "text/html", "htm": "text/html", "css": "text/css",
14
+ "csv": "text/csv", "tsv": "text/tab-separated-values", "xml": "application/xml", "json": "application/json",
15
+ "map": "application/json", "yaml": "text/yaml", "yml": "text/yaml", "toml": "application/toml",
16
+ "js": "text/javascript", "mjs": "text/javascript", "cjs": "text/javascript", "jsx": "text/jsx", "ts": "video/mp2t",
17
+ "tsx": "text/jsx", "sh": "application/x-sh", "py": "text/x-python", "c": "text/x-c", "h": "text/x-c", "cc": "text/x-c",
18
+ "cpp": "text/x-c", "java": "text/x-java-source", "rs": "text/x-rust", "go": "text/x-go", "rb": "text/x-ruby",
19
+ "php": "application/x-httpd-php", "sql": "application/sql", "svg": "image/svg+xml", "png": "image/png",
20
+ "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "webp": "image/webp", "bmp": "image/bmp",
21
+ "ico": "image/vnd.microsoft.icon", "tif": "image/tiff", "tiff": "image/tiff", "avif": "image/avif",
22
+ "pdf": "application/pdf", "zip": "application/zip", "gz": "application/gzip", "tar": "application/x-tar",
23
+ "wasm": "application/wasm", "mp3": "audio/mpeg", "wav": "audio/wav", "mp4": "video/mp4", "webm": "video/webm",
24
+ "woff": "font/woff", "woff2": "font/woff2", "ttf": "font/ttf", "otf": "font/otf",
25
+ }
26
+
27
+ // contentTypeOf is the content type for a path's extension; "" when unknown and no text fallback.
28
+ func contentTypeOf(path string, textFallback bool) string {
29
+ ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), "."))
30
+ if t, ok := mimeTypes[ext]; ok {
31
+ return t
32
+ }
33
+ if textFallback {
34
+ return "text/plain"
35
+ }
36
+ return ""
37
+ }
@@ -0,0 +1,108 @@
1
+ // Package svc holds the services that expose the machine to a remote client: file resources,
2
+ // resource watches and terminals. Every one is opt-in and confined; nothing here runs unless the
3
+ // extension was explicitly configured to enable it (port of pi-ahp's resource-*, terminal
4
+ // services).
5
+ package svc
6
+
7
+ import (
8
+ "errors"
9
+ "fmt"
10
+ "io/fs"
11
+ "os"
12
+ "path/filepath"
13
+ "strings"
14
+
15
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
16
+ )
17
+
18
+ // canonicalPath resolves symlinks in path even when the path (or its tail) does not exist yet: a
19
+ // dangling symlink is followed to where it would create the file, so a write through a link that
20
+ // points outside the permitted roots cannot slip past the check.
21
+ func canonicalPath(path string) (string, error) {
22
+ absolute, err := filepath.Abs(path)
23
+ if err != nil {
24
+ return "", err
25
+ }
26
+ if real, err := filepath.EvalSymlinks(absolute); err == nil {
27
+ return real, nil
28
+ } else if !errors.Is(err, fs.ErrNotExist) {
29
+ return "", err
30
+ }
31
+ if info, err := os.Lstat(absolute); err == nil {
32
+ if info.Mode()&fs.ModeSymlink != 0 {
33
+ target, err := os.Readlink(absolute)
34
+ if err != nil {
35
+ return "", err
36
+ }
37
+ if !filepath.IsAbs(target) {
38
+ target = filepath.Join(filepath.Dir(absolute), target)
39
+ }
40
+ return canonicalPath(target)
41
+ }
42
+ } else if !errors.Is(err, fs.ErrNotExist) {
43
+ return "", err
44
+ }
45
+ parent := filepath.Dir(absolute)
46
+ if parent == absolute {
47
+ return absolute, nil
48
+ }
49
+ canonicalParent, err := canonicalPath(parent)
50
+ if err != nil {
51
+ return "", err
52
+ }
53
+ return filepath.Join(canonicalParent, filepath.Base(absolute)), nil
54
+ }
55
+
56
+ // PathPolicy turns file: URIs into local paths and, when roots are configured, refuses anything
57
+ // that resolves outside them. With no roots every path is allowed; the extension never builds a
58
+ // policy without roots unless the user explicitly asked for an unrestricted one.
59
+ type PathPolicy struct{ roots []string }
60
+
61
+ // NewPathPolicy creates a policy over roots (each must exist; they are canonicalised).
62
+ func NewPathPolicy(roots ...string) (*PathPolicy, error) {
63
+ p := &PathPolicy{}
64
+ for _, root := range roots {
65
+ abs, err := filepath.Abs(root)
66
+ if err != nil {
67
+ return nil, err
68
+ }
69
+ real, err := filepath.EvalSymlinks(abs)
70
+ if err != nil {
71
+ return nil, fmt.Errorf("resource root %s: %w", root, err)
72
+ }
73
+ p.roots = append(p.roots, real)
74
+ }
75
+ return p, nil
76
+ }
77
+
78
+ // Restricted reports whether the policy confines paths to roots.
79
+ func (p *PathPolicy) Restricted() bool { return len(p.roots) > 0 }
80
+
81
+ // PathFor returns the local path of a file: URI, or a protocol error: InvalidParams for a wrong
82
+ // scheme or malformed URI, PermissionDenied for a path outside the roots.
83
+ func (p *PathPolicy) PathFor(uri string) (string, error) {
84
+ if !strings.HasPrefix(uri, "file://") {
85
+ return "", wire.InvalidParams("Only file: URIs are supported, got: " + uri)
86
+ }
87
+ path, err := wire.FileURIToPath(uri)
88
+ if err != nil {
89
+ return "", wire.InvalidParams("Malformed file URI: " + uri)
90
+ }
91
+ if len(p.roots) == 0 {
92
+ return path, nil
93
+ }
94
+ candidate, err := canonicalPath(path)
95
+ if err != nil {
96
+ return "", wire.Coded(wire.CodePermissionDenied, err.Error())
97
+ }
98
+ for _, root := range p.roots {
99
+ prefix := root
100
+ if !strings.HasSuffix(prefix, string(filepath.Separator)) {
101
+ prefix += string(filepath.Separator)
102
+ }
103
+ if candidate == root || strings.HasPrefix(candidate, prefix) {
104
+ return path, nil
105
+ }
106
+ }
107
+ return "", wire.Coded(wire.CodePermissionDenied, "Outside the permitted roots: "+uri)
108
+ }