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