@omercnet/paseo-shared-browser 0.3.1-next.72.1

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.
@@ -0,0 +1,35 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import { SharedBrowserPanel, contributeSharedBrowserClient } from "./client/browser";
3
+
4
+ export default function contribute(client: PluginClientContext) {
5
+ client.addWorkspacePanel({
6
+ id: "shared-browser",
7
+ title: "Shared Browser",
8
+ icon: "PanelsTopLeft",
9
+ context: "workspace",
10
+ locations: ["workspace", "explorer"],
11
+ Component: SharedBrowserPanel,
12
+ });
13
+ client.addCommandCenterItem({
14
+ id: "open-shared-browser",
15
+ title: "Open Shared Browser",
16
+ icon: "PanelsTopLeft",
17
+ keywords: ["browser", "shared", "remote"],
18
+ context: "workspace",
19
+ onSelect({ openPanel }) {
20
+ openPanel("shared-browser");
21
+ },
22
+ });
23
+ client.addCommandCenterItem({
24
+ id: "open-shared-browser-agent",
25
+ title: "Open Shared Browser",
26
+ icon: "PanelsTopLeft",
27
+ keywords: ["browser", "shared", "remote"],
28
+ context: "agent",
29
+ onSelect({ openPanel }) {
30
+ openPanel("shared-browser");
31
+ },
32
+ });
33
+
34
+ return contributeSharedBrowserClient(client);
35
+ }
@@ -0,0 +1,118 @@
1
+ import type { PluginServerContext } from "@getpaseo/plugin/server";
2
+ import { randomBytes } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ bindAgentTicket,
7
+ cleanupBrowserServer,
8
+ handleAcquireControl,
9
+ handleApplyDevicePreset,
10
+ handleAttachBrowser,
11
+ handleCaptureBrowser,
12
+ handleDetachBrowser,
13
+ handleListOpenBrowserWorkspaces,
14
+ handleNavigateBrowser,
15
+ handleReleaseControl,
16
+ handleResizeBrowser,
17
+ handleSendBrowserInput,
18
+ handleWorkspaceArchived,
19
+ issueAgentTicket,
20
+ revokeAgentBrowserAccess,
21
+ } from "./server/browser";
22
+ import {
23
+ acquireControlRpc,
24
+ applyDevicePresetRpc,
25
+ attachBrowserRpc,
26
+ captureBrowserRpc,
27
+ detachBrowserRpc,
28
+ listOpenBrowserWorkspacesRpc,
29
+ navigateBrowserRpc,
30
+ releaseControlRpc,
31
+ resizeBrowserRpc,
32
+ sendBrowserInputRpc,
33
+ } from "./shared/browser";
34
+
35
+ const TICKET_ENV = "PASEO_SHARED_BROWSER_TICKET";
36
+ const MCP_SERVER_ID = "shared-browser";
37
+ const TICKET_ISSUE_TIMEOUT_MS = 2_000;
38
+
39
+ async function issueTicketWithinDeadline(ticket: string): Promise<boolean> {
40
+ let timer: NodeJS.Timeout | undefined;
41
+ try {
42
+ await Promise.race([
43
+ issueAgentTicket(ticket),
44
+ new Promise<never>((_, reject) => {
45
+ timer = setTimeout(
46
+ () => reject(new Error("Shared Browser ticket issuance timed out")),
47
+ TICKET_ISSUE_TIMEOUT_MS,
48
+ );
49
+ timer.unref();
50
+ }),
51
+ ]);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+
60
+ function paseoHome(): string {
61
+ return process.env.PASEO_HOME ?? join(homedir(), ".paseo");
62
+ }
63
+
64
+ function mcpBundlePath(): string {
65
+ return join(paseoHome(), "plugin-data", "shared-browser", "runtime", "shared-browser-mcp.cjs");
66
+ }
67
+
68
+ export default function contribute(server: PluginServerContext) {
69
+ server.handle(attachBrowserRpc, handleAttachBrowser);
70
+ server.handle(detachBrowserRpc, handleDetachBrowser);
71
+ server.handle(captureBrowserRpc, handleCaptureBrowser);
72
+ server.handle(listOpenBrowserWorkspacesRpc, handleListOpenBrowserWorkspaces);
73
+ server.handle(acquireControlRpc, handleAcquireControl);
74
+ server.handle(releaseControlRpc, handleReleaseControl);
75
+ server.handle(navigateBrowserRpc, handleNavigateBrowser);
76
+ server.handle(resizeBrowserRpc, handleResizeBrowser);
77
+ server.handle(applyDevicePresetRpc, handleApplyDevicePreset);
78
+ server.handle(sendBrowserInputRpc, handleSendBrowserInput);
79
+ server.before("agent.create", async ({ request }) => {
80
+ if (request.config.internal || request.config.provider === "omp") return request;
81
+ const ticket = randomBytes(32).toString("base64url");
82
+ if (!(await issueTicketWithinDeadline(ticket))) return request;
83
+ return {
84
+ ...request,
85
+ env: { ...request.env, [TICKET_ENV]: ticket },
86
+ config: {
87
+ ...request.config,
88
+ mcpServers: {
89
+ ...request.config.mcpServers,
90
+ [MCP_SERVER_ID]: {
91
+ type: "stdio",
92
+ command: process.execPath,
93
+ args: [mcpBundlePath()],
94
+ env: {
95
+ PASEO_HOME: paseoHome(),
96
+ [TICKET_ENV]: ticket,
97
+ },
98
+ },
99
+ },
100
+ },
101
+ };
102
+ });
103
+
104
+ server.before("agent.session_open", async ({ request }) => {
105
+ if (request.reason !== "create") return request;
106
+ const ticket = request.env[TICKET_ENV];
107
+ if (!ticket || request.purpose !== "interactive" || !request.workspaceId) return request;
108
+ await bindAgentTicket(ticket, request.agentId, request.workspaceId);
109
+ const { [TICKET_ENV]: _ticket, ...environment } = request.env;
110
+ return { ...request, env: environment };
111
+ });
112
+
113
+ server.on("agent.archived", ({ agent }) => revokeAgentBrowserAccess(agent.id));
114
+
115
+ server.on("workspace.archived", ({ workspace }) => handleWorkspaceArchived(workspace.id));
116
+
117
+ return cleanupBrowserServer;
118
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@omercnet/paseo-shared-browser",
3
+ "version": "0.3.1-next.72.1",
4
+ "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
5
+ "keywords": [
6
+ "browser",
7
+ "chromium",
8
+ "coding-agents",
9
+ "paseo",
10
+ "paseo-plugin",
11
+ "remote-browser"
12
+ ],
13
+ "homepage": "https://github.com/omercnet/paseo-plugins/tree/main/paseo-shared-browser#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/omercnet/paseo-plugins/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Omer Cohen",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/omercnet/paseo-plugins.git",
22
+ "directory": "paseo-shared-browser"
23
+ },
24
+ "files": [
25
+ "CHANGELOG.md",
26
+ "LICENSE",
27
+ "README.md",
28
+ "index.client.tsx",
29
+ "index.server.ts",
30
+ "client",
31
+ "docs",
32
+ "scripts/prepare-dependencies.mjs",
33
+ "scripts/prepare-runtime.mjs",
34
+ "server",
35
+ "shared",
36
+ "paseo-plugin.json",
37
+ "tsconfig.json"
38
+ ],
39
+ "type": "module",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "typecheck": "tsc --noEmit",
45
+ "lint": "oxlint --deny-warnings .",
46
+ "format": "oxfmt . '!CHANGELOG.md'",
47
+ "format:check": "oxfmt --check . '!CHANGELOG.md'",
48
+ "prepare:runtime": "node scripts/prepare-runtime.mjs",
49
+ "test:unit": "node --test scripts/prepare-dependencies.test.mjs && vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts",
50
+ "test:smoke": "vitest run tests/browser.smoke.test.ts --testTimeout=30000"
51
+ },
52
+ "dependencies": {
53
+ "@getpaseo/plugin": "0.8.0",
54
+ "@modelcontextprotocol/sdk": "1.30.0",
55
+ "agent-browser": "0.37.1",
56
+ "esbuild": "0.28.2",
57
+ "zod": "4.4.3"
58
+ },
59
+ "devDependencies": {
60
+ "@tanstack/react-query": "^5.90.11",
61
+ "@types/node": "^22.0.0",
62
+ "@types/react": "~19.2.0",
63
+ "oxfmt": "0.66.0",
64
+ "oxlint": "1.81.0",
65
+ "react": "19.1.0",
66
+ "react-native": "0.81.5",
67
+ "typescript": "^7.0.0",
68
+ "vitest": "^5.0.0"
69
+ },
70
+ "engines": {
71
+ "node": ">=24"
72
+ },
73
+ "packageManager": "npm@11.19.1",
74
+ "allowScripts": {
75
+ "agent-browser": true,
76
+ "esbuild@0.28.2": true
77
+ }
78
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "shared-browser",
3
+ "requirements": {
4
+ "paseo": "^0.8.0"
5
+ },
6
+ "build": [
7
+ ["node", "scripts/prepare-dependencies.mjs"],
8
+ ["npm", "run", "prepare:runtime"]
9
+ ]
10
+ }
@@ -0,0 +1,25 @@
1
+ import { createRequire } from "node:module";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
+
9
+ export function resolveDependencyRoot(packageName, from = import.meta.url) {
10
+ const require = createRequire(from);
11
+ return dirname(require.resolve(`${packageName}/package.json`));
12
+ }
13
+ export function prepareDependencies(root = projectRoot, execute = execFileSync) {
14
+ if (!existsSync(join(root, "package-lock.json"))) return false;
15
+
16
+ execute(process.platform === "win32" ? "npm.cmd" : "npm", ["ci", "--include=dev"], {
17
+ cwd: root,
18
+ stdio: "inherit",
19
+ });
20
+ return true;
21
+ }
22
+
23
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
24
+ prepareDependencies();
25
+ }
@@ -0,0 +1,168 @@
1
+ import { resolveDependencyRoot } from "./prepare-dependencies.mjs";
2
+ import { build } from "esbuild";
3
+ import { execFile } from "node:child_process";
4
+ import {
5
+ access,
6
+ chmod,
7
+ cp,
8
+ lstat,
9
+ mkdir,
10
+ readdir,
11
+ rename,
12
+ rm,
13
+ symlink,
14
+ writeFile,
15
+ } from "node:fs/promises";
16
+ import { constants } from "node:fs";
17
+ import { arch, homedir, platform } from "node:os";
18
+ import { dirname, join, resolve } from "node:path";
19
+ import { promisify } from "node:util";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const execFileAsync = promisify(execFile);
23
+ const expectedVersion = "0.37.1";
24
+ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
25
+ const paseoHome = process.env.PASEO_HOME || join(homedir(), ".paseo");
26
+ const pluginDataRoot = join(paseoHome, "plugin-data", "shared-browser");
27
+ const runtimeRoot = join(pluginDataRoot, "runtime");
28
+ const stagingRoot = join(pluginDataRoot, `.runtime-${process.pid}`);
29
+ const runtimeModules = join(stagingRoot, "node_modules");
30
+ const packagedAgentBrowser = resolveDependencyRoot("agent-browser");
31
+ const runtimeEntry = join(runtimeModules, ".bin", "agent-browser");
32
+ const supervisorEntry = join(projectRoot, "server", "supervisor-entry.ts");
33
+ const mcpEntry = join(projectRoot, "server", "mcp-entry.ts");
34
+
35
+ async function requireExecutable(path, label) {
36
+ const absolutePath = resolve(path);
37
+ try {
38
+ await access(absolutePath, constants.X_OK);
39
+ } catch {
40
+ throw new Error(`${label} is missing or not executable: ${absolutePath}`);
41
+ }
42
+ return absolutePath;
43
+ }
44
+
45
+ async function validateAgentBrowser(path) {
46
+ const { stdout } = await execFileAsync(path, ["--version"], { encoding: "utf8" });
47
+ const version = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/)?.[0];
48
+ if (version !== expectedVersion) {
49
+ throw new Error(`Expected agent-browser ${expectedVersion}, received ${version ?? "unknown"}`);
50
+ }
51
+ }
52
+ async function bundleRuntimeEntries() {
53
+ await build({
54
+ entryPoints: {
55
+ supervisor: supervisorEntry,
56
+ "shared-browser-mcp": mcpEntry,
57
+ },
58
+ outdir: stagingRoot,
59
+ outExtension: { ".js": ".cjs" },
60
+ bundle: true,
61
+ platform: "node",
62
+ target: "node20",
63
+ format: "cjs",
64
+ logLevel: "silent",
65
+ });
66
+ }
67
+
68
+ async function applyPrivatePermissions(path) {
69
+ const metadata = await lstat(path);
70
+ if (metadata.isSymbolicLink()) return;
71
+ if (metadata.isDirectory()) {
72
+ await chmod(path, 0o700);
73
+ for (const entry of await readdir(path)) {
74
+ await applyPrivatePermissions(join(path, entry));
75
+ }
76
+ return;
77
+ }
78
+ await chmod(path, metadata.mode & 0o111 ? 0o700 : 0o600);
79
+ }
80
+
81
+ async function stageChromiumExecutable(executable) {
82
+ const chromiumRoot = join(stagingRoot, "chromium");
83
+ await mkdir(chromiumRoot, { recursive: true, mode: 0o700 });
84
+ await symlink(executable, join(chromiumRoot, "chrome"));
85
+ }
86
+
87
+ async function findLinuxArm64Chromium() {
88
+ if (platform() !== "linux" || arch() !== "arm64") return null;
89
+
90
+ const candidate = "/usr/bin/chromium";
91
+ try {
92
+ return await requireExecutable(candidate, "System Chromium executable");
93
+ } catch {
94
+ throw new Error(
95
+ `Linux ARM64 requires native Chromium at ${candidate}. ` +
96
+ "Install a non-Snap Chromium build or set " +
97
+ "PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE to its absolute path.",
98
+ );
99
+ }
100
+ }
101
+
102
+ async function installChromium() {
103
+ const override = process.env.PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE;
104
+ if (override) {
105
+ const executable = await requireExecutable(
106
+ override,
107
+ "PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE",
108
+ );
109
+ await stageChromiumExecutable(executable);
110
+ return;
111
+ }
112
+
113
+ const systemChromium = await findLinuxArm64Chromium();
114
+ if (systemChromium) {
115
+ await stageChromiumExecutable(systemChromium);
116
+ console.log(`Using system Chromium at ${systemChromium}`);
117
+ return;
118
+ }
119
+
120
+ const installHome = join(stagingRoot, "install-home");
121
+ await mkdir(installHome, { recursive: true, mode: 0o700 });
122
+ await execFileAsync(runtimeEntry, ["install"], {
123
+ env: { ...process.env, HOME: installHome },
124
+ timeout: 180_000,
125
+ });
126
+
127
+ const browsersRoot = join(installHome, ".agent-browser", "browsers");
128
+ const releases = (await readdir(browsersRoot, { withFileTypes: true }))
129
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("chrome-"))
130
+ .map((entry) => entry.name)
131
+ .sort()
132
+ .reverse();
133
+ const chrome = releases[0] && join(browsersRoot, releases[0], "chrome");
134
+ if (!chrome) throw new Error("agent-browser install did not produce a Chromium release");
135
+ await requireExecutable(chrome, "Installed Chromium executable");
136
+ await cp(dirname(chrome), join(stagingRoot, "chromium"), { recursive: true, force: true });
137
+ await rm(installHome, { recursive: true, force: true });
138
+ }
139
+
140
+ await rm(stagingRoot, { recursive: true, force: true });
141
+ await mkdir(join(runtimeModules, ".bin"), { recursive: true, mode: 0o700 });
142
+ await chmod(pluginDataRoot, 0o700);
143
+ await cp(packagedAgentBrowser, join(runtimeModules, "agent-browser"), {
144
+ recursive: true,
145
+ force: true,
146
+ });
147
+ await symlink("../agent-browser/bin/agent-browser.js", runtimeEntry);
148
+ await chmod(runtimeEntry, 0o700);
149
+
150
+ const binaryOverride = process.env.PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY;
151
+ if (binaryOverride)
152
+ await validateAgentBrowser(
153
+ await requireExecutable(binaryOverride, "PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY"),
154
+ );
155
+ await validateAgentBrowser(await requireExecutable(runtimeEntry, "Packaged agent-browser entry"));
156
+ await installChromium();
157
+ await bundleRuntimeEntries();
158
+
159
+ await writeFile(
160
+ join(stagingRoot, "package.json"),
161
+ `${JSON.stringify({ private: true, type: "commonjs" }, null, 2)}\n`,
162
+ { mode: 0o600 },
163
+ );
164
+ await applyPrivatePermissions(stagingRoot);
165
+ await rm(runtimeRoot, { recursive: true, force: true });
166
+ await rename(stagingRoot, runtimeRoot);
167
+
168
+ console.log(`Prepared immutable Shared Browser runtime assets in ${runtimeRoot}`);