@omercnet/paseo-shared-browser 0.4.2 → 1.0.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/README.md +15 -32
- package/client/browser.tsx +114 -32
- package/index.client.tsx +1 -1
- package/index.server.ts +1 -1
- package/package.json +12 -14
- package/paseo-plugin.json +3 -7
- package/scripts/prepare-dependencies.mjs +46 -9
- package/scripts/prepare-runtime.mjs +18 -6
- package/server/agent-browser-runtime.ts +23 -30
- package/server/browser-policy.ts +10 -10
- package/server/browser.ts +5 -5
- package/server/cdp.ts +13 -3
- package/server/mcp-entry.ts +57 -15
- package/server/runtime-owner.ts +9 -2
- package/server/runtime-protocol.ts +33 -6
- package/server/supervisor-client.ts +4 -4
- package/server/supervisor.ts +4 -4
- package/shared/browser.ts +4 -1
package/README.md
CHANGED
|
@@ -72,24 +72,10 @@ paseo plugin install npm:@omercnet/paseo-shared-browser
|
|
|
72
72
|
paseo plugin ls
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
paseo plugin add omercnet/paseo-plugins:paseo-shared-browser
|
|
79
|
-
paseo plugin ls
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Installation runs `npm ci --include=dev` and `npm run prepare:runtime` on the daemon host. The
|
|
83
|
-
preparation step installs the pinned browser runtime and builds both `supervisor.cjs` and
|
|
75
|
+
When Paseo acquires the npm package, it runs the `npm run prepare:runtime` build hook automatically. The
|
|
76
|
+
hook installs the pinned browser runtime and builds both `supervisor.cjs` and
|
|
84
77
|
`shared-browser-mcp.cjs` under
|
|
85
|
-
`$PASEO_HOME/plugin-data/shared-browser/runtime`.
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
cd paseo-plugins/paseo-shared-browser
|
|
89
|
-
npm ci --include=dev
|
|
90
|
-
npm run prepare:runtime
|
|
91
|
-
paseo plugin install "$PWD"
|
|
92
|
-
```
|
|
78
|
+
`$PASEO_HOME/plugin-data/shared-browser/runtime`.
|
|
93
79
|
|
|
94
80
|
The build requires Node.js 24 or newer and npm on the daemon host. On Linux x64, macOS, and
|
|
95
81
|
Windows, runtime preparation installs and stages the platform's Chrome for Testing distribution. On
|
|
@@ -162,9 +148,9 @@ User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
|
|
|
162
148
|
isolation boundary between local OS users.
|
|
163
149
|
- `agent-browser` IPC metadata and workspace profile directories are owner-only on POSIX systems.
|
|
164
150
|
The plugin rejects a non-loopback CDP endpoint.
|
|
165
|
-
- Viewer and control tokens coordinate clients already paired to the same Paseo daemon.
|
|
166
|
-
|
|
167
|
-
|
|
151
|
+
- Viewer and control tokens coordinate clients already paired to the same Paseo daemon. Plugin RPC
|
|
152
|
+
callbacks expose no authenticated caller identity, so these human-viewer tokens are a workflow
|
|
153
|
+
safeguard, not an authorization boundary. The stdio MCP adapter separately uses an
|
|
168
154
|
opaque, workspace-bound credential.
|
|
169
155
|
- Downloads, uploads, clipboard synchronization, media permissions, extensions, native passkeys,
|
|
170
156
|
and platform authenticators are not exposed by this plugin.
|
|
@@ -175,23 +161,20 @@ User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
|
|
|
175
161
|
## Develop
|
|
176
162
|
|
|
177
163
|
```bash
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
paseo plugin install "$PWD"
|
|
185
|
-
paseo plugin reload shared-browser
|
|
164
|
+
bun install
|
|
165
|
+
bun run typecheck
|
|
166
|
+
bun run lint
|
|
167
|
+
bun run format:check
|
|
168
|
+
bun run test:unit
|
|
169
|
+
bun run prepare:runtime
|
|
186
170
|
```
|
|
187
171
|
|
|
188
|
-
`
|
|
172
|
+
`bun run test:smoke` launches the configured real Chromium runtime and exercises two viewers,
|
|
189
173
|
control handoff, reconnect, stale-frame rejection, viewport changes, device emulation, profile
|
|
190
174
|
persistence, and archive teardown.
|
|
191
175
|
|
|
192
176
|
Release Please maintains the version, changelog, component tag, and GitHub release from
|
|
193
177
|
Conventional Commits in the monorepo.
|
|
194
178
|
|
|
195
|
-
Both the Paseo daemon and app must satisfy `^0.
|
|
196
|
-
and
|
|
197
|
-
web, iOS, and Android Paseo clients.
|
|
179
|
+
Both the Paseo daemon and app must satisfy `^0.9.0`. The client surface uses React Native primitives
|
|
180
|
+
and works in desktop, web, iOS, and Android Paseo clients.
|
package/client/browser.tsx
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
2
1
|
import {
|
|
3
2
|
type PluginClientContext,
|
|
4
3
|
type PluginWorkspacePanelProps,
|
|
5
4
|
useRpc,
|
|
6
5
|
} from "@getpaseo/plugin/client";
|
|
7
6
|
import { Icon, Modal, ScrollView, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
8
|
-
import {
|
|
7
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
8
|
+
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import {
|
|
10
10
|
ActivityIndicator,
|
|
11
|
+
type GestureResponderEvent,
|
|
11
12
|
Image,
|
|
13
|
+
type LayoutChangeEvent,
|
|
12
14
|
PanResponder,
|
|
13
15
|
Pressable,
|
|
16
|
+
type StyleProp,
|
|
14
17
|
StyleSheet,
|
|
15
18
|
Text,
|
|
16
|
-
View,
|
|
17
|
-
type GestureResponderEvent,
|
|
18
|
-
type LayoutChangeEvent,
|
|
19
|
-
type StyleProp,
|
|
20
19
|
type TextInputProps,
|
|
21
20
|
type TextStyle,
|
|
21
|
+
View,
|
|
22
22
|
type ViewStyle,
|
|
23
23
|
} from "react-native";
|
|
24
24
|
import {
|
|
25
|
-
DEVICE_PRESETS,
|
|
26
|
-
MAX_VIEWPORT,
|
|
27
|
-
MIN_VIEWPORT,
|
|
28
25
|
acquireControlRpc,
|
|
29
26
|
applyDevicePresetRpc,
|
|
30
27
|
attachBrowserRpc,
|
|
28
|
+
type BrowserFrame,
|
|
29
|
+
type BrowserInputEvent,
|
|
30
|
+
type BrowserState,
|
|
31
31
|
captureBrowserRpc,
|
|
32
|
+
DEVICE_PRESETS,
|
|
33
|
+
type DevicePresetId,
|
|
32
34
|
detachBrowserRpc,
|
|
35
|
+
didBrowserRuntimeRestart,
|
|
36
|
+
isBrowserStateCurrent,
|
|
33
37
|
listOpenBrowserWorkspacesRpc,
|
|
38
|
+
MAX_VIEWPORT,
|
|
39
|
+
MIN_VIEWPORT,
|
|
34
40
|
navigateBrowserRpc,
|
|
35
41
|
releaseControlRpc,
|
|
36
42
|
resizeBrowserRpc,
|
|
37
43
|
sendBrowserInputRpc,
|
|
38
|
-
didBrowserRuntimeRestart,
|
|
39
|
-
isBrowserStateCurrent,
|
|
40
|
-
type BrowserFrame,
|
|
41
|
-
type BrowserInputEvent,
|
|
42
|
-
type BrowserState,
|
|
43
|
-
type DevicePresetId,
|
|
44
44
|
} from "../shared/browser";
|
|
45
45
|
|
|
46
46
|
const SPACE = {
|
|
@@ -72,6 +72,7 @@ const CAPTURE_INTERVAL_READY = 250;
|
|
|
72
72
|
const CAPTURE_INTERVAL_WAITING = 1_500;
|
|
73
73
|
const MAX_VIEWER_LABEL_LENGTH = 64;
|
|
74
74
|
const PILL_PRESENCE_POLL_MS = 2_000;
|
|
75
|
+
const AGENT_DIRECTORY_PAGE_LIMIT = 200;
|
|
75
76
|
const MAX_URL_LENGTH = 8_192;
|
|
76
77
|
const MAX_TEXT_LENGTH = 4_000;
|
|
77
78
|
const BYTES_PER_KIBIBYTE = 1_024;
|
|
@@ -803,18 +804,33 @@ function CanvasPlaceholder({
|
|
|
803
804
|
);
|
|
804
805
|
}
|
|
805
806
|
|
|
807
|
+
interface AgentDirectoryPage {
|
|
808
|
+
entries: Array<{ agent: { id: string; workspaceId?: string | undefined } }>;
|
|
809
|
+
pageInfo: { hasMore: boolean; nextCursor: string | null };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
type AgentDirectoryUpdate =
|
|
813
|
+
| { kind: "remove"; agentId: string }
|
|
814
|
+
| { kind: "upsert"; agent: { id: string; workspaceId?: string | undefined } };
|
|
815
|
+
type AgentPlacement = { id: string; workspaceId: string };
|
|
816
|
+
|
|
806
817
|
export function contributeSharedBrowserClient(client: PluginClientContext) {
|
|
807
|
-
const agents = new Map<string,
|
|
818
|
+
const agents = new Map<string, AgentPlacement>();
|
|
808
819
|
const pills = new Map<string, { workspaceId: string; remove: () => void }>();
|
|
820
|
+
const lifetime = new AbortController();
|
|
809
821
|
let openWorkspaceIds = new Set<string>();
|
|
810
822
|
let refreshing = false;
|
|
811
823
|
let stopped = false;
|
|
824
|
+
let directoryGeneration = 0;
|
|
825
|
+
let pendingDirectory: { generation: number; updates: AgentDirectoryUpdate[] } | null = null;
|
|
826
|
+
let unsubscribeDirectory: (() => void) | null = null;
|
|
827
|
+
let releaseDirectory: (() => Promise<void>) | null = null;
|
|
812
828
|
|
|
813
829
|
const removePill = (agentId: string) => {
|
|
814
830
|
pills.get(agentId)?.remove();
|
|
815
831
|
pills.delete(agentId);
|
|
816
832
|
};
|
|
817
|
-
const syncPill = (agent:
|
|
833
|
+
const syncPill = (agent: AgentPlacement) => {
|
|
818
834
|
const current = pills.get(agent.id);
|
|
819
835
|
if (!openWorkspaceIds.has(agent.workspaceId)) {
|
|
820
836
|
removePill(agent.id);
|
|
@@ -858,32 +874,94 @@ export function contributeSharedBrowserClient(client: PluginClientContext) {
|
|
|
858
874
|
refreshing = false;
|
|
859
875
|
}
|
|
860
876
|
};
|
|
861
|
-
|
|
862
|
-
|
|
877
|
+
const applyUpdate = (target: Map<string, AgentPlacement>, update: AgentDirectoryUpdate) => {
|
|
878
|
+
if (update.kind === "remove") {
|
|
879
|
+
target.delete(update.agentId);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const { id, workspaceId } = update.agent;
|
|
883
|
+
if (workspaceId) target.set(id, { id, workspaceId });
|
|
884
|
+
else target.delete(id);
|
|
885
|
+
};
|
|
886
|
+
const applyLiveUpdate = (update: AgentDirectoryUpdate) => {
|
|
887
|
+
if (stopped) return;
|
|
888
|
+
pendingDirectory?.updates.push(update);
|
|
889
|
+
applyUpdate(agents, update);
|
|
863
890
|
if (update.kind === "remove") {
|
|
864
|
-
agents.delete(update.agentId);
|
|
865
891
|
removePill(update.agentId);
|
|
866
892
|
return;
|
|
867
893
|
}
|
|
868
894
|
const { id, workspaceId } = update.agent;
|
|
869
895
|
if (!workspaceId) {
|
|
870
|
-
agents.delete(id);
|
|
871
896
|
removePill(id);
|
|
872
897
|
return;
|
|
873
898
|
}
|
|
874
|
-
|
|
875
|
-
agents.set(id, agent);
|
|
876
|
-
syncPill(agent);
|
|
899
|
+
syncPill({ id, workspaceId });
|
|
877
900
|
void refreshPresence();
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
901
|
+
};
|
|
902
|
+
const replaceAgents = (next: Map<string, AgentPlacement>) => {
|
|
903
|
+
for (const agentId of agents.keys()) {
|
|
904
|
+
if (!next.has(agentId)) removePill(agentId);
|
|
905
|
+
}
|
|
906
|
+
agents.clear();
|
|
907
|
+
for (const [agentId, agent] of next) agents.set(agentId, agent);
|
|
908
|
+
syncAllPills();
|
|
909
|
+
};
|
|
910
|
+
const followSnapshot = async (snapshot: AgentDirectoryPage) => {
|
|
911
|
+
if (stopped) return;
|
|
912
|
+
const generation = ++directoryGeneration;
|
|
913
|
+
const transaction = { generation, updates: [] as AgentDirectoryUpdate[] };
|
|
914
|
+
pendingDirectory = transaction;
|
|
915
|
+
const next = new Map<string, AgentPlacement>();
|
|
916
|
+
for (const { agent } of snapshot.entries) {
|
|
917
|
+
if (agent.workspaceId) next.set(agent.id, { id: agent.id, workspaceId: agent.workspaceId });
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
let cursor = snapshot.pageInfo.hasMore ? snapshot.pageInfo.nextCursor : null;
|
|
922
|
+
while (cursor) {
|
|
923
|
+
const page = await client.paseo.agents.list({
|
|
924
|
+
scope: "active",
|
|
925
|
+
page: { limit: AGENT_DIRECTORY_PAGE_LIMIT, cursor },
|
|
926
|
+
signal: lifetime.signal,
|
|
927
|
+
});
|
|
928
|
+
if (stopped || pendingDirectory?.generation !== generation) return;
|
|
929
|
+
for (const { agent } of page.entries) {
|
|
930
|
+
if (agent.workspaceId)
|
|
931
|
+
next.set(agent.id, { id: agent.id, workspaceId: agent.workspaceId });
|
|
932
|
+
}
|
|
933
|
+
cursor = page.pageInfo.hasMore ? page.pageInfo.nextCursor : null;
|
|
885
934
|
}
|
|
935
|
+
if (stopped || pendingDirectory?.generation !== generation) return;
|
|
936
|
+
for (const update of transaction.updates) applyUpdate(next, update);
|
|
937
|
+
pendingDirectory = null;
|
|
938
|
+
replaceAgents(next);
|
|
886
939
|
await refreshPresence();
|
|
940
|
+
} catch {
|
|
941
|
+
if (!stopped && pendingDirectory?.generation === generation) pendingDirectory = null;
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
void client.paseo.agents
|
|
946
|
+
.list({
|
|
947
|
+
scope: "active",
|
|
948
|
+
page: { limit: AGENT_DIRECTORY_PAGE_LIMIT },
|
|
949
|
+
subscribe: {},
|
|
950
|
+
signal: lifetime.signal,
|
|
951
|
+
})
|
|
952
|
+
.then(({ subscription }) => {
|
|
953
|
+
if (stopped) {
|
|
954
|
+
void subscription.release().catch(() => undefined);
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
releaseDirectory = subscription.release;
|
|
958
|
+
unsubscribeDirectory = subscription.subscribe({
|
|
959
|
+
snapshot: (snapshot) => void followSnapshot(snapshot),
|
|
960
|
+
update: (message) => {
|
|
961
|
+
if (message.type === "agent_update") applyLiveUpdate(message.payload);
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
return undefined;
|
|
887
965
|
})
|
|
888
966
|
.catch(() => undefined);
|
|
889
967
|
const presenceTimer = setInterval(() => void refreshPresence(), PILL_PRESENCE_POLL_MS);
|
|
@@ -891,8 +969,12 @@ export function contributeSharedBrowserClient(client: PluginClientContext) {
|
|
|
891
969
|
return () => {
|
|
892
970
|
if (stopped) return;
|
|
893
971
|
stopped = true;
|
|
972
|
+
directoryGeneration += 1;
|
|
973
|
+
pendingDirectory = null;
|
|
894
974
|
clearInterval(presenceTimer);
|
|
895
|
-
|
|
975
|
+
unsubscribeDirectory?.();
|
|
976
|
+
void releaseDirectory?.().catch(() => undefined);
|
|
977
|
+
lifetime.abort();
|
|
896
978
|
for (const { remove } of pills.values()) remove();
|
|
897
979
|
pills.clear();
|
|
898
980
|
agents.clear();
|
package/index.client.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
-
import {
|
|
2
|
+
import { contributeSharedBrowserClient, SharedBrowserPanel } from "./client/browser";
|
|
3
3
|
|
|
4
4
|
export default function contribute(client: PluginClientContext) {
|
|
5
5
|
client.addWorkspacePanel({
|
package/index.server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
1
|
import { randomBytes } from "node:crypto";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
5
5
|
import {
|
|
6
6
|
bindAgentTicket,
|
|
7
7
|
cleanupBrowserServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-shared-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -38,37 +38,35 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
+
"check": "biome check .",
|
|
42
|
+
"check:write": "biome check --write .",
|
|
41
43
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"lint": "oxlint --deny-warnings .",
|
|
43
|
-
"format": "oxfmt --ignore-path .formatignore .",
|
|
44
|
-
"format:check": "oxfmt --check --ignore-path .formatignore .",
|
|
45
44
|
"prepare:runtime": "node scripts/prepare-runtime.mjs",
|
|
46
|
-
"test
|
|
45
|
+
"test": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts",
|
|
46
|
+
"test:unit": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts",
|
|
47
47
|
"test:smoke": "vitest run tests/browser.smoke.test.ts --testTimeout=30000"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@getpaseo/plugin": "0.
|
|
50
|
+
"@getpaseo/plugin": "0.9.0",
|
|
51
51
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
52
52
|
"agent-browser": "0.37.1",
|
|
53
53
|
"esbuild": "0.28.2",
|
|
54
54
|
"zod": "4.4.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@
|
|
58
|
-
"@tanstack/react-query": "^5.
|
|
59
|
-
"@types/node": "^
|
|
60
|
-
"@types/react": "
|
|
61
|
-
"oxfmt": "0.66.0",
|
|
62
|
-
"oxlint": "1.81.0",
|
|
57
|
+
"@biomejs/biome": "^2.5.10",
|
|
58
|
+
"@tanstack/react-query": "^5.102.3",
|
|
59
|
+
"@types/node": "^24.10.1",
|
|
60
|
+
"@types/react": "^19.2.18",
|
|
63
61
|
"react": "19.1.0",
|
|
64
62
|
"react-native": "0.81.5",
|
|
65
|
-
"typescript": "^7.0.
|
|
63
|
+
"typescript": "^7.0.2",
|
|
66
64
|
"vitest": "^5.0.0"
|
|
67
65
|
},
|
|
68
66
|
"engines": {
|
|
69
67
|
"node": ">=24"
|
|
70
68
|
},
|
|
71
|
-
"packageManager": "
|
|
69
|
+
"packageManager": "bun@1.4.2",
|
|
72
70
|
"allowScripts": {
|
|
73
71
|
"agent-browser": true,
|
|
74
72
|
"esbuild@0.28.2": true
|
package/paseo-plugin.json
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shared-browser",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"build": [
|
|
7
|
-
["node", "scripts/prepare-dependencies.mjs"],
|
|
8
|
-
["npm", "run", "prepare:runtime"]
|
|
9
|
-
]
|
|
3
|
+
"description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
|
|
4
|
+
"requirements": { "paseo": "^0.9.0" },
|
|
5
|
+
"build": [["node", "scripts/prepare-dependencies.mjs"], ["npm", "run", "prepare:runtime"]]
|
|
10
6
|
}
|
|
@@ -1,21 +1,58 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
8
|
|
|
9
|
+
function loadRootCatalog(root) {
|
|
10
|
+
for (let directory = root; ; directory = dirname(directory)) {
|
|
11
|
+
const manifestPath = join(directory, "package.json");
|
|
12
|
+
if (existsSync(manifestPath)) {
|
|
13
|
+
const catalog = JSON.parse(readFileSync(manifestPath, "utf8")).catalog;
|
|
14
|
+
if (catalog) return catalog;
|
|
15
|
+
}
|
|
16
|
+
if (directory === dirname(directory)) return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stageManifest(root, catalog, install) {
|
|
21
|
+
const manifestPath = join(root, "package.json");
|
|
22
|
+
const source = readFileSync(manifestPath, "utf8");
|
|
23
|
+
const manifest = JSON.parse(source);
|
|
24
|
+
|
|
25
|
+
for (const [name, spec] of Object.entries(manifest.dependencies)) {
|
|
26
|
+
if (spec !== "catalog:") continue;
|
|
27
|
+
if (typeof catalog[name] !== "string") throw new Error(`Missing catalog entry for ${name}`);
|
|
28
|
+
manifest.dependencies[name] = catalog[name];
|
|
29
|
+
}
|
|
30
|
+
delete manifest.devDependencies;
|
|
31
|
+
|
|
32
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
33
|
+
try {
|
|
34
|
+
install();
|
|
35
|
+
} finally {
|
|
36
|
+
writeFileSync(manifestPath, source);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
9
40
|
export function resolveDependencyRoot(packageName, from = import.meta.url) {
|
|
10
|
-
|
|
11
|
-
return dirname(require.resolve(`${packageName}/package.json`));
|
|
41
|
+
return dirname(createRequire(from).resolve(`${packageName}/package.json`));
|
|
12
42
|
}
|
|
13
|
-
export function prepareDependencies(root = projectRoot, execute = execFileSync) {
|
|
14
|
-
if (!existsSync(join(root, "package-lock.json"))) return false;
|
|
15
43
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
export function prepareDependencies(root = projectRoot, execute = execFileSync) {
|
|
45
|
+
const catalog = loadRootCatalog(root);
|
|
46
|
+
if (!catalog) return false;
|
|
47
|
+
stageManifest(root, catalog, () => {
|
|
48
|
+
execute(
|
|
49
|
+
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
50
|
+
["install", "--omit=dev", "--ignore-scripts", "--no-package-lock", "--workspaces=false"],
|
|
51
|
+
{
|
|
52
|
+
cwd: root,
|
|
53
|
+
stdio: "inherit",
|
|
54
|
+
},
|
|
55
|
+
);
|
|
19
56
|
});
|
|
20
57
|
return true;
|
|
21
58
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { resolveDependencyRoot } from "./prepare-dependencies.mjs";
|
|
2
|
-
import { build } from "esbuild";
|
|
3
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
4
3
|
import {
|
|
5
4
|
access,
|
|
6
5
|
chmod,
|
|
@@ -13,11 +12,19 @@ import {
|
|
|
13
12
|
symlink,
|
|
14
13
|
writeFile,
|
|
15
14
|
} from "node:fs/promises";
|
|
16
|
-
import {
|
|
15
|
+
import { createRequire } from "node:module";
|
|
17
16
|
import { arch, homedir, platform, userInfo } from "node:os";
|
|
18
17
|
import { dirname, join, resolve } from "node:path";
|
|
19
|
-
import { promisify } from "node:util";
|
|
20
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { promisify } from "node:util";
|
|
20
|
+
import { build } from "esbuild";
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
function resolveDependencyRoot(packageName) {
|
|
25
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
26
|
+
return dirname(packageJsonPath);
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
const execFileAsync = promisify(execFile);
|
|
23
30
|
const expectedVersion = "0.37.1";
|
|
@@ -54,7 +61,9 @@ async function requireExecutable(path, label) {
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
async function validateAgentBrowser(path) {
|
|
57
|
-
const { stdout } = await execFileAsync(path, ["--version"], {
|
|
64
|
+
const { stdout } = await execFileAsync(path, ["--version"], {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
});
|
|
58
67
|
const version = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/)?.[0];
|
|
59
68
|
if (version !== expectedVersion) {
|
|
60
69
|
throw new Error(`Expected agent-browser ${expectedVersion}, received ${version ?? "unknown"}`);
|
|
@@ -185,7 +194,10 @@ async function installChromium() {
|
|
|
185
194
|
if (!releaseDir) throw new Error("agent-browser install did not produce a Chromium release");
|
|
186
195
|
const chrome = installedChromiumExecutable(releaseDir);
|
|
187
196
|
await requireExecutable(chrome, "Installed Chromium executable");
|
|
188
|
-
await cp(releaseDir, join(stagingRoot, "chromium"), {
|
|
197
|
+
await cp(releaseDir, join(stagingRoot, "chromium"), {
|
|
198
|
+
recursive: true,
|
|
199
|
+
force: true,
|
|
200
|
+
});
|
|
189
201
|
if (hostPlatform === "darwin") {
|
|
190
202
|
// chrome can't be a symlink to the .app's real binary here: macOS dyld
|
|
191
203
|
// resolves @executable_path from the invoked path's own directory, so a
|
|
@@ -3,14 +3,14 @@ import { chmod, mkdir, readFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import {
|
|
6
|
+
attachToTarget,
|
|
6
7
|
CdpConnection,
|
|
7
|
-
|
|
8
|
+
type CdpEvent,
|
|
9
|
+
type CdpSession,
|
|
10
|
+
type CdpTarget,
|
|
8
11
|
CdpUnavailableError,
|
|
9
12
|
CdpUnknownOutcomeError,
|
|
10
|
-
attachToTarget,
|
|
11
13
|
listPageTargets,
|
|
12
|
-
type CdpEvent,
|
|
13
|
-
type CdpTarget,
|
|
14
14
|
} from "./cdp";
|
|
15
15
|
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
@@ -93,6 +93,7 @@ export interface AgentBrowserRuntimeOptions {
|
|
|
93
93
|
ipcDirectory: string;
|
|
94
94
|
session: string;
|
|
95
95
|
initialUrl?: string;
|
|
96
|
+
headed?: boolean;
|
|
96
97
|
timeoutMs?: number;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -164,9 +165,7 @@ function parseJsonOutput(stdout: string): unknown {
|
|
|
164
165
|
for (const line of lines) {
|
|
165
166
|
try {
|
|
166
167
|
return JSON.parse(line);
|
|
167
|
-
} catch {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
168
|
+
} catch {}
|
|
170
169
|
}
|
|
171
170
|
throw new AgentBrowserIncompatibleError("agent-browser returned invalid JSON");
|
|
172
171
|
}
|
|
@@ -191,6 +190,7 @@ export class AgentBrowserRuntime {
|
|
|
191
190
|
readonly ipcDirectory: string;
|
|
192
191
|
readonly session: string;
|
|
193
192
|
private readonly initialUrl: string;
|
|
193
|
+
private readonly headed: boolean;
|
|
194
194
|
private readonly timeoutMs: number;
|
|
195
195
|
private readonly environment: NodeJS.ProcessEnv;
|
|
196
196
|
private connection: CdpConnection | null = null;
|
|
@@ -214,6 +214,7 @@ export class AgentBrowserRuntime {
|
|
|
214
214
|
throw new AgentBrowserIncompatibleError("agent-browser session name is invalid");
|
|
215
215
|
}
|
|
216
216
|
this.session = options.session;
|
|
217
|
+
this.headed = options.headed ?? false;
|
|
217
218
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
219
|
this.environment = runtimeEnvironment(this.ipcDirectory);
|
|
219
220
|
this.initialUrl = options.initialUrl ?? "about:blank";
|
|
@@ -238,6 +239,7 @@ export class AgentBrowserRuntime {
|
|
|
238
239
|
this.profilePath,
|
|
239
240
|
"--executable-path",
|
|
240
241
|
this.executablePath,
|
|
242
|
+
...(this.headed ? ["--headed"] : []),
|
|
241
243
|
...(chromiumArguments ? ["--args", chromiumArguments] : []),
|
|
242
244
|
"--json",
|
|
243
245
|
"open",
|
|
@@ -508,9 +510,7 @@ export class AgentBrowserRuntime {
|
|
|
508
510
|
|
|
509
511
|
async mouseMove(x: number, y: number): Promise<void> {
|
|
510
512
|
this.assertPoint(x, y);
|
|
511
|
-
await (
|
|
512
|
-
await this.requirePage()
|
|
513
|
-
).send(
|
|
513
|
+
await (await this.requirePage()).send(
|
|
514
514
|
"Input.dispatchMouseEvent",
|
|
515
515
|
{
|
|
516
516
|
type: "mouseMoved",
|
|
@@ -530,9 +530,7 @@ export class AgentBrowserRuntime {
|
|
|
530
530
|
clickCount = 1,
|
|
531
531
|
): Promise<void> {
|
|
532
532
|
this.assertPoint(x, y);
|
|
533
|
-
await (
|
|
534
|
-
await this.requirePage()
|
|
535
|
-
).send(
|
|
533
|
+
await (await this.requirePage()).send(
|
|
536
534
|
"Input.dispatchMouseEvent",
|
|
537
535
|
{
|
|
538
536
|
type: "mousePressed",
|
|
@@ -549,9 +547,7 @@ export class AgentBrowserRuntime {
|
|
|
549
547
|
|
|
550
548
|
async mouseUp(x: number, y: number, button: MouseButton = "left", clickCount = 1): Promise<void> {
|
|
551
549
|
this.assertPoint(x, y);
|
|
552
|
-
await (
|
|
553
|
-
await this.requirePage()
|
|
554
|
-
).send(
|
|
550
|
+
await (await this.requirePage()).send(
|
|
555
551
|
"Input.dispatchMouseEvent",
|
|
556
552
|
{
|
|
557
553
|
type: "mouseReleased",
|
|
@@ -568,9 +564,7 @@ export class AgentBrowserRuntime {
|
|
|
568
564
|
|
|
569
565
|
async wheel(x: number, y: number, deltaX: number, deltaY: number): Promise<void> {
|
|
570
566
|
this.assertPoint(x, y);
|
|
571
|
-
await (
|
|
572
|
-
await this.requirePage()
|
|
573
|
-
).send(
|
|
567
|
+
await (await this.requirePage()).send(
|
|
574
568
|
"Input.dispatchMouseEvent",
|
|
575
569
|
{
|
|
576
570
|
type: "mouseWheel",
|
|
@@ -590,9 +584,7 @@ export class AgentBrowserRuntime {
|
|
|
590
584
|
}
|
|
591
585
|
|
|
592
586
|
async keyDown(key: string, code = key): Promise<void> {
|
|
593
|
-
await (
|
|
594
|
-
await this.requirePage()
|
|
595
|
-
).send(
|
|
587
|
+
await (await this.requirePage()).send(
|
|
596
588
|
"Input.dispatchKeyEvent",
|
|
597
589
|
{
|
|
598
590
|
type: "keyDown",
|
|
@@ -606,9 +598,11 @@ export class AgentBrowserRuntime {
|
|
|
606
598
|
}
|
|
607
599
|
|
|
608
600
|
async keyUp(key: string, code = key): Promise<void> {
|
|
609
|
-
await (
|
|
610
|
-
|
|
611
|
-
|
|
601
|
+
await (await this.requirePage()).send(
|
|
602
|
+
"Input.dispatchKeyEvent",
|
|
603
|
+
{ type: "keyUp", key, code },
|
|
604
|
+
{ mutation: true },
|
|
605
|
+
);
|
|
612
606
|
this.heldKeys.delete(key);
|
|
613
607
|
}
|
|
614
608
|
|
|
@@ -617,9 +611,7 @@ export class AgentBrowserRuntime {
|
|
|
617
611
|
points: Array<{ x: number; y: number; id?: number }>,
|
|
618
612
|
): Promise<void> {
|
|
619
613
|
for (const point of points) this.assertPoint(point.x, point.y);
|
|
620
|
-
await (
|
|
621
|
-
await this.requirePage()
|
|
622
|
-
).send(
|
|
614
|
+
await (await this.requirePage()).send(
|
|
623
615
|
"Input.dispatchTouchEvent",
|
|
624
616
|
{
|
|
625
617
|
type,
|
|
@@ -681,9 +673,10 @@ export class AgentBrowserRuntime {
|
|
|
681
673
|
if (!force) {
|
|
682
674
|
try {
|
|
683
675
|
await this.invoke(["--session", this.session, "--json", "close"]);
|
|
684
|
-
} catch
|
|
676
|
+
} catch {
|
|
685
677
|
this.stopping = false;
|
|
686
|
-
|
|
678
|
+
await this.shutdown(true);
|
|
679
|
+
return;
|
|
687
680
|
}
|
|
688
681
|
} else {
|
|
689
682
|
const pid = await this.daemonPid();
|
package/server/browser-policy.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import type { RpcInput } from "@getpaseo/plugin";
|
|
2
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
type applyDevicePresetRpc,
|
|
5
|
+
type BrowserFrame,
|
|
6
|
+
type BrowserInputEvent,
|
|
7
|
+
type BrowserState,
|
|
5
8
|
DEFAULT_VIEWPORT,
|
|
9
|
+
DEVICE_PRESETS,
|
|
10
|
+
type DevicePresetId,
|
|
6
11
|
FRAME_MAX_BYTES,
|
|
7
12
|
MAX_VIEWPORT,
|
|
8
13
|
MIN_VIEWPORT,
|
|
9
14
|
mapDisplayedPoint,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sendBrowserInputRpc,
|
|
14
|
-
type BrowserFrame,
|
|
15
|
-
type BrowserInputEvent,
|
|
16
|
-
type BrowserState,
|
|
17
|
-
type DevicePresetId,
|
|
15
|
+
type navigateBrowserRpc,
|
|
16
|
+
type resizeBrowserRpc,
|
|
17
|
+
type sendBrowserInputRpc,
|
|
18
18
|
type Viewport,
|
|
19
19
|
} from "../shared/browser";
|
|
20
20
|
import type { JsonValue } from "./runtime-protocol";
|
package/server/browser.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { RpcInput, RpcOutput } from "@getpaseo/plugin";
|
|
2
|
-
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
3
1
|
import { spawn } from "node:child_process";
|
|
4
2
|
import { randomUUID } from "node:crypto";
|
|
5
3
|
import { access } from "node:fs/promises";
|
|
6
4
|
import { homedir } from "node:os";
|
|
7
5
|
import { join } from "node:path";
|
|
8
|
-
import {
|
|
6
|
+
import type { RpcInput, RpcOutput } from "@getpaseo/plugin";
|
|
7
|
+
import type { PluginHandlerContext } from "@getpaseo/plugin/server";
|
|
8
|
+
import type {
|
|
9
9
|
acquireControlRpc,
|
|
10
10
|
applyDevicePresetRpc,
|
|
11
11
|
attachBrowserRpc,
|
|
@@ -18,15 +18,15 @@ import {
|
|
|
18
18
|
sendBrowserInputRpc,
|
|
19
19
|
} from "../shared/browser";
|
|
20
20
|
import type { JsonValue } from "./runtime-protocol";
|
|
21
|
-
import { SupervisorClient } from "./supervisor-client";
|
|
22
21
|
import { resolveSupervisorPaths } from "./supervisor";
|
|
22
|
+
import { SupervisorClient } from "./supervisor-client";
|
|
23
23
|
|
|
24
|
-
export { SessionManager, normalizeBrowserUrl } from "./browser-policy";
|
|
25
24
|
export type {
|
|
26
25
|
BrowserRuntimeClient,
|
|
27
26
|
SessionManagerOptions,
|
|
28
27
|
WorkspaceValidator,
|
|
29
28
|
} from "./browser-policy";
|
|
29
|
+
export { normalizeBrowserUrl, SessionManager } from "./browser-policy";
|
|
30
30
|
|
|
31
31
|
type AttachInput = RpcInput<typeof attachBrowserRpc>;
|
|
32
32
|
type AttachOutput = RpcOutput<typeof attachBrowserRpc>;
|
package/server/cdp.ts
CHANGED
|
@@ -102,7 +102,11 @@ export class CdpConnection extends EventEmitter {
|
|
|
102
102
|
async send<T = unknown>(
|
|
103
103
|
method: string,
|
|
104
104
|
params: Record<string, unknown> = {},
|
|
105
|
-
options: {
|
|
105
|
+
options: {
|
|
106
|
+
sessionId?: string;
|
|
107
|
+
mutation?: boolean;
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
} = {},
|
|
106
110
|
): Promise<T> {
|
|
107
111
|
if (!this.isOpen) throw new CdpUnavailableError("CDP connection is closed");
|
|
108
112
|
const id = this.nextId++;
|
|
@@ -173,7 +177,10 @@ export class CdpConnection extends EventEmitter {
|
|
|
173
177
|
return;
|
|
174
178
|
}
|
|
175
179
|
if (message.method) {
|
|
176
|
-
const event: CdpEvent = {
|
|
180
|
+
const event: CdpEvent = {
|
|
181
|
+
method: message.method,
|
|
182
|
+
params: message.params ?? {},
|
|
183
|
+
};
|
|
177
184
|
if (message.sessionId) event.sessionId = message.sessionId;
|
|
178
185
|
this.emit("event", event);
|
|
179
186
|
this.emit(message.method, event.params, event.sessionId);
|
|
@@ -220,7 +227,10 @@ export class CdpSession extends EventEmitter {
|
|
|
220
227
|
params: Record<string, unknown> = {},
|
|
221
228
|
options: { mutation?: boolean; timeoutMs?: number } = {},
|
|
222
229
|
): Promise<T> {
|
|
223
|
-
return this.connection.send<T>(method, params, {
|
|
230
|
+
return this.connection.send<T>(method, params, {
|
|
231
|
+
...options,
|
|
232
|
+
sessionId: this.sessionId,
|
|
233
|
+
});
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
detach(): Promise<void> {
|
package/server/mcp-entry.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
1
|
import { homedir } from "node:os";
|
|
4
2
|
import { join } from "node:path";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import type { JsonValue } from "./runtime-protocol";
|
|
7
|
-
import { AgentSupervisorClient } from "./supervisor-client";
|
|
8
7
|
import { resolveSupervisorPaths } from "./supervisor";
|
|
8
|
+
import { AgentSupervisorClient } from "./supervisor-client";
|
|
9
9
|
|
|
10
10
|
const TICKET_ENV = "PASEO_SHARED_BROWSER_TICKET";
|
|
11
11
|
const MIN_VIEWPORT = { width: 320, height: 480 } as const;
|
|
@@ -71,7 +71,9 @@ function asObject(value: JsonValue): Record<string, JsonValue> {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function textResult(value: JsonValue) {
|
|
74
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
76
|
+
};
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
async function main(): Promise<void> {
|
|
@@ -83,7 +85,10 @@ async function main(): Promise<void> {
|
|
|
83
85
|
});
|
|
84
86
|
await client.open();
|
|
85
87
|
|
|
86
|
-
const server = new McpServer({
|
|
88
|
+
const server = new McpServer({
|
|
89
|
+
name: "paseo-shared-browser",
|
|
90
|
+
version: "0.2.2",
|
|
91
|
+
});
|
|
87
92
|
|
|
88
93
|
server.registerTool(
|
|
89
94
|
"shared_browser_status",
|
|
@@ -100,7 +105,9 @@ async function main(): Promise<void> {
|
|
|
100
105
|
{
|
|
101
106
|
description:
|
|
102
107
|
"Capture the current shared browser frame and state. Capture before sending input.",
|
|
103
|
-
inputSchema: z.object({
|
|
108
|
+
inputSchema: z.object({
|
|
109
|
+
quality: z.enum(["low", "medium", "high"]).default("medium"),
|
|
110
|
+
}),
|
|
104
111
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
105
112
|
},
|
|
106
113
|
async ({ quality }) => {
|
|
@@ -113,7 +120,11 @@ async function main(): Promise<void> {
|
|
|
113
120
|
if (frame && typeof frame === "object" && !Array.isArray(frame)) {
|
|
114
121
|
const image = frame as Record<string, JsonValue>;
|
|
115
122
|
if (typeof image.dataBase64 === "string" && typeof image.mimeType === "string")
|
|
116
|
-
content.push({
|
|
123
|
+
content.push({
|
|
124
|
+
type: "image",
|
|
125
|
+
data: image.dataBase64,
|
|
126
|
+
mimeType: image.mimeType,
|
|
127
|
+
});
|
|
117
128
|
}
|
|
118
129
|
return { content };
|
|
119
130
|
},
|
|
@@ -125,7 +136,11 @@ async function main(): Promise<void> {
|
|
|
125
136
|
description:
|
|
126
137
|
"Acquire browser control if no human or other viewer currently holds it. Forced takeover is unavailable.",
|
|
127
138
|
inputSchema: z.object({}),
|
|
128
|
-
annotations: {
|
|
139
|
+
annotations: {
|
|
140
|
+
readOnlyHint: false,
|
|
141
|
+
destructiveHint: false,
|
|
142
|
+
idempotentHint: false,
|
|
143
|
+
},
|
|
129
144
|
},
|
|
130
145
|
async () => textResult(await client.request("acquire-control", {})),
|
|
131
146
|
);
|
|
@@ -135,7 +150,11 @@ async function main(): Promise<void> {
|
|
|
135
150
|
{
|
|
136
151
|
description: "Release this agent's browser control lease.",
|
|
137
152
|
inputSchema: z.object({}),
|
|
138
|
-
annotations: {
|
|
153
|
+
annotations: {
|
|
154
|
+
readOnlyHint: false,
|
|
155
|
+
destructiveHint: false,
|
|
156
|
+
idempotentHint: false,
|
|
157
|
+
},
|
|
139
158
|
},
|
|
140
159
|
async () => textResult(await client.request("release-control", {})),
|
|
141
160
|
);
|
|
@@ -145,16 +164,27 @@ async function main(): Promise<void> {
|
|
|
145
164
|
description: "Navigate the shared browser using the agent's current observed state.",
|
|
146
165
|
inputSchema: z.object({
|
|
147
166
|
action: z.discriminatedUnion("kind", [
|
|
148
|
-
z.object({
|
|
167
|
+
z.object({
|
|
168
|
+
kind: z.literal("goto"),
|
|
169
|
+
url: z.string().trim().min(1).max(8_192),
|
|
170
|
+
}),
|
|
149
171
|
z.object({ kind: z.literal("back") }),
|
|
150
172
|
z.object({ kind: z.literal("forward") }),
|
|
151
173
|
z.object({ kind: z.literal("reload") }),
|
|
152
174
|
]),
|
|
153
175
|
}),
|
|
154
|
-
annotations: {
|
|
176
|
+
annotations: {
|
|
177
|
+
readOnlyHint: false,
|
|
178
|
+
destructiveHint: false,
|
|
179
|
+
idempotentHint: false,
|
|
180
|
+
},
|
|
155
181
|
},
|
|
156
182
|
async ({ action }) =>
|
|
157
|
-
textResult(
|
|
183
|
+
textResult(
|
|
184
|
+
await client.request("navigate", {
|
|
185
|
+
action: action as unknown as JsonValue,
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
158
188
|
);
|
|
159
189
|
|
|
160
190
|
server.registerTool(
|
|
@@ -163,7 +193,11 @@ async function main(): Promise<void> {
|
|
|
163
193
|
description:
|
|
164
194
|
"Send one input event using the exact frame returned by the latest capture. Stale frames are rejected.",
|
|
165
195
|
inputSchema: z.object({ event: inputEventSchema }),
|
|
166
|
-
annotations: {
|
|
196
|
+
annotations: {
|
|
197
|
+
readOnlyHint: false,
|
|
198
|
+
destructiveHint: false,
|
|
199
|
+
idempotentHint: false,
|
|
200
|
+
},
|
|
167
201
|
},
|
|
168
202
|
async ({ event }) =>
|
|
169
203
|
textResult(await client.request("input", { event: event as unknown as JsonValue })),
|
|
@@ -177,10 +211,18 @@ async function main(): Promise<void> {
|
|
|
177
211
|
width: z.number().int().min(MIN_VIEWPORT.width).max(MAX_VIEWPORT.width),
|
|
178
212
|
height: z.number().int().min(MIN_VIEWPORT.height).max(MAX_VIEWPORT.height),
|
|
179
213
|
}),
|
|
180
|
-
annotations: {
|
|
214
|
+
annotations: {
|
|
215
|
+
readOnlyHint: false,
|
|
216
|
+
destructiveHint: false,
|
|
217
|
+
idempotentHint: false,
|
|
218
|
+
},
|
|
181
219
|
},
|
|
182
220
|
async (viewport) =>
|
|
183
|
-
textResult(
|
|
221
|
+
textResult(
|
|
222
|
+
await client.request("viewport", {
|
|
223
|
+
viewport: viewport as unknown as JsonValue,
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
184
226
|
);
|
|
185
227
|
|
|
186
228
|
const transport = new StdioServerTransport();
|
package/server/runtime-owner.ts
CHANGED
|
@@ -11,12 +11,18 @@ interface OwnedRuntime {
|
|
|
11
11
|
runtimeId: string;
|
|
12
12
|
runtime: AgentBrowserRuntime;
|
|
13
13
|
}
|
|
14
|
+
interface RuntimeOwnerOptions {
|
|
15
|
+
initialUrl?: string;
|
|
16
|
+
headed?: boolean;
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
function paseoHome(): string {
|
|
16
20
|
return process.env.PASEO_HOME ?? join(homedir(), ".paseo");
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
export async function createRuntimeOwner(
|
|
23
|
+
export async function createRuntimeOwner(
|
|
24
|
+
options: RuntimeOwnerOptions = {},
|
|
25
|
+
): Promise<RuntimeOwner<OwnedRuntime>> {
|
|
20
26
|
const root = join(paseoHome(), "plugin-data", "shared-browser");
|
|
21
27
|
const ipcDirectory =
|
|
22
28
|
process.platform === "win32"
|
|
@@ -46,7 +52,8 @@ export async function createRuntimeOwner(): Promise<RuntimeOwner<OwnedRuntime>>
|
|
|
46
52
|
profilePath: join(root, "profiles", hash),
|
|
47
53
|
ipcDirectory,
|
|
48
54
|
session: `ws-${hash.slice(0, 16)}`,
|
|
49
|
-
initialUrl: DEFAULT_BROWSER_URL,
|
|
55
|
+
initialUrl: options.initialUrl ?? DEFAULT_BROWSER_URL,
|
|
56
|
+
headed: options.headed ?? false,
|
|
50
57
|
});
|
|
51
58
|
await runtime.launch();
|
|
52
59
|
return { runtimeId: randomUUID(), runtime };
|
|
@@ -41,7 +41,11 @@ export type AgentBrowserOperation =
|
|
|
41
41
|
export type RuntimeRequest =
|
|
42
42
|
| (AdminRequestBase & { method: "bridge.claim"; takeover?: boolean })
|
|
43
43
|
| (AdminRequestBase & { method: "bridge.heartbeat"; epoch: number })
|
|
44
|
-
| (AdminRequestBase & {
|
|
44
|
+
| (AdminRequestBase & {
|
|
45
|
+
method: "workspace.ensure";
|
|
46
|
+
epoch: number;
|
|
47
|
+
workspaceId: string;
|
|
48
|
+
})
|
|
45
49
|
| (AdminRequestBase & {
|
|
46
50
|
method: "workspace.request";
|
|
47
51
|
epoch: number;
|
|
@@ -49,14 +53,22 @@ export type RuntimeRequest =
|
|
|
49
53
|
operation: string;
|
|
50
54
|
input: JsonValue;
|
|
51
55
|
})
|
|
52
|
-
| (AdminRequestBase & {
|
|
56
|
+
| (AdminRequestBase & {
|
|
57
|
+
method: "workspace.archive";
|
|
58
|
+
epoch: number;
|
|
59
|
+
workspaceId: string;
|
|
60
|
+
})
|
|
53
61
|
| (AdminRequestBase & {
|
|
54
62
|
method: "browser.request";
|
|
55
63
|
epoch: number;
|
|
56
64
|
operation: string;
|
|
57
65
|
input: JsonValue;
|
|
58
66
|
})
|
|
59
|
-
| (AdminRequestBase & {
|
|
67
|
+
| (AdminRequestBase & {
|
|
68
|
+
method: "ticket.issue";
|
|
69
|
+
epoch: number;
|
|
70
|
+
ticket: string;
|
|
71
|
+
})
|
|
60
72
|
| (AdminRequestBase & {
|
|
61
73
|
method: "ticket.bind";
|
|
62
74
|
epoch: number;
|
|
@@ -64,7 +76,11 @@ export type RuntimeRequest =
|
|
|
64
76
|
agentId: string;
|
|
65
77
|
workspaceId: string;
|
|
66
78
|
})
|
|
67
|
-
| (AdminRequestBase & {
|
|
79
|
+
| (AdminRequestBase & {
|
|
80
|
+
method: "agent.revoke";
|
|
81
|
+
epoch: number;
|
|
82
|
+
agentId: string;
|
|
83
|
+
})
|
|
68
84
|
| (RuntimeRequestBase & {
|
|
69
85
|
method: "agent.request";
|
|
70
86
|
ticket: string;
|
|
@@ -76,7 +92,11 @@ export type RuntimeResult = BridgeLease | RuntimeDescriptor | JsonValue | { arch
|
|
|
76
92
|
|
|
77
93
|
export type RuntimeResponse =
|
|
78
94
|
| { id: string; ok: true; result: RuntimeResult }
|
|
79
|
-
| {
|
|
95
|
+
| {
|
|
96
|
+
id: string;
|
|
97
|
+
ok: false;
|
|
98
|
+
error: { code: RuntimeErrorCode; message: string };
|
|
99
|
+
};
|
|
80
100
|
|
|
81
101
|
export type RuntimeErrorCode =
|
|
82
102
|
| "AUTHENTICATION_FAILED"
|
|
@@ -145,7 +165,14 @@ export function parseRuntimeRequest(value: unknown): RuntimeRequest {
|
|
|
145
165
|
};
|
|
146
166
|
const epoch = requireInteger(value, "epoch");
|
|
147
167
|
if (method === "bridge.heartbeat")
|
|
148
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
id,
|
|
170
|
+
version: RUNTIME_PROTOCOL_VERSION,
|
|
171
|
+
method,
|
|
172
|
+
token,
|
|
173
|
+
bridgeId,
|
|
174
|
+
epoch,
|
|
175
|
+
};
|
|
149
176
|
if (method === "workspace.ensure" || method === "workspace.archive")
|
|
150
177
|
return {
|
|
151
178
|
id,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import { createConnection, type Socket } from "node:net";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
4
|
import {
|
|
5
|
-
RUNTIME_PROTOCOL_VERSION,
|
|
6
|
-
RuntimeProtocolError,
|
|
7
|
-
isRuntimeResponse,
|
|
8
5
|
type AgentBrowserOperation,
|
|
9
6
|
type BridgeLease,
|
|
7
|
+
isRuntimeResponse,
|
|
10
8
|
type JsonValue,
|
|
9
|
+
RUNTIME_PROTOCOL_VERSION,
|
|
11
10
|
type RuntimeDescriptor,
|
|
11
|
+
RuntimeProtocolError,
|
|
12
12
|
type RuntimeRequest,
|
|
13
13
|
type RuntimeResponse,
|
|
14
14
|
type RuntimeResult,
|
package/server/supervisor.ts
CHANGED
|
@@ -7,15 +7,15 @@ import type { BrowserFrame, BrowserInputEvent, BrowserState, Viewport } from "..
|
|
|
7
7
|
import { SessionManager } from "./browser-policy";
|
|
8
8
|
import { CdpUnknownOutcomeError } from "./cdp";
|
|
9
9
|
import {
|
|
10
|
+
type BridgeLease,
|
|
10
11
|
DEFAULT_BRIDGE_HEARTBEAT_MS,
|
|
11
12
|
DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
12
13
|
DEFAULT_ORPHAN_GRACE_MS,
|
|
13
|
-
RUNTIME_PROTOCOL_VERSION,
|
|
14
|
-
RuntimeProtocolError,
|
|
15
|
-
parseRuntimeRequest,
|
|
16
|
-
type BridgeLease,
|
|
17
14
|
type JsonValue,
|
|
15
|
+
parseRuntimeRequest,
|
|
16
|
+
RUNTIME_PROTOCOL_VERSION,
|
|
18
17
|
type RuntimeDescriptor,
|
|
18
|
+
RuntimeProtocolError,
|
|
19
19
|
type RuntimeRequest,
|
|
20
20
|
type RuntimeResponse,
|
|
21
21
|
type RuntimeResult,
|
package/shared/browser.ts
CHANGED
|
@@ -206,7 +206,10 @@ export const navigateBrowserRpc = defineRpc({
|
|
|
206
206
|
controlToken: opaqueTokenSchema,
|
|
207
207
|
expected: expectedStateSchema,
|
|
208
208
|
action: z.discriminatedUnion("kind", [
|
|
209
|
-
z.object({
|
|
209
|
+
z.object({
|
|
210
|
+
kind: z.literal("goto"),
|
|
211
|
+
url: z.string().trim().min(1).max(8_192),
|
|
212
|
+
}),
|
|
210
213
|
z.object({ kind: z.literal("back") }),
|
|
211
214
|
z.object({ kind: z.literal("forward") }),
|
|
212
215
|
z.object({ kind: z.literal("reload") }),
|