@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,356 @@
1
+ /**
2
+ * The `resource*` command family — filesystem access over the protocol.
3
+ *
4
+ * These are connection-level (`channel: 'ahp-root://'`) and symmetrical: the
5
+ * same nine methods may be issued in either direction. This module implements
6
+ * the host side, backed by the local filesystem.
7
+ *
8
+ * They matter most when host and client are on different machines, which is the
9
+ * case this host is built for: without them a remote client cannot browse,
10
+ * open, or edit anything the agent is working on.
11
+ *
12
+ * @see https://microsoft.github.io/agent-host-protocol/specification/root-channel
13
+ */
14
+
15
+ import { isUtf8 } from "node:buffer";
16
+ import { constants, type Dirent, type Stats } from "node:fs";
17
+ import {
18
+ access,
19
+ copyFile,
20
+ cp,
21
+ lstat,
22
+ mkdir,
23
+ readdir,
24
+ readFile,
25
+ realpath,
26
+ rename,
27
+ rm,
28
+ stat,
29
+ writeFile,
30
+ } from "node:fs/promises";
31
+ import { join } from "node:path";
32
+ import { pathToFileURL } from "node:url";
33
+ import {
34
+ AhpErrorCodes,
35
+ ContentEncoding,
36
+ type DirectoryEntry,
37
+ type ResourceCopyParams,
38
+ type ResourceDeleteParams,
39
+ type ResourceListResult,
40
+ type ResourceMkdirParams,
41
+ type ResourceMoveParams,
42
+ type ResourceReadParams,
43
+ type ResourceReadResult,
44
+ type ResourceResolveParams,
45
+ type ResourceResolveResult,
46
+ ResourceType,
47
+ ResourceWriteMode,
48
+ type ResourceWriteParams,
49
+ } from "@microsoft/agent-host-protocol";
50
+ import mime from "mime";
51
+ import { ProtocolError } from "../protocol/errors.ts";
52
+ import { ResourcePathPolicy } from "./resource-paths.ts";
53
+
54
+ export interface ResourceServiceOptions {
55
+ /** Resolves host-owned virtual content references before filesystem routing. */
56
+ readonly readVirtual?: (params: ResourceReadParams) => Promise<ResourceReadResult | undefined>;
57
+ /**
58
+ * Directories a client may reach, as filesystem paths.
59
+ *
60
+ * Empty (the default) means unrestricted, which is honest rather than lax: a
61
+ * client that can reach this endpoint can already start a session and run
62
+ * shell commands, so a filesystem allowlist
63
+ * on its own is not a security boundary. Configured roots must already exist;
64
+ * they are canonicalized once so later symlink changes cannot retarget them.
65
+ */
66
+ readonly roots?: readonly string[];
67
+ /** Shared with resource watches when both surfaces use the same policy. */
68
+ readonly pathPolicy?: ResourcePathPolicy;
69
+ }
70
+
71
+ function isUtf8Text(data: Buffer): boolean {
72
+ return isUtf8(data) && !data.includes(0);
73
+ }
74
+
75
+ function contentTypeOf(path: string, textFallback = false): string | undefined {
76
+ return mime.getType(path) ?? (textFallback ? "text/plain" : undefined);
77
+ }
78
+
79
+ function etagOf(stats: Stats): string {
80
+ return `${stats.dev}-${stats.ino}-${stats.size}-${stats.mtimeMs}-${stats.ctimeMs}`;
81
+ }
82
+
83
+ function errorCodeOf(error: unknown): string | undefined {
84
+ return (error as NodeJS.ErrnoException | undefined)?.code;
85
+ }
86
+
87
+ /** Maps a Node filesystem error onto the protocol's error codes. */
88
+ function translate(error: unknown, uri: string): ProtocolError {
89
+ if (error instanceof ProtocolError) return error;
90
+ switch (errorCodeOf(error)) {
91
+ case "ENOENT":
92
+ return ProtocolError.notFound(uri);
93
+ case "EACCES":
94
+ case "EPERM":
95
+ return new ProtocolError(AhpErrorCodes.PermissionDenied, `Permission denied: ${uri}`);
96
+ case "EEXIST":
97
+ return new ProtocolError(AhpErrorCodes.AlreadyExists, `Already exists: ${uri}`);
98
+ case "ENOTEMPTY":
99
+ return new ProtocolError(AhpErrorCodes.Conflict, `Directory not empty: ${uri}`);
100
+ case "EISDIR":
101
+ return ProtocolError.invalidParams(`Is a directory: ${uri}`);
102
+ case "ENOTDIR":
103
+ return ProtocolError.invalidParams(`Not a directory: ${uri}`);
104
+ default:
105
+ return new ProtocolError(AhpErrorCodes.PermissionDenied, error instanceof Error ? error.message : String(error));
106
+ }
107
+ }
108
+
109
+ export class ResourceService {
110
+ readonly #paths: ResourcePathPolicy;
111
+ readonly #readVirtual: ResourceServiceOptions["readVirtual"];
112
+ readonly #writeTails = new Map<string, Promise<void>>();
113
+
114
+ constructor(options: ResourceServiceOptions = {}) {
115
+ this.#paths = options.pathPolicy ?? new ResourcePathPolicy(options.roots);
116
+ this.#readVirtual = options.readVirtual;
117
+ }
118
+
119
+ // ── Commands ────────────────────────────────────────────────────────────
120
+
121
+ async read(params: ResourceReadParams): Promise<ResourceReadResult> {
122
+ const virtual = typeof params?.uri === "string" ? await this.#readVirtual?.(params) : undefined;
123
+ if (virtual) return virtual;
124
+ const path = await this.#paths.pathFor(params.uri);
125
+ try {
126
+ const buffer = await readFile(path);
127
+ // Never claim UTF-8 when decoding would replace invalid bytes. Unknown
128
+ // extensions still remain editable when their contents are valid text.
129
+ const text = isUtf8Text(buffer);
130
+ const encoding =
131
+ params.encoding === ContentEncoding.Base64 || !text ? ContentEncoding.Base64 : ContentEncoding.Utf8;
132
+ const contentType = contentTypeOf(path, text);
133
+ return {
134
+ data: buffer.toString(encoding === ContentEncoding.Utf8 ? "utf8" : "base64"),
135
+ encoding,
136
+ ...(contentType ? { contentType } : {}),
137
+ };
138
+ } catch (error) {
139
+ throw translate(error, params.uri);
140
+ }
141
+ }
142
+
143
+ async write(params: ResourceWriteParams): Promise<Record<string, never>> {
144
+ const path = await this.#paths.pathFor(params.uri);
145
+ if (params.encoding !== ContentEncoding.Base64 && params.encoding !== ContentEncoding.Utf8) {
146
+ throw ProtocolError.invalidParams(`Unsupported content encoding: ${params.encoding}`);
147
+ }
148
+ const data = Buffer.from(params.data, params.encoding === ContentEncoding.Base64 ? "base64" : "utf8");
149
+ const mode = params.mode ?? ResourceWriteMode.Truncate;
150
+ if (mode !== ResourceWriteMode.Truncate && mode !== ResourceWriteMode.Append && mode !== ResourceWriteMode.Insert) {
151
+ throw ProtocolError.invalidParams(`Unsupported write mode: ${mode}`);
152
+ }
153
+ const position = params.position ?? 0;
154
+ if (!Number.isSafeInteger(position) || position < 0) {
155
+ throw ProtocolError.invalidParams("Write position must be a non-negative integer");
156
+ }
157
+ if (params.ifMatch !== undefined && typeof params.ifMatch !== "string") {
158
+ throw ProtocolError.invalidParams("ifMatch must be a string");
159
+ }
160
+
161
+ try {
162
+ await this.#queueWrite(path, () => this.#writeLocked(path, params, data, mode, position));
163
+ return {};
164
+ } catch (error) {
165
+ throw translate(error, params.uri);
166
+ }
167
+ }
168
+
169
+ async #queueWrite(path: string, operation: () => Promise<void>): Promise<void> {
170
+ const previous = this.#writeTails.get(path) ?? Promise.resolve();
171
+ const current = previous.catch(() => undefined).then(operation);
172
+ this.#writeTails.set(path, current);
173
+ try {
174
+ await current;
175
+ } finally {
176
+ if (this.#writeTails.get(path) === current) this.#writeTails.delete(path);
177
+ }
178
+ }
179
+
180
+ async #writeLocked(
181
+ path: string,
182
+ params: ResourceWriteParams,
183
+ data: Buffer,
184
+ mode: ResourceWriteMode,
185
+ position: number,
186
+ ): Promise<void> {
187
+ if (params.createOnly && params.ifMatch === undefined) {
188
+ await writeFile(path, data, { flag: "wx" });
189
+ return;
190
+ }
191
+
192
+ if (params.ifMatch !== undefined) {
193
+ let currentEtag: string | undefined;
194
+ try {
195
+ currentEtag = etagOf(await stat(path));
196
+ } catch (error) {
197
+ if (errorCodeOf(error) !== "ENOENT") throw error;
198
+ }
199
+ if (params.createOnly && currentEtag !== undefined) {
200
+ throw new ProtocolError(AhpErrorCodes.AlreadyExists, `Already exists: ${params.uri}`);
201
+ }
202
+ if (params.ifMatch !== currentEtag) {
203
+ throw new ProtocolError(AhpErrorCodes.Conflict, `ifMatch precondition failed: ${params.uri}`);
204
+ }
205
+ }
206
+
207
+ if (position === 0 && mode !== ResourceWriteMode.Insert) {
208
+ // Keep ordinary appends on O_APPEND; truncate remains a direct overwrite.
209
+ await writeFile(path, data, { flag: mode === ResourceWriteMode.Append ? "a" : "w" });
210
+ return;
211
+ }
212
+
213
+ let existing: Buffer;
214
+ try {
215
+ existing = await readFile(path);
216
+ } catch (error) {
217
+ if (errorCodeOf(error) !== "ENOENT") throw error;
218
+ existing = Buffer.alloc(0);
219
+ }
220
+ const offset =
221
+ mode === ResourceWriteMode.Append ? Math.max(0, existing.length - position) : Math.min(position, existing.length);
222
+ const updated =
223
+ mode === ResourceWriteMode.Truncate
224
+ ? Buffer.concat([existing.subarray(0, offset), data])
225
+ : Buffer.concat([existing.subarray(0, offset), data, existing.subarray(offset)]);
226
+ await writeFile(path, updated, { flag: "w" });
227
+ }
228
+
229
+ async list(uri: string): Promise<ResourceListResult> {
230
+ const path = await this.#paths.pathFor(uri);
231
+ try {
232
+ const found = await readdir(path, { withFileTypes: true });
233
+ const entries: DirectoryEntry[] = await Promise.all(
234
+ found.map(async (entry) => ({
235
+ name: entry.name,
236
+ type: (await this.#isDirectoryEntry(path, entry)) ? "directory" : "file",
237
+ })),
238
+ );
239
+ entries.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1));
240
+ return { entries };
241
+ } catch (error) {
242
+ throw translate(error, uri);
243
+ }
244
+ }
245
+
246
+ async #isDirectoryEntry(parent: string, entry: Dirent): Promise<boolean> {
247
+ if (!entry.isSymbolicLink()) return entry.isDirectory();
248
+ const path = join(parent, entry.name);
249
+ try {
250
+ await this.#paths.pathFor(pathToFileURL(path).toString());
251
+ return (await stat(path)).isDirectory();
252
+ } catch {
253
+ // DirectoryEntry has no symlink kind; an unresolved or forbidden link is
254
+ // safest as a non-navigable file entry.
255
+ return false;
256
+ }
257
+ }
258
+
259
+ async resolve(params: ResourceResolveParams): Promise<ResourceResolveResult> {
260
+ const path = await this.#paths.pathFor(params.uri);
261
+ const followSymlinks = params.followSymlinks ?? true;
262
+ try {
263
+ const stats = followSymlinks ? await stat(path) : await lstat(path);
264
+ const realPath = followSymlinks ? await realpath(path) : path;
265
+ const type = stats.isDirectory()
266
+ ? ResourceType.Directory
267
+ : stats.isSymbolicLink()
268
+ ? ResourceType.Symlink
269
+ : ResourceType.File;
270
+ const contentType = type === ResourceType.File ? contentTypeOf(path) : undefined;
271
+ return {
272
+ uri: pathToFileURL(realPath).toString(),
273
+ type,
274
+ size: stats.size,
275
+ mtime: stats.mtime.toISOString(),
276
+ ctime: stats.ctime.toISOString(),
277
+ ...(contentType ? { contentType } : {}),
278
+ // Opaque change token shared with resourceWrite's ifMatch check.
279
+ etag: etagOf(stats),
280
+ };
281
+ } catch (error) {
282
+ throw translate(error, params.uri);
283
+ }
284
+ }
285
+
286
+ async mkdir(params: ResourceMkdirParams): Promise<Record<string, never>> {
287
+ const path = await this.#paths.pathFor(params.uri);
288
+ try {
289
+ await mkdir(path, { recursive: true });
290
+ return {};
291
+ } catch (error) {
292
+ throw translate(error, params.uri);
293
+ }
294
+ }
295
+
296
+ async delete(params: ResourceDeleteParams): Promise<Record<string, never>> {
297
+ const path = await this.#paths.pathFor(params.uri);
298
+ try {
299
+ const stats = await lstat(path);
300
+ if (stats.isDirectory() && !params.recursive) {
301
+ // Non-recursive delete of a non-empty directory must fail rather
302
+ // than quietly take the tree with it.
303
+ await rm(path, { recursive: false });
304
+ } else {
305
+ await rm(path, { recursive: params.recursive ?? false, force: false });
306
+ }
307
+ return {};
308
+ } catch (error) {
309
+ throw translate(error, params.uri);
310
+ }
311
+ }
312
+
313
+ async move(params: ResourceMoveParams): Promise<Record<string, never>> {
314
+ const source = await this.#paths.pathFor(params.source);
315
+ const destination = await this.#paths.pathFor(params.destination);
316
+ await this.#guardDestination(destination, params.destination, params.failIfExists);
317
+ try {
318
+ await rename(source, destination);
319
+ return {};
320
+ } catch (error) {
321
+ throw translate(error, params.source);
322
+ }
323
+ }
324
+
325
+ async copy(params: ResourceCopyParams): Promise<Record<string, never>> {
326
+ const source = await this.#paths.pathFor(params.source);
327
+ const destination = await this.#paths.pathFor(params.destination);
328
+ await this.#guardDestination(destination, params.destination, params.failIfExists);
329
+ try {
330
+ const stats = await lstat(source);
331
+ if (stats.isDirectory()) {
332
+ await cp(source, destination, { recursive: true });
333
+ } else {
334
+ await copyFile(source, destination);
335
+ }
336
+ return {};
337
+ } catch (error) {
338
+ throw translate(error, params.source);
339
+ }
340
+ }
341
+
342
+ async #guardDestination(path: string, uri: string, failIfExists: boolean | undefined): Promise<void> {
343
+ if (!failIfExists) {
344
+ return;
345
+ }
346
+ try {
347
+ await access(path, constants.F_OK);
348
+ } catch (error) {
349
+ if (errorCodeOf(error) === "ENOENT") {
350
+ return;
351
+ }
352
+ throw translate(error, uri);
353
+ }
354
+ throw new ProtocolError(AhpErrorCodes.AlreadyExists, `Already exists: ${uri}`);
355
+ }
356
+ }
@@ -0,0 +1,35 @@
1
+ /** Pure path filtering and per-batch transitions used by resource watches. */
2
+ import { isAbsolute, matchesGlob, relative, resolve, sep } from "node:path";
3
+ import { ResourceChangeType } from "@microsoft/agent-host-protocol";
4
+
5
+ /** Returns a POSIX-style path below root, or undefined for an escaped path. */
6
+ export function relativeWatchPath(root: string, path: string): string | undefined {
7
+ const result = relative(root, resolve(path));
8
+ if (isAbsolute(result) || result === ".." || result.startsWith(`..${sep}`)) return undefined;
9
+ return result.split(sep).join("/");
10
+ }
11
+
12
+ export function isExcluded(path: string, excludes: readonly string[]): boolean {
13
+ return path.length > 0 && excludes.some((pattern) => matchesGlob(path, pattern));
14
+ }
15
+
16
+ export function matchesPatterns(path: string, includes: readonly string[], excludes: readonly string[]): boolean {
17
+ // The root itself is always in scope; filters describe its descendants.
18
+ if (path.length === 0) return true;
19
+ return (
20
+ !isExcluded(path, excludes) && (includes.length === 0 || includes.some((pattern) => matchesGlob(path, pattern)))
21
+ );
22
+ }
23
+
24
+ /** Coalesces one path's transitions without losing its state at batch boundaries. */
25
+ export function mergeChange(
26
+ previous: ResourceChangeType | undefined,
27
+ next: ResourceChangeType,
28
+ ): ResourceChangeType | undefined {
29
+ if (previous === undefined) return next;
30
+ if (previous === ResourceChangeType.Added)
31
+ return next === ResourceChangeType.Deleted ? undefined : ResourceChangeType.Added;
32
+ if (previous === ResourceChangeType.Deleted)
33
+ return next === ResourceChangeType.Deleted ? ResourceChangeType.Deleted : ResourceChangeType.Updated;
34
+ return next === ResourceChangeType.Deleted ? ResourceChangeType.Deleted : ResourceChangeType.Updated;
35
+ }