@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.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Package wire holds the protocol-level helpers of the AHP host: channel URI classification,
|
|
2
|
+
// protocol errors, version negotiation, the client-dispatchable action table and file: URI
|
|
3
|
+
// conversion. It has no I/O. Ports pi-ahp src/core/channels.ts, uri.ts and src/protocol/*.
|
|
4
|
+
package wire
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"net/url"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"regexp"
|
|
12
|
+
"runtime"
|
|
13
|
+
"strings"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// ResourceWatchScheme prefixes every resource watch channel URI ("<scheme>/<id>").
|
|
17
|
+
const ResourceWatchScheme = "ahp-resource-watch:"
|
|
18
|
+
|
|
19
|
+
// RootChannel is the singleton root channel.
|
|
20
|
+
const RootChannel = "ahp-root://"
|
|
21
|
+
|
|
22
|
+
// ChannelKind classifies a channel URI by the reducer that serves it.
|
|
23
|
+
type ChannelKind string
|
|
24
|
+
|
|
25
|
+
const (
|
|
26
|
+
KindRoot ChannelKind = "root"
|
|
27
|
+
KindSession ChannelKind = "session"
|
|
28
|
+
KindChat ChannelKind = "chat"
|
|
29
|
+
KindTerminal ChannelKind = "terminal"
|
|
30
|
+
KindChangeset ChannelKind = "changeset"
|
|
31
|
+
KindResourceWatch ChannelKind = "resourceWatch"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// JSON-RPC and AHP error codes (pinned spec: errors.generated.go).
|
|
35
|
+
const (
|
|
36
|
+
CodeParseError = -32700
|
|
37
|
+
CodeInvalidRequest = -32600
|
|
38
|
+
CodeMethodNotFound = -32601
|
|
39
|
+
CodeInvalidParams = -32602
|
|
40
|
+
CodeInternalError = -32603
|
|
41
|
+
CodeSessionNotFound = -32001
|
|
42
|
+
CodeProviderNotFound = -32002
|
|
43
|
+
CodeSessionAlreadyExists = -32003
|
|
44
|
+
CodeTurnInProgress = -32004
|
|
45
|
+
CodeUnsupportedProtocolVersion = -32005
|
|
46
|
+
CodeAuthRequired = -32007
|
|
47
|
+
CodeNotFound = -32008
|
|
48
|
+
CodePermissionDenied = -32009
|
|
49
|
+
CodeAlreadyExists = -32010
|
|
50
|
+
// CodeConflict is the code of the npm 0.9.0 package pi-ahp was written against (errors.ts,
|
|
51
|
+
// AhpErrorCodes.Conflict); the pinned spec commit has no such code, see PORT.md.
|
|
52
|
+
CodeConflict = -32011
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// ProtocolVersion is the one AHP version this host speaks (pinned: 0.9.0).
|
|
56
|
+
const ProtocolVersion = "0.9.0"
|
|
57
|
+
|
|
58
|
+
// Error is a protocol error: handlers return it and the router turns it into a JSON-RPC error response.
|
|
59
|
+
type Error struct {
|
|
60
|
+
Code int
|
|
61
|
+
Message string
|
|
62
|
+
Data any
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (e *Error) Error() string { return e.Message }
|
|
66
|
+
|
|
67
|
+
const (
|
|
68
|
+
sessionScheme = "ahp-session:"
|
|
69
|
+
chatScheme = "ahp-chat:"
|
|
70
|
+
terminalScheme = "ahp-terminal:"
|
|
71
|
+
changesetScheme = "ahp-changeset:"
|
|
72
|
+
resourceWatchScheme = "ahp-resource-watch:"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// KindOf classifies a channel URI, false for a scheme this host does not serve. Deliberately
|
|
76
|
+
// strict: it drives subscribe, where guessing wrong would create a channel with the wrong reducer.
|
|
77
|
+
func KindOf(uri string) (ChannelKind, bool) {
|
|
78
|
+
switch {
|
|
79
|
+
case uri == RootChannel:
|
|
80
|
+
return KindRoot, true
|
|
81
|
+
case strings.HasPrefix(uri, sessionScheme+"/"):
|
|
82
|
+
return KindSession, true
|
|
83
|
+
case strings.HasPrefix(uri, chatScheme+"/"):
|
|
84
|
+
return KindChat, true
|
|
85
|
+
case strings.HasPrefix(uri, terminalScheme+"/"):
|
|
86
|
+
return KindTerminal, true
|
|
87
|
+
case strings.HasPrefix(uri, changesetScheme+"/"):
|
|
88
|
+
return KindChangeset, true
|
|
89
|
+
case strings.HasPrefix(uri, resourceWatchScheme+"/"):
|
|
90
|
+
return KindResourceWatch, true
|
|
91
|
+
}
|
|
92
|
+
return "", false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ActionBelongsToChannel reports whether an action type's namespace belongs to a channel kind.
|
|
96
|
+
func ActionBelongsToChannel(actionType string, kind ChannelKind) bool {
|
|
97
|
+
return strings.HasPrefix(actionType, string(kind)+"/")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// IsChatChannel reports whether the URI is `ahp-chat:/…`.
|
|
101
|
+
func IsChatChannel(uri string) bool { return strings.HasPrefix(uri, chatScheme+"/") }
|
|
102
|
+
|
|
103
|
+
// SessionURI returns `ahp-session:/<id>`.
|
|
104
|
+
func SessionURI(id string) string { return sessionScheme + "/" + id }
|
|
105
|
+
|
|
106
|
+
// ChatURI returns `ahp-chat:/<id>`.
|
|
107
|
+
func ChatURI(id string) string { return chatScheme + "/" + id }
|
|
108
|
+
|
|
109
|
+
// ChatIDFromURI extracts `<id>` from `ahp-chat:/<id>`.
|
|
110
|
+
func ChatIDFromURI(uri string) (string, bool) {
|
|
111
|
+
if !IsChatChannel(uri) {
|
|
112
|
+
return "", false
|
|
113
|
+
}
|
|
114
|
+
id := uri[len(chatScheme)+1:]
|
|
115
|
+
return id, id != ""
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var singleSegment = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9+.-]*):/([^/?#]+)$`)
|
|
119
|
+
|
|
120
|
+
// SessionIDFromURI identifies a session from its canonical scheme or from explicit provider
|
|
121
|
+
// aliases given as bare scheme names. Shape alone never makes an unknown URI a session.
|
|
122
|
+
func SessionIDFromURI(uri string, providerAliases ...string) (string, bool) {
|
|
123
|
+
m := singleSegment.FindStringSubmatch(uri)
|
|
124
|
+
if m == nil {
|
|
125
|
+
return "", false
|
|
126
|
+
}
|
|
127
|
+
scheme := strings.ToLower(m[1])
|
|
128
|
+
if scheme == "ahp-session" {
|
|
129
|
+
return m[2], true
|
|
130
|
+
}
|
|
131
|
+
for _, alias := range providerAliases {
|
|
132
|
+
if scheme == strings.ToLower(alias) {
|
|
133
|
+
return m[2], true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return "", false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func newError(code int, message string, data any) *Error {
|
|
140
|
+
return &Error{Code: code, Message: message, Data: data}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// InvalidParams and the other constructors mirror pi-ahp's ProtocolError statics.
|
|
144
|
+
// Coded builds a protocol error with an explicit code.
|
|
145
|
+
func Coded(code int, message string) *Error { return newError(code, message, nil) }
|
|
146
|
+
|
|
147
|
+
func InvalidParams(message string) *Error { return newError(CodeInvalidParams, message, nil) }
|
|
148
|
+
func MethodNotFound(method string) *Error {
|
|
149
|
+
return newError(CodeMethodNotFound, "Unknown method: "+method, nil)
|
|
150
|
+
}
|
|
151
|
+
func SessionNotFound(channel string) *Error {
|
|
152
|
+
return newError(CodeSessionNotFound, "Session not found: "+channel, nil)
|
|
153
|
+
}
|
|
154
|
+
func SessionAlreadyExists(channel string) *Error {
|
|
155
|
+
return newError(CodeSessionAlreadyExists, "Session already exists: "+channel, nil)
|
|
156
|
+
}
|
|
157
|
+
func ProviderNotFound(provider string) *Error {
|
|
158
|
+
return newError(CodeProviderNotFound, "No agent for provider: "+provider, nil)
|
|
159
|
+
}
|
|
160
|
+
func NotFound(uri string) *Error { return newError(CodeNotFound, "Not found: "+uri, nil) }
|
|
161
|
+
|
|
162
|
+
// HostSupportedVersions is what this host can speak. Generated wire models do not down-convert
|
|
163
|
+
// required fields across minor versions, so it is the current version only.
|
|
164
|
+
func HostSupportedVersions() []string { return []string{ProtocolVersion} }
|
|
165
|
+
|
|
166
|
+
// NegotiateProtocolVersion picks the client's most-preferred version this host also speaks.
|
|
167
|
+
func NegotiateProtocolVersion(offered []string) (string, error) {
|
|
168
|
+
if len(offered) == 0 {
|
|
169
|
+
return "", InvalidParams("initialize requires a non-empty protocolVersions array of strings")
|
|
170
|
+
}
|
|
171
|
+
for _, candidate := range offered {
|
|
172
|
+
for _, supported := range HostSupportedVersions() {
|
|
173
|
+
if candidate == supported {
|
|
174
|
+
return candidate, nil
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return "", newError(CodeUnsupportedProtocolVersion,
|
|
179
|
+
"No mutually supported protocol version. Offered: "+strings.Join(offered, ", "),
|
|
180
|
+
map[string]any{"supportedVersions": HostSupportedVersions()})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// IsClientDispatchable reports whether a client may dispatch an action of this type.
|
|
184
|
+
func IsClientDispatchable(actionType string) bool { return clientDispatchable[actionType] }
|
|
185
|
+
|
|
186
|
+
// FileURIToPath converts a file: URI to a filesystem path. A value that is not a file: URI is
|
|
187
|
+
// returned unchanged: clients occasionally send a bare path where the protocol asks for a URI.
|
|
188
|
+
// A URI naming a remote host is an error (Node's fileURLToPath does the same).
|
|
189
|
+
func FileURIToPath(uri string) (string, error) {
|
|
190
|
+
if !strings.HasPrefix(uri, "file://") {
|
|
191
|
+
return uri, nil
|
|
192
|
+
}
|
|
193
|
+
u, err := url.Parse(uri)
|
|
194
|
+
if err != nil {
|
|
195
|
+
return "", fmt.Errorf("invalid file URL %q: %w", uri, err)
|
|
196
|
+
}
|
|
197
|
+
if u.Host != "" && u.Host != "localhost" {
|
|
198
|
+
return "", fmt.Errorf("file URL host must be \"localhost\" or empty on %s: %q", runtime.GOOS, uri)
|
|
199
|
+
}
|
|
200
|
+
p := u.Path
|
|
201
|
+
if runtime.GOOS == "windows" {
|
|
202
|
+
p = strings.TrimPrefix(p, "/")
|
|
203
|
+
return filepath.FromSlash(p), nil
|
|
204
|
+
}
|
|
205
|
+
if strings.Contains(u.EscapedPath(), "%2F") || strings.Contains(u.EscapedPath(), "%2f") {
|
|
206
|
+
return "", errors.New("file URL path must not include encoded / characters")
|
|
207
|
+
}
|
|
208
|
+
return p, nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// PathToFileURI converts an absolute path to a file: URI.
|
|
212
|
+
func PathToFileURI(path string) string {
|
|
213
|
+
p := filepath.ToSlash(path)
|
|
214
|
+
if !strings.HasPrefix(p, "/") {
|
|
215
|
+
p = "/" + p
|
|
216
|
+
}
|
|
217
|
+
return (&url.URL{Scheme: "file", Path: p}).String()
|
|
218
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
package wire_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"reflect"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/twin"
|
|
8
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Twins of upstream test/uri.test.ts (src/core/uri.ts).
|
|
12
|
+
func TestFileURIConversion(t *testing.T) {
|
|
13
|
+
twin.Run(t, "uri", "decodes percent-escapes", func(t *testing.T) {
|
|
14
|
+
got, err := wire.FileURIToPath("file:///tmp/a%20b")
|
|
15
|
+
if err != nil || got != "/tmp/a b" {
|
|
16
|
+
t.Fatalf("got %q, %v; want /tmp/a b", got, err)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
twin.Run(t, "uri", "round-trips a path", func(t *testing.T) {
|
|
20
|
+
got, err := wire.FileURIToPath(wire.PathToFileURI("/tmp/a b/c"))
|
|
21
|
+
if err != nil || got != "/tmp/a b/c" {
|
|
22
|
+
t.Fatalf("got %q, %v; want /tmp/a b/c", got, err)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
twin.Run(t, "uri", "passes a bare path through", func(t *testing.T) {
|
|
26
|
+
// Clients occasionally send a path where the protocol asks for a URI.
|
|
27
|
+
got, err := wire.FileURIToPath("/tmp/plain")
|
|
28
|
+
if err != nil || got != "/tmp/plain" {
|
|
29
|
+
t.Fatalf("got %q, %v; want /tmp/plain", got, err)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
twin.Run(t, "uri", "refuses a URI naming a remote host", func(t *testing.T) {
|
|
33
|
+
if got, err := wire.FileURIToPath("file://host/share/x"); err == nil {
|
|
34
|
+
t.Fatalf("expected an error, got %q", got)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Channel helpers (src/core/channels.ts); no upstream test file, so these are additional Go cases.
|
|
40
|
+
func TestChannelKinds(t *testing.T) {
|
|
41
|
+
cases := map[string]wire.ChannelKind{
|
|
42
|
+
"ahp-root://": wire.KindRoot,
|
|
43
|
+
"ahp-session:/abc": wire.KindSession,
|
|
44
|
+
"ahp-chat:/abc": wire.KindChat,
|
|
45
|
+
"ahp-terminal:/t1": wire.KindTerminal,
|
|
46
|
+
"ahp-changeset:/c": wire.KindChangeset,
|
|
47
|
+
"ahp-resource-watch:/w": wire.KindResourceWatch,
|
|
48
|
+
}
|
|
49
|
+
for uri, want := range cases {
|
|
50
|
+
if got, ok := wire.KindOf(uri); !ok || got != want {
|
|
51
|
+
t.Errorf("KindOf(%q) = %v, %v; want %v", uri, got, ok, want)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for _, uri := range []string{"ahp-unknown:/x", "pi:/abc", "", "ahp-session:"} {
|
|
55
|
+
if got, ok := wire.KindOf(uri); ok {
|
|
56
|
+
t.Errorf("KindOf(%q) = %v, want unsupported", uri, got)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if !wire.ActionBelongsToChannel("chat/delta", wire.KindChat) || wire.ActionBelongsToChannel("chat/delta", wire.KindSession) {
|
|
60
|
+
t.Error("action namespace must match the channel kind")
|
|
61
|
+
}
|
|
62
|
+
if wire.ActionBelongsToChannel("resourceWatch/changed", wire.KindResourceWatch) != true {
|
|
63
|
+
t.Error("resourceWatch/ belongs to resourceWatch")
|
|
64
|
+
}
|
|
65
|
+
if wire.SessionURI("x") != "ahp-session:/x" || wire.ChatURI("x") != "ahp-chat:/x" {
|
|
66
|
+
t.Error("uri constructors")
|
|
67
|
+
}
|
|
68
|
+
if id, ok := wire.ChatIDFromURI("ahp-chat:/abc"); !ok || id != "abc" {
|
|
69
|
+
t.Errorf("ChatIDFromURI = %q, %v", id, ok)
|
|
70
|
+
}
|
|
71
|
+
if _, ok := wire.ChatIDFromURI("ahp-session:/abc"); ok {
|
|
72
|
+
t.Error("a session URI is not a chat")
|
|
73
|
+
}
|
|
74
|
+
if id, ok := wire.SessionIDFromURI("ahp-session:/abc"); !ok || id != "abc" {
|
|
75
|
+
t.Errorf("SessionIDFromURI canonical = %q, %v", id, ok)
|
|
76
|
+
}
|
|
77
|
+
if id, ok := wire.SessionIDFromURI("pi:/abc", "pi"); !ok || id != "abc" {
|
|
78
|
+
t.Errorf("SessionIDFromURI alias = %q, %v", id, ok)
|
|
79
|
+
}
|
|
80
|
+
if _, ok := wire.SessionIDFromURI("pi:/abc"); ok {
|
|
81
|
+
t.Error("shape alone never makes an unknown URI a session")
|
|
82
|
+
}
|
|
83
|
+
if _, ok := wire.SessionIDFromURI("ahp-session:/a/b"); ok {
|
|
84
|
+
t.Error("a session URI has a single path segment")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func TestNegotiateProtocolVersion(t *testing.T) {
|
|
89
|
+
got, err := wire.NegotiateProtocolVersion([]string{"0.9.0", "0.8.0"})
|
|
90
|
+
if err != nil || got != "0.9.0" {
|
|
91
|
+
t.Fatalf("got %q, %v", got, err)
|
|
92
|
+
}
|
|
93
|
+
got, err = wire.NegotiateProtocolVersion([]string{"99.0.0", "0.9.0"})
|
|
94
|
+
if err != nil || got != "0.9.0" {
|
|
95
|
+
t.Fatalf("unknown versions are skipped: got %q, %v", got, err)
|
|
96
|
+
}
|
|
97
|
+
_, err = wire.NegotiateProtocolVersion([]string{"0.8.0"})
|
|
98
|
+
var pe *wire.Error
|
|
99
|
+
if !asError(err, &pe) || pe.Code != wire.CodeUnsupportedProtocolVersion {
|
|
100
|
+
t.Fatalf("want UnsupportedProtocolVersion, got %v", err)
|
|
101
|
+
}
|
|
102
|
+
if !reflect.DeepEqual(pe.Data, map[string]any{"supportedVersions": []string{"0.9.0"}}) {
|
|
103
|
+
t.Fatalf("error data = %#v", pe.Data)
|
|
104
|
+
}
|
|
105
|
+
if _, err = wire.NegotiateProtocolVersion(nil); err == nil {
|
|
106
|
+
t.Fatal("an empty offer is invalid")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The table is generated from the pinned spec's @clientDispatchable annotations
|
|
111
|
+
// (types/action-origin.generated.ts, IS_CLIENT_DISPATCHABLE); this checks a sample of both kinds.
|
|
112
|
+
func TestClientDispatchable(t *testing.T) {
|
|
113
|
+
for _, typ := range []string{"root/configChanged", "session/titleChanged", "chat/turnStarted", "chat/turnCancelled", "chat/truncated", "terminal/input", "terminal/cleared"} {
|
|
114
|
+
if !wire.IsClientDispatchable(typ) {
|
|
115
|
+
t.Errorf("%s must be client-dispatchable", typ)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for _, typ := range []string{"root/agentsChanged", "session/ready", "chat/delta", "chat/turnComplete", "terminal/data", "resourceWatch/changed", "nope/unknown"} {
|
|
119
|
+
if wire.IsClientDispatchable(typ) {
|
|
120
|
+
t.Errorf("%s must be server-only", typ)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
package ws_test
|
|
2
|
+
|
|
3
|
+
import "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
4
|
+
|
|
5
|
+
func activeSessions(n int64) ahptypes.StateAction {
|
|
6
|
+
return ahptypes.StateAction{Value: &ahptypes.RootActiveSessionsChangedAction{Type: ahptypes.ActionTypeRootActiveSessionsChanged, ActiveSessions: n}}
|
|
7
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
package ws_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"io"
|
|
7
|
+
"net"
|
|
8
|
+
"net/http"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/pi"
|
|
14
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
15
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/ws"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// VS Code's Agents window connects to a WebSocket AHP host given as ws://<host>:<port>?tkn=<token>
|
|
19
|
+
// (command "Agents: Add Remote Agent Host...", or the chat.remoteAgentHosts setting). These tests send what VS Code sends, as
|
|
20
|
+
// read from microsoft/vscode main; nothing here was run against a live VS Code. Files are under src/vs/platform/agentHost/.
|
|
21
|
+
|
|
22
|
+
// vsCodeInitialize is VS Code's first message, field for field (browser/agentHostProtocolClient.ts, connect): request id 1
|
|
23
|
+
// (_nextRequestId starts at 1), the root channel, every protocol version it can speak newest first with 0.8.0 left out
|
|
24
|
+
// (CLIENT_SUPPORTED_PROTOCOL_VERSIONS, from common/state/protocol/version/registry.ts SUPPORTED_PROTOCOL_VERSIONS), a UUID
|
|
25
|
+
// client id, the Agents window's clientInfo (common/agentHostClientInfo.ts), the _meta of toAgentHostClientMeta
|
|
26
|
+
// (common/agentHostTelemetry.ts) for a direct WebSocket, and the root as the one initial subscription.
|
|
27
|
+
func vsCodeInitialize(t *testing.T) []byte { return vsCodeInitializeOffering(t, vsCodeOffers["main"]) }
|
|
28
|
+
|
|
29
|
+
// vsCodeOffers is the protocolVersions list each VS Code sends: 1.140.0 speaks 0.9.0 itself (PROTOCOL_VERSION), release/1.141
|
|
30
|
+
// and main speak 0.10.0 and still offer 0.9.0.
|
|
31
|
+
var vsCodeOffers = map[string][]string{
|
|
32
|
+
"1.140.0": {"0.9.0", "0.7.0", "0.6.0", "0.5.2", "0.5.1"},
|
|
33
|
+
"main": {"0.10.0", "0.9.0", "0.7.0", "0.6.0", "0.5.2", "0.5.1"},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func vsCodeInitializeOffering(t *testing.T, versions []string) []byte {
|
|
37
|
+
t.Helper()
|
|
38
|
+
req, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{
|
|
39
|
+
"channel": wire.RootChannel,
|
|
40
|
+
"protocolVersions": []string{"0.10.0", "0.9.0", "0.7.0", "0.6.0", "0.5.2", "0.5.1"},
|
|
41
|
+
"clientId": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
|
|
42
|
+
"clientInfo": map[string]any{"name": "vscode-agents-window", "title": "VS Code Agents Window"},
|
|
43
|
+
"_meta": map[string]any{"vscode.telemetryLevel": "all", "vscode.clientConnectionKind": "direct_websocket"},
|
|
44
|
+
"initialSubscriptions": []string{wire.RootChannel},
|
|
45
|
+
}})
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
return req
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// vsCodeRootConfig is one of the notifications VS Code sends right after initialize (_forwardClientConfig, which dispatches
|
|
53
|
+
// root/configChanged with client sequence 0 for each forwarded setting: _dispatchRootConfig).
|
|
54
|
+
func vsCodeRootConfig(t *testing.T, config map[string]any) []byte {
|
|
55
|
+
t.Helper()
|
|
56
|
+
msg, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "method": "dispatchAction", "params": map[string]any{
|
|
57
|
+
"channel": wire.RootChannel, "clientSeq": 0, "action": map[string]any{"type": "root/configChanged", "config": config},
|
|
58
|
+
}})
|
|
59
|
+
if err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
return msg
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// rawAt upgrades by hand at the request target VS Code's address produces, so a test can see opcodes and the status line.
|
|
66
|
+
func rawAt(t *testing.T, s *ws.Server, target string) (net.Conn, *bufio.Reader, *http.Response) {
|
|
67
|
+
t.Helper()
|
|
68
|
+
conn, err := net.Dial("tcp", "127.0.0.1:"+itoa(s.Port()))
|
|
69
|
+
if err != nil {
|
|
70
|
+
t.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
t.Cleanup(func() { conn.Close() })
|
|
73
|
+
io.WriteString(conn, "GET "+target+" HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n")
|
|
74
|
+
br := bufio.NewReader(conn)
|
|
75
|
+
resp, err := http.ReadResponse(br, nil)
|
|
76
|
+
if err != nil {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
return conn, br, resp
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestVSCodeHandshakeNegotiatesTheSupportedVersionOverTextFrames(t *testing.T) {
|
|
83
|
+
_, s := serve(t, ws.Options{Token: "s3cret"})
|
|
84
|
+
conn, br, resp := rawAt(t, s, "/?tkn=s3cret")
|
|
85
|
+
if resp.StatusCode != http.StatusSwitchingProtocols {
|
|
86
|
+
t.Fatalf("upgrade with the right tkn = %d", resp.StatusCode)
|
|
87
|
+
}
|
|
88
|
+
if _, err := conn.Write(maskedFrame(true, 1, vsCodeInitialize(t))); err != nil {
|
|
89
|
+
t.Fatal(err)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
93
|
+
op, payload := readFrame(t, br)
|
|
94
|
+
if op != 1 {
|
|
95
|
+
t.Fatalf("the reply was opcode %d; VS Code drops every non-text frame and closes the connection (4002) after ten (browser/webSocketClientTransport.ts)", op)
|
|
96
|
+
}
|
|
97
|
+
var reply struct {
|
|
98
|
+
Result struct {
|
|
99
|
+
ProtocolVersion string `json:"protocolVersion"`
|
|
100
|
+
Snapshots []struct {
|
|
101
|
+
Resource string `json:"resource"`
|
|
102
|
+
State struct {
|
|
103
|
+
Agents []struct {
|
|
104
|
+
Provider string `json:"provider"`
|
|
105
|
+
} `json:"agents"`
|
|
106
|
+
} `json:"state"`
|
|
107
|
+
} `json:"snapshots"`
|
|
108
|
+
} `json:"result"`
|
|
109
|
+
}
|
|
110
|
+
if err := json.Unmarshal(payload, &reply); err != nil {
|
|
111
|
+
t.Fatalf("%s: %v", payload, err)
|
|
112
|
+
}
|
|
113
|
+
if reply.Result.ProtocolVersion != "0.9.0" {
|
|
114
|
+
t.Fatalf("VS Code's offer (0.10.0, 0.9.0, 0.7.0, 0.6.0, 0.5.2, 0.5.1) negotiated %q, want the one version this host speaks, 0.9.0", reply.Result.ProtocolVersion)
|
|
115
|
+
}
|
|
116
|
+
if len(reply.Result.Snapshots) != 1 || reply.Result.Snapshots[0].Resource != wire.RootChannel {
|
|
117
|
+
t.Fatalf("the initial root subscription returned %+v", reply.Result.Snapshots)
|
|
118
|
+
}
|
|
119
|
+
for _, agent := range reply.Result.Snapshots[0].State.Agents {
|
|
120
|
+
if agent.Provider == "" || strings.Contains(agent.Provider, "-") {
|
|
121
|
+
t.Errorf("agent provider id %q must be non-empty and dash-free", agent.Provider)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Then, as VS Code does, forward settings as root/configChanged notifications and keep using the connection (VS Code's
|
|
126
|
+
// keep-alive ping is {"channel":"ahp-root://"}): the host must neither drop it nor answer with anything but text frames.
|
|
127
|
+
for _, config := range []map[string]any{{"telemetryLevel": "all"}, {"terminalAutoApproveEnabled": true}} {
|
|
128
|
+
if _, err := conn.Write(maskedFrame(true, 1, vsCodeRootConfig(t, config))); err != nil {
|
|
129
|
+
t.Fatal(err)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if _, err := conn.Write(maskedFrame(true, 1, []byte(`{"jsonrpc":"2.0","id":2,"method":"ping","params":{"channel":"ahp-root://"}}`))); err != nil {
|
|
133
|
+
t.Fatal(err)
|
|
134
|
+
}
|
|
135
|
+
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
136
|
+
for {
|
|
137
|
+
op, payload := readFrame(t, br)
|
|
138
|
+
if op != 1 {
|
|
139
|
+
t.Fatalf("a later frame was opcode %d, want text", op)
|
|
140
|
+
}
|
|
141
|
+
var msg struct {
|
|
142
|
+
ID *int `json:"id"`
|
|
143
|
+
Error json.RawMessage `json:"error"`
|
|
144
|
+
}
|
|
145
|
+
if err := json.Unmarshal(payload, &msg); err != nil {
|
|
146
|
+
t.Fatalf("%s: %v", payload, err)
|
|
147
|
+
}
|
|
148
|
+
if msg.ID != nil && *msg.ID == 2 {
|
|
149
|
+
if msg.Error != nil {
|
|
150
|
+
t.Fatalf("ping after the config notifications failed: %s", payload)
|
|
151
|
+
}
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func TestVSCodeAddressFormIsAccepted(t *testing.T) {
|
|
158
|
+
_, s := serve(t, ws.Options{Token: "s3cret"})
|
|
159
|
+
for release, versions := range vsCodeOffers {
|
|
160
|
+
// The address VS Code is given has no slash before the query.
|
|
161
|
+
c, _, err := ws.Dial("ws://127.0.0.1:"+itoa(s.Port())+"?tkn=s3cret", nil)
|
|
162
|
+
if err != nil {
|
|
163
|
+
t.Fatal(err)
|
|
164
|
+
}
|
|
165
|
+
if err := c.WriteText(vsCodeInitializeOffering(t, versions)); err != nil {
|
|
166
|
+
t.Fatal(err)
|
|
167
|
+
}
|
|
168
|
+
msg, err := c.ReadMessage(2 * time.Second)
|
|
169
|
+
c.Close()
|
|
170
|
+
if err != nil || !strings.Contains(string(msg), `"protocolVersion":"0.9.0"`) {
|
|
171
|
+
t.Fatalf("VS Code %s: reply %s (%v)", release, msg, err)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func TestVSCodeWrongOrMissingTknIsRefusedAtTheUpgrade(t *testing.T) {
|
|
177
|
+
_, s := serve(t, ws.Options{Token: "s3cret"})
|
|
178
|
+
for _, target := range []string{"/?tkn=wrong", "/?tkn=", "/", "/?TKN=s3cret"} {
|
|
179
|
+
_, _, resp := rawAt(t, s, target)
|
|
180
|
+
// 403 Forbidden with no upgrade, as VS Code's own agent host answers a missing or wrong tkn
|
|
181
|
+
// (microsoft/vscode src/vs/platform/agentHost/node/webSocketTransport.ts, verifyClient: cb(false, 403, 'Forbidden')).
|
|
182
|
+
// The AHP spec leaves endpoint access to the transport (docs/specification/transport.md, "Authentication"), and
|
|
183
|
+
// VS Code's client cannot see the status: a browser WebSocket reports any refused upgrade as one error.
|
|
184
|
+
if resp.StatusCode != http.StatusForbidden {
|
|
185
|
+
t.Errorf("GET %s = %d, want 403 and no upgrade", target, resp.StatusCode)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func TestProviderIDIsDashFree(t *testing.T) {
|
|
191
|
+
// AHP session URIs are <provider>:/<id>; VS Code parses the scheme, so a dash in the provider breaks them.
|
|
192
|
+
if pi.Provider == "" || strings.Contains(pi.Provider, "-") {
|
|
193
|
+
t.Fatalf("provider id %q must be non-empty and dash-free", pi.Provider)
|
|
194
|
+
}
|
|
195
|
+
if got := pi.BuildAgentInfo(nil).Provider; got != pi.Provider {
|
|
196
|
+
t.Fatalf("the agent advertises provider %q, not %q", got, pi.Provider)
|
|
197
|
+
}
|
|
198
|
+
}
|