@omercnet/paseo-shared-browser 0.4.0 → 0.4.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.
- package/README.md +32 -18
- package/package.json +4 -7
- package/scripts/prepare-runtime.mjs +103 -21
- package/server/agent-browser-runtime.ts +11 -0
- package/server/runtime-owner.ts +16 -3
- package/server/supervisor-client.ts +6 -4
- package/server/supervisor.ts +16 -7
- package/CHANGELOG.md +0 -83
- package/docs/images/shared-browser-compact.png +0 -0
- package/docs/images/shared-browser-wide.png +0 -0
- package/tsconfig.json +0 -29
package/README.md
CHANGED
|
@@ -12,13 +12,17 @@ input.
|
|
|
12
12
|
|
|
13
13
|
## Screenshots
|
|
14
14
|
|
|
15
|
+
Both PNGs show the same isolated Chromium session from a dedicated Paseo test daemon. They were
|
|
16
|
+
captured at 2× pixel density after browser DevTools verified the rendered page contained no private
|
|
17
|
+
organization names.
|
|
18
|
+
|
|
15
19
|
### Wide desktop
|
|
16
20
|
|
|
17
|
-

|
|
21
|
+

|
|
18
22
|
|
|
19
23
|
### Compact client
|
|
20
24
|
|
|
21
|
-

|
|
25
|
+

|
|
22
26
|
|
|
23
27
|
## Runtime model
|
|
24
28
|
|
|
@@ -87,12 +91,16 @@ npm run prepare:runtime
|
|
|
87
91
|
paseo plugin install "$PWD"
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
The build requires Node.js 24 or newer and npm on the daemon host.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
build with the system package manager before
|
|
94
|
-
`PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE` to an absolute path when Chromium
|
|
95
|
-
The plugin does not emulate x64 Chromium.
|
|
94
|
+
The build requires Node.js 24 or newer and npm on the daemon host. On Linux x64, macOS, and
|
|
95
|
+
Windows, runtime preparation installs and stages the platform's Chrome for Testing distribution. On
|
|
96
|
+
Linux ARM64, where that download is unavailable, the installer automatically uses native Chromium
|
|
97
|
+
from `/usr/bin/chromium`. Install a non-Snap Chromium build with the system package manager before
|
|
98
|
+
adding the plugin. Set `PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE` to an absolute path when Chromium
|
|
99
|
+
is installed elsewhere. The plugin does not emulate x64 Chromium on Linux.
|
|
100
|
+
|
|
101
|
+
On Windows, the upstream installer retains its Chrome download cache under the OS user profile.
|
|
102
|
+
Before upgrading the plugin on Windows, close active Shared Browser sessions and stop the Paseo
|
|
103
|
+
daemon; Windows does not allow the installer to replace runtime executables that are still running.
|
|
96
104
|
|
|
97
105
|
Open a workspace, search the Command Center for **Open Shared Browser**, or tap the **Shared
|
|
98
106
|
Browser** composer pill while a workspace session is open.
|
|
@@ -124,11 +132,12 @@ disconnect does not close or reset either process.
|
|
|
124
132
|
|
|
125
133
|
The plugin recognizes only these deployment overrides:
|
|
126
134
|
|
|
127
|
-
| Variable | Meaning
|
|
128
|
-
| ------------------------------------------- |
|
|
129
|
-
| `PASEO_HOME` | Paseo data root. Defaults to `~/.paseo`; browser runtime, supervisor IPC, and profiles live below `plugin-data/shared-browser`.
|
|
130
|
-
| `PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY` | Absolute path to the pinned `agent-browser` executable. Defaults to `$PASEO_HOME/plugin-data/shared-browser/runtime/node_modules/.bin/agent-browser
|
|
131
|
-
| `PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE` | Absolute path to Chromium. Defaults to `$PASEO_HOME/plugin-data/shared-browser/runtime/chromium/chrome
|
|
135
|
+
| Variable | Meaning |
|
|
136
|
+
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `PASEO_HOME` | Paseo data root. Defaults to `~/.paseo`; browser runtime, supervisor IPC, and profiles live below `plugin-data/shared-browser`. |
|
|
138
|
+
| `PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY` | Absolute path to the pinned `agent-browser` executable. Defaults to `$PASEO_HOME/plugin-data/shared-browser/runtime/node_modules/.bin/agent-browser` (`agent-browser.exe` on Windows). |
|
|
139
|
+
| `PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE` | Absolute path to Chromium. Defaults to `$PASEO_HOME/plugin-data/shared-browser/runtime/chromium/chrome` (`chrome.exe` on Windows). On Linux ARM64, installation automatically links `/usr/bin/chromium`; use this override for another compatible, non-Snap location. |
|
|
140
|
+
| `PASEO_SHARED_BROWSER_CHROMIUM_ARGS` | Optional Chromium arguments passed through the managed runtime. Intended for host requirements such as `--no-sandbox` in an already-isolated CI runner; do not disable the browser sandbox on a general-purpose host. |
|
|
132
141
|
|
|
133
142
|
User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
|
|
134
143
|
|
|
@@ -143,11 +152,16 @@ User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
|
|
|
143
152
|
## Security boundary
|
|
144
153
|
|
|
145
154
|
- Paseo plugins are trusted, unsandboxed code. The plugin server, detached supervisor,
|
|
146
|
-
`agent-browser`, and
|
|
147
|
-
processes, credentials, and network.
|
|
148
|
-
- Supervisor IPC uses a user-private Unix socket and
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
`agent-browser`, Chromium, and other processes running as the daemon OS user share one trust
|
|
156
|
+
boundary and can reach that user's files, processes, credentials, and network.
|
|
157
|
+
- Supervisor IPC uses a user-private Unix socket on Linux and macOS or an installation-specific
|
|
158
|
+
named pipe on Windows, plus a token file under Paseo's user data root for every connection. On
|
|
159
|
+
Windows, file privacy relies on the ACL inherited from `PASEO_HOME`; custom locations must remain
|
|
160
|
+
private to the daemon user. `agent-browser` uses loopback TCP for its command and stream services
|
|
161
|
+
on Windows, so Windows support assumes a trusted single-user host; these services are not an
|
|
162
|
+
isolation boundary between local OS users.
|
|
163
|
+
- `agent-browser` IPC metadata and workspace profile directories are owner-only on POSIX systems.
|
|
164
|
+
The plugin rejects a non-loopback CDP endpoint.
|
|
151
165
|
- Viewer and control tokens coordinate clients already paired to the same Paseo daemon. Paseo v0.8
|
|
152
166
|
plugin RPC callbacks expose no authenticated caller identity, so these human-viewer tokens are a
|
|
153
167
|
workflow safeguard, not an authorization boundary. The stdio MCP adapter separately uses an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-shared-browser",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -22,19 +22,16 @@
|
|
|
22
22
|
"directory": "paseo-shared-browser"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"CHANGELOG.md",
|
|
26
25
|
"LICENSE",
|
|
27
26
|
"README.md",
|
|
28
27
|
"index.client.tsx",
|
|
29
28
|
"index.server.ts",
|
|
30
29
|
"client",
|
|
31
|
-
"docs",
|
|
32
30
|
"scripts/prepare-dependencies.mjs",
|
|
33
31
|
"scripts/prepare-runtime.mjs",
|
|
34
32
|
"server",
|
|
35
33
|
"shared",
|
|
36
|
-
"paseo-plugin.json"
|
|
37
|
-
"tsconfig.json"
|
|
34
|
+
"paseo-plugin.json"
|
|
38
35
|
],
|
|
39
36
|
"type": "module",
|
|
40
37
|
"publishConfig": {
|
|
@@ -43,8 +40,8 @@
|
|
|
43
40
|
"scripts": {
|
|
44
41
|
"typecheck": "tsc --noEmit",
|
|
45
42
|
"lint": "oxlint --deny-warnings .",
|
|
46
|
-
"format": "oxfmt .
|
|
47
|
-
"format:check": "oxfmt --check .
|
|
43
|
+
"format": "oxfmt --ignore-path .formatignore .",
|
|
44
|
+
"format:check": "oxfmt --check --ignore-path .formatignore .",
|
|
48
45
|
"prepare:runtime": "node scripts/prepare-runtime.mjs",
|
|
49
46
|
"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
47
|
"test:smoke": "vitest run tests/browser.smoke.test.ts --testTimeout=30000"
|
|
@@ -14,13 +14,15 @@ import {
|
|
|
14
14
|
writeFile,
|
|
15
15
|
} from "node:fs/promises";
|
|
16
16
|
import { constants } from "node:fs";
|
|
17
|
-
import { arch, homedir, platform } from "node:os";
|
|
17
|
+
import { arch, homedir, platform, userInfo } from "node:os";
|
|
18
18
|
import { dirname, join, resolve } from "node:path";
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
|
|
22
22
|
const execFileAsync = promisify(execFile);
|
|
23
23
|
const expectedVersion = "0.37.1";
|
|
24
|
+
const hostPlatform = platform();
|
|
25
|
+
const hostArch = arch();
|
|
24
26
|
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
25
27
|
const paseoHome = process.env.PASEO_HOME || join(homedir(), ".paseo");
|
|
26
28
|
const pluginDataRoot = join(paseoHome, "plugin-data", "shared-browser");
|
|
@@ -28,7 +30,16 @@ const runtimeRoot = join(pluginDataRoot, "runtime");
|
|
|
28
30
|
const stagingRoot = join(pluginDataRoot, `.runtime-${process.pid}`);
|
|
29
31
|
const runtimeModules = join(stagingRoot, "node_modules");
|
|
30
32
|
const packagedAgentBrowser = resolveDependencyRoot("agent-browser");
|
|
31
|
-
const runtimeEntry = join(
|
|
33
|
+
const runtimeEntry = join(
|
|
34
|
+
runtimeModules,
|
|
35
|
+
".bin",
|
|
36
|
+
hostPlatform === "win32" ? "agent-browser.exe" : "agent-browser",
|
|
37
|
+
);
|
|
38
|
+
const runtimeChromiumEntry = join(
|
|
39
|
+
stagingRoot,
|
|
40
|
+
"chromium",
|
|
41
|
+
hostPlatform === "win32" ? "chrome.exe" : "chrome",
|
|
42
|
+
);
|
|
32
43
|
const supervisorEntry = join(projectRoot, "server", "supervisor-entry.ts");
|
|
33
44
|
const mcpEntry = join(projectRoot, "server", "mcp-entry.ts");
|
|
34
45
|
|
|
@@ -49,6 +60,15 @@ async function validateAgentBrowser(path) {
|
|
|
49
60
|
throw new Error(`Expected agent-browser ${expectedVersion}, received ${version ?? "unknown"}`);
|
|
50
61
|
}
|
|
51
62
|
}
|
|
63
|
+
|
|
64
|
+
async function validateChromium(path) {
|
|
65
|
+
await execFileAsync(path, ["--version"], {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
timeout: 15_000,
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
async function bundleRuntimeEntries() {
|
|
53
73
|
await build({
|
|
54
74
|
entryPoints: {
|
|
@@ -81,11 +101,18 @@ async function applyPrivatePermissions(path) {
|
|
|
81
101
|
async function stageChromiumExecutable(executable) {
|
|
82
102
|
const chromiumRoot = join(stagingRoot, "chromium");
|
|
83
103
|
await mkdir(chromiumRoot, { recursive: true, mode: 0o700 });
|
|
84
|
-
|
|
104
|
+
if (hostPlatform === "darwin") {
|
|
105
|
+
const quotedExecutable = `'${executable.replaceAll("'", `'"'"'`)}'`;
|
|
106
|
+
await writeFile(runtimeChromiumEntry, `#!/bin/sh\nexec ${quotedExecutable} "$@"\n`);
|
|
107
|
+
await chmod(runtimeChromiumEntry, 0o700);
|
|
108
|
+
} else {
|
|
109
|
+
await symlink(executable, runtimeChromiumEntry);
|
|
110
|
+
}
|
|
111
|
+
return runtimeChromiumEntry;
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
async function findLinuxArm64Chromium() {
|
|
88
|
-
if (
|
|
115
|
+
if (hostPlatform !== "linux" || hostArch !== "arm64") return null;
|
|
89
116
|
|
|
90
117
|
const candidate = "/usr/bin/chromium";
|
|
91
118
|
try {
|
|
@@ -99,6 +126,25 @@ async function findLinuxArm64Chromium() {
|
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
|
|
129
|
+
function installedChromiumExecutable(releaseDir) {
|
|
130
|
+
switch (hostPlatform) {
|
|
131
|
+
case "darwin":
|
|
132
|
+
return join(
|
|
133
|
+
releaseDir,
|
|
134
|
+
"Google Chrome for Testing.app",
|
|
135
|
+
"Contents",
|
|
136
|
+
"MacOS",
|
|
137
|
+
"Google Chrome for Testing",
|
|
138
|
+
);
|
|
139
|
+
case "linux":
|
|
140
|
+
return join(releaseDir, "chrome");
|
|
141
|
+
case "win32":
|
|
142
|
+
return join(releaseDir, "chrome.exe");
|
|
143
|
+
default:
|
|
144
|
+
throw new Error(`Unsupported Chromium platform: ${hostPlatform}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
async function installChromium() {
|
|
103
149
|
const override = process.env.PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE;
|
|
104
150
|
if (override) {
|
|
@@ -106,35 +152,70 @@ async function installChromium() {
|
|
|
106
152
|
override,
|
|
107
153
|
"PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE",
|
|
108
154
|
);
|
|
109
|
-
|
|
110
|
-
return;
|
|
155
|
+
if (hostPlatform === "win32") return executable;
|
|
156
|
+
return stageChromiumExecutable(executable);
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
const systemChromium = await findLinuxArm64Chromium();
|
|
114
160
|
if (systemChromium) {
|
|
115
|
-
await stageChromiumExecutable(systemChromium);
|
|
161
|
+
const executable = await stageChromiumExecutable(systemChromium);
|
|
116
162
|
console.log(`Using system Chromium at ${systemChromium}`);
|
|
117
|
-
return;
|
|
163
|
+
return executable;
|
|
118
164
|
}
|
|
119
165
|
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
// Rust's dirs::home_dir() ignores HOME on Windows and resolves the profile
|
|
167
|
+
// through SHGetKnownFolderPath. Use that cache there; Unix stays isolated.
|
|
168
|
+
const installHome =
|
|
169
|
+
hostPlatform === "win32" ? userInfo().homedir : join(stagingRoot, "install-home");
|
|
170
|
+
if (hostPlatform !== "win32") {
|
|
171
|
+
await mkdir(installHome, { recursive: true, mode: 0o700 });
|
|
172
|
+
}
|
|
122
173
|
await execFileAsync(runtimeEntry, ["install"], {
|
|
123
|
-
env: { ...process.env, HOME: installHome },
|
|
174
|
+
env: hostPlatform === "win32" ? process.env : { ...process.env, HOME: installHome },
|
|
124
175
|
timeout: 180_000,
|
|
176
|
+
windowsHide: true,
|
|
125
177
|
});
|
|
126
178
|
|
|
127
179
|
const browsersRoot = join(installHome, ".agent-browser", "browsers");
|
|
128
180
|
const releases = (await readdir(browsersRoot, { withFileTypes: true }))
|
|
129
181
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith("chrome-"))
|
|
130
182
|
.map((entry) => entry.name)
|
|
131
|
-
.sort()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
183
|
+
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
|
|
184
|
+
const releaseDir = releases[0] && join(browsersRoot, releases[0]);
|
|
185
|
+
if (!releaseDir) throw new Error("agent-browser install did not produce a Chromium release");
|
|
186
|
+
const chrome = installedChromiumExecutable(releaseDir);
|
|
135
187
|
await requireExecutable(chrome, "Installed Chromium executable");
|
|
136
|
-
await cp(
|
|
137
|
-
|
|
188
|
+
await cp(releaseDir, join(stagingRoot, "chromium"), { recursive: true, force: true });
|
|
189
|
+
if (hostPlatform === "darwin") {
|
|
190
|
+
// chrome can't be a symlink to the .app's real binary here: macOS dyld
|
|
191
|
+
// resolves @executable_path from the invoked path's own directory, so a
|
|
192
|
+
// symlink at chromium/chrome would break the bundle's relative Frameworks
|
|
193
|
+
// lookup. Exec the real binary by its actual path instead.
|
|
194
|
+
await writeFile(
|
|
195
|
+
runtimeChromiumEntry,
|
|
196
|
+
`#!/bin/sh\nexec "$(cd "$(dirname "$0")" && pwd)/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" "$@"\n`,
|
|
197
|
+
);
|
|
198
|
+
await chmod(runtimeChromiumEntry, 0o700);
|
|
199
|
+
}
|
|
200
|
+
if (hostPlatform !== "win32") await rm(installHome, { recursive: true, force: true });
|
|
201
|
+
return runtimeChromiumEntry;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function stageAgentBrowserEntry() {
|
|
205
|
+
if (hostPlatform === "win32") {
|
|
206
|
+
const executableArch = hostArch === "arm64" ? "x64" : hostArch;
|
|
207
|
+
if (executableArch !== "x64") {
|
|
208
|
+
throw new Error(`Unsupported agent-browser Windows architecture: ${hostArch}`);
|
|
209
|
+
}
|
|
210
|
+
await cp(
|
|
211
|
+
join(packagedAgentBrowser, "bin", `agent-browser-win32-${executableArch}.exe`),
|
|
212
|
+
runtimeEntry,
|
|
213
|
+
{ force: true },
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
await symlink("../agent-browser/bin/agent-browser.js", runtimeEntry);
|
|
217
|
+
}
|
|
218
|
+
await chmod(runtimeEntry, 0o700);
|
|
138
219
|
}
|
|
139
220
|
|
|
140
221
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
@@ -144,8 +225,7 @@ await cp(packagedAgentBrowser, join(runtimeModules, "agent-browser"), {
|
|
|
144
225
|
recursive: true,
|
|
145
226
|
force: true,
|
|
146
227
|
});
|
|
147
|
-
await
|
|
148
|
-
await chmod(runtimeEntry, 0o700);
|
|
228
|
+
await stageAgentBrowserEntry();
|
|
149
229
|
|
|
150
230
|
const binaryOverride = process.env.PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY;
|
|
151
231
|
if (binaryOverride)
|
|
@@ -153,7 +233,7 @@ if (binaryOverride)
|
|
|
153
233
|
await requireExecutable(binaryOverride, "PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY"),
|
|
154
234
|
);
|
|
155
235
|
await validateAgentBrowser(await requireExecutable(runtimeEntry, "Packaged agent-browser entry"));
|
|
156
|
-
await installChromium();
|
|
236
|
+
const chromiumEntry = await installChromium();
|
|
157
237
|
await bundleRuntimeEntries();
|
|
158
238
|
|
|
159
239
|
await writeFile(
|
|
@@ -162,7 +242,9 @@ await writeFile(
|
|
|
162
242
|
{ mode: 0o600 },
|
|
163
243
|
);
|
|
164
244
|
await applyPrivatePermissions(stagingRoot);
|
|
165
|
-
|
|
245
|
+
if (hostPlatform !== "win32") {
|
|
246
|
+
await validateChromium(await requireExecutable(chromiumEntry, "Staged Chromium executable"));
|
|
247
|
+
}
|
|
166
248
|
await rename(stagingRoot, runtimeRoot);
|
|
167
249
|
|
|
168
250
|
console.log(`Prepared immutable Shared Browser runtime assets in ${runtimeRoot}`);
|
|
@@ -139,6 +139,15 @@ function runtimeEnvironment(ipcDirectory: string): NodeJS.ProcessEnv {
|
|
|
139
139
|
"DISPLAY",
|
|
140
140
|
"WAYLAND_DISPLAY",
|
|
141
141
|
"XAUTHORITY",
|
|
142
|
+
"APPDATA",
|
|
143
|
+
"LOCALAPPDATA",
|
|
144
|
+
"USERPROFILE",
|
|
145
|
+
"HOMEDRIVE",
|
|
146
|
+
"HOMEPATH",
|
|
147
|
+
"SYSTEMROOT",
|
|
148
|
+
"WINDIR",
|
|
149
|
+
"COMSPEC",
|
|
150
|
+
"PATHEXT",
|
|
142
151
|
]) {
|
|
143
152
|
const value = process.env[key];
|
|
144
153
|
if (value !== undefined) environment[key] = value;
|
|
@@ -221,6 +230,7 @@ export class AgentBrowserRuntime {
|
|
|
221
230
|
chmod(this.ipcDirectory, PRIVATE_DIRECTORY_MODE),
|
|
222
231
|
]);
|
|
223
232
|
await this.assertVersion();
|
|
233
|
+
const chromiumArguments = process.env.PASEO_SHARED_BROWSER_CHROMIUM_ARGS;
|
|
224
234
|
const opened = await this.invoke([
|
|
225
235
|
"--session",
|
|
226
236
|
this.session,
|
|
@@ -228,6 +238,7 @@ export class AgentBrowserRuntime {
|
|
|
228
238
|
this.profilePath,
|
|
229
239
|
"--executable-path",
|
|
230
240
|
this.executablePath,
|
|
241
|
+
...(chromiumArguments ? ["--args", chromiumArguments] : []),
|
|
231
242
|
"--json",
|
|
232
243
|
"open",
|
|
233
244
|
this.initialUrl,
|
package/server/runtime-owner.ts
CHANGED
|
@@ -18,12 +18,25 @@ function paseoHome(): string {
|
|
|
18
18
|
|
|
19
19
|
export async function createRuntimeOwner(): Promise<RuntimeOwner<OwnedRuntime>> {
|
|
20
20
|
const root = join(paseoHome(), "plugin-data", "shared-browser");
|
|
21
|
+
const ipcDirectory =
|
|
22
|
+
process.platform === "win32"
|
|
23
|
+
? join(root, "ipc")
|
|
24
|
+
: join(
|
|
25
|
+
"/tmp",
|
|
26
|
+
`paseo-shared-browser-${createHash("sha256").update(root).digest("hex").slice(0, 16)}`,
|
|
27
|
+
);
|
|
21
28
|
const binaryPath =
|
|
22
29
|
process.env.PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY ??
|
|
23
|
-
join(
|
|
30
|
+
join(
|
|
31
|
+
root,
|
|
32
|
+
"runtime",
|
|
33
|
+
"node_modules",
|
|
34
|
+
".bin",
|
|
35
|
+
process.platform === "win32" ? "agent-browser.exe" : "agent-browser",
|
|
36
|
+
);
|
|
24
37
|
const executablePath =
|
|
25
38
|
process.env.PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE ??
|
|
26
|
-
join(root, "runtime", "chromium", "chrome");
|
|
39
|
+
join(root, "runtime", "chromium", process.platform === "win32" ? "chrome.exe" : "chrome");
|
|
27
40
|
return {
|
|
28
41
|
async create(workspaceId) {
|
|
29
42
|
const hash = createHash("sha256").update(workspaceId).digest("hex");
|
|
@@ -31,7 +44,7 @@ export async function createRuntimeOwner(): Promise<RuntimeOwner<OwnedRuntime>>
|
|
|
31
44
|
binaryPath,
|
|
32
45
|
executablePath,
|
|
33
46
|
profilePath: join(root, "profiles", hash),
|
|
34
|
-
ipcDirectory
|
|
47
|
+
ipcDirectory,
|
|
35
48
|
session: `ws-${hash.slice(0, 16)}`,
|
|
36
49
|
initialUrl: DEFAULT_BROWSER_URL,
|
|
37
50
|
});
|
|
@@ -393,10 +393,12 @@ export class AgentSupervisorClient {
|
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
async function readPrivateFile(path: string): Promise<string> {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
396
|
+
if (process.platform !== "win32") {
|
|
397
|
+
const metadata = await stat(path);
|
|
398
|
+
if ((metadata.mode & 0o077) !== 0) throw new Error(`Supervisor file is not private: ${path}`);
|
|
399
|
+
if (typeof process.getuid === "function" && metadata.uid !== process.getuid()) {
|
|
400
|
+
throw new Error(`Supervisor file is owned by another user: ${path}`);
|
|
401
|
+
}
|
|
400
402
|
}
|
|
401
403
|
return await readFile(path, "utf8");
|
|
402
404
|
}
|
package/server/supervisor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createServer, type Socket } from "node:net";
|
|
4
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import type { BrowserFrame, BrowserInputEvent, BrowserState, Viewport } from "../shared/browser";
|
|
7
7
|
import { SessionManager } from "./browser-policy";
|
|
@@ -826,15 +826,24 @@ export function resolveSupervisorPaths(
|
|
|
826
826
|
"shared-browser",
|
|
827
827
|
`supervisor-v${RUNTIME_PROTOCOL_VERSION}`,
|
|
828
828
|
);
|
|
829
|
+
const socketId = createHash("sha256").update(root).digest("hex");
|
|
830
|
+
const socket =
|
|
831
|
+
process.platform === "win32"
|
|
832
|
+
? String.raw`\\.\pipe\paseo-shared-browser-${socketId}`
|
|
833
|
+
: join(tmpdir(), `psb-${socketId.slice(0, 16)}.sock`);
|
|
829
834
|
return {
|
|
830
835
|
root,
|
|
831
|
-
socket
|
|
836
|
+
socket,
|
|
832
837
|
token: join(root, "runtime.token"),
|
|
833
838
|
endpoint: join(root, "runtime.json"),
|
|
834
839
|
lock: join(root, "startup.lock"),
|
|
835
840
|
};
|
|
836
841
|
}
|
|
837
842
|
|
|
843
|
+
async function removeSupervisorSocket(path: string): Promise<void> {
|
|
844
|
+
if (process.platform !== "win32") await rm(path, { force: true });
|
|
845
|
+
}
|
|
846
|
+
|
|
838
847
|
export async function acquireStartupLock(path: string): Promise<() => Promise<void>> {
|
|
839
848
|
await mkdir(dirname(path), { recursive: true, mode: DIRECTORY_MODE });
|
|
840
849
|
await chmod(dirname(path), DIRECTORY_MODE);
|
|
@@ -956,7 +965,7 @@ export async function startSupervisorServer<Runtime extends RuntimeInstance>(
|
|
|
956
965
|
await tokenHandle.close();
|
|
957
966
|
}
|
|
958
967
|
await chmod(paths.token, FILE_MODE);
|
|
959
|
-
await
|
|
968
|
+
await removeSupervisorSocket(paths.socket);
|
|
960
969
|
await new Promise<void>((resolve, reject) => {
|
|
961
970
|
server.once("error", reject);
|
|
962
971
|
server.listen(paths.socket, () => {
|
|
@@ -964,7 +973,7 @@ export async function startSupervisorServer<Runtime extends RuntimeInstance>(
|
|
|
964
973
|
resolve();
|
|
965
974
|
});
|
|
966
975
|
});
|
|
967
|
-
await chmod(paths.socket, SOCKET_MODE);
|
|
976
|
+
if (process.platform !== "win32") await chmod(paths.socket, SOCKET_MODE);
|
|
968
977
|
await writeFile(
|
|
969
978
|
paths.endpoint,
|
|
970
979
|
JSON.stringify({ version: RUNTIME_PROTOCOL_VERSION, socket: paths.socket }),
|
|
@@ -974,7 +983,7 @@ export async function startSupervisorServer<Runtime extends RuntimeInstance>(
|
|
|
974
983
|
} catch (error) {
|
|
975
984
|
server.close();
|
|
976
985
|
await Promise.allSettled([
|
|
977
|
-
|
|
986
|
+
removeSupervisorSocket(paths.socket),
|
|
978
987
|
rm(paths.endpoint, { force: true }),
|
|
979
988
|
rm(paths.token, { force: true }),
|
|
980
989
|
releaseLock(),
|
|
@@ -1006,7 +1015,7 @@ export async function startSupervisorServer<Runtime extends RuntimeInstance>(
|
|
|
1006
1015
|
);
|
|
1007
1016
|
} finally {
|
|
1008
1017
|
await Promise.allSettled([
|
|
1009
|
-
|
|
1018
|
+
removeSupervisorSocket(paths.socket),
|
|
1010
1019
|
rm(paths.endpoint, { force: true }),
|
|
1011
1020
|
rm(paths.token, { force: true }),
|
|
1012
1021
|
releaseLock(),
|
package/CHANGELOG.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.4.0](https://github.com/omercnet/paseo-plugins/compare/shared-browser-v0.3.1...shared-browser-v0.4.0) (2026-09-18)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
### Features
|
|
7
|
-
|
|
8
|
-
* **release:** publish plugins to npm ([#80](https://github.com/omercnet/paseo-plugins/issues/80)) ([3c93048](https://github.com/omercnet/paseo-plugins/commit/3c93048cfefda97d8c2bc1631e3428fb64bdad09))
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Bug Fixes
|
|
12
|
-
|
|
13
|
-
* **shared-browser:** support Paseo 0.9 beta ([#86](https://github.com/omercnet/paseo-plugins/issues/86)) ([3af209f](https://github.com/omercnet/paseo-plugins/commit/3af209f3a8fa347ba7dc05c300061a682d809f88))
|
|
14
|
-
|
|
15
|
-
## [0.3.1](https://github.com/omercnet/paseo-plugins/compare/shared-browser-v0.3.0...shared-browser-v0.3.1) (2026-09-15)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
### Bug Fixes
|
|
19
|
-
|
|
20
|
-
* **shared-browser:** support system Chromium on Linux ARM64 ([#58](https://github.com/omercnet/paseo-plugins/issues/58)) ([f9c3dcb](https://github.com/omercnet/paseo-plugins/commit/f9c3dcb1031840c7cd0aed4a70f984d9d9bd24c1))
|
|
21
|
-
|
|
22
|
-
## [0.3.0](https://github.com/omercnet/paseo-plugins/compare/shared-browser-v0.2.2...shared-browser-v0.3.0) (2026-09-12)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### Features
|
|
26
|
-
|
|
27
|
-
* **shared-browser:** adopt agent-browser runtime ([#18](https://github.com/omercnet/paseo-plugins/issues/18)) ([09623f3](https://github.com/omercnet/paseo-plugins/commit/09623f34f2b93943c19f809a865813fa37f467bd))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
### Bug Fixes
|
|
31
|
-
|
|
32
|
-
* **shared-browser:** skip MCP injection for OMP ([#21](https://github.com/omercnet/paseo-plugins/issues/21)) ([ee2db08](https://github.com/omercnet/paseo-plugins/commit/ee2db08243640600c5592debe72f680446fa26d0))
|
|
33
|
-
|
|
34
|
-
## [1.0.0](https://github.com/omercnet/paseo-plugins/compare/shared-browser-v0.2.2...shared-browser-v1.0.0) (2026-09-12)
|
|
35
|
-
|
|
36
|
-
### ⚠ BREAKING CHANGES
|
|
37
|
-
|
|
38
|
-
* replace the browser runtime in place with plugin-owned `agent-browser` 0.37.1 and a detached daemon-host supervisor; deployments must provide the packaged runtime and a compatible Chromium executable, with Linux ARM64 deployments supplying native Chromium through `PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE`
|
|
39
|
-
|
|
40
|
-
### Bug Fixes
|
|
41
|
-
|
|
42
|
-
* leave OMP agent creation unchanged because the provider rejects external MCP servers
|
|
43
|
-
|
|
44
|
-
### Features
|
|
45
|
-
|
|
46
|
-
* preserve workspace browser processes across client disconnects and plugin reloads during a 120-second orphan grace, while retaining per-workspace profiles and tearing down active runtimes when workspace archive events are received
|
|
47
|
-
* add daemon-local runtime overrides through `PASEO_SHARED_BROWSER_AGENT_BROWSER_BINARY` and `PASEO_SHARED_BROWSER_CHROMIUM_EXECUTABLE`
|
|
48
|
-
|
|
49
|
-
## [0.2.2](https://github.com/omercnet/paseo-plugins/compare/shared-browser-v0.2.1...shared-browser-v0.2.2) (2026-09-10)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### Bug Fixes
|
|
53
|
-
|
|
54
|
-
* upgrade plugins to Paseo 0.8.0 ([c6b4e40](https://github.com/omercnet/paseo-plugins/commit/c6b4e4081fc9527de0ca6183a3903ff5ec4f93c4))
|
|
55
|
-
* upgrade plugins to Paseo 0.8.0 ([d7ad454](https://github.com/omercnet/paseo-plugins/commit/d7ad4540d2e1875bdf4a4d21f76ded25cac2f3dc))
|
|
56
|
-
|
|
57
|
-
## [0.2.1](https://github.com/omercnet/paseo-shared-browser/compare/v0.2.0...v0.2.1) (2026-09-08)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### Bug Fixes
|
|
61
|
-
|
|
62
|
-
* address Paseo 0.8 migration review feedback ([#10](https://github.com/omercnet/paseo-shared-browser/issues/10)) ([15d2515](https://github.com/omercnet/paseo-shared-browser/commit/15d25157503b7fb70d02c5e420522764250cab62))
|
|
63
|
-
|
|
64
|
-
## [0.2.0](https://github.com/omercnet/paseo-shared-browser/compare/v0.1.1...v0.2.0) (2026-09-08)
|
|
65
|
-
|
|
66
|
-
### Features
|
|
67
|
-
|
|
68
|
-
* migrate shared browser to Paseo 0.8 ([#8](https://github.com/omercnet/paseo-shared-browser/issues/8)) ([36f27ab](https://github.com/omercnet/paseo-shared-browser/commit/36f27abc5dab4a16c9770d6296916631965af333))
|
|
69
|
-
## [0.1.1](https://github.com/omercnet/paseo-shared-browser/compare/v0.1.0...v0.1.1) (2026-09-07)
|
|
70
|
-
|
|
71
|
-
### Bug Fixes
|
|
72
|
-
|
|
73
|
-
* include README screenshots in release archives ([#6](https://github.com/omercnet/paseo-shared-browser/issues/6)) ([a231db4](https://github.com/omercnet/paseo-shared-browser/commit/a231db4b20e9c52ebe8624ad69e34e907037ac0a))
|
|
74
|
-
|
|
75
|
-
## 0.1.0 (2026-09-07)
|
|
76
|
-
|
|
77
|
-
### Features
|
|
78
|
-
|
|
79
|
-
* share one live Chromium session per Paseo workspace ([b1e9fac](https://github.com/omercnet/paseo-shared-browser/commit/b1e9fac5021746cf2dc09f7bfb9ccf0cb3b82e5d))
|
|
80
|
-
|
|
81
|
-
### Bug Fixes
|
|
82
|
-
|
|
83
|
-
* keep format check off the generated changelog ([#4](https://github.com/omercnet/paseo-shared-browser/issues/4)) ([2219adf](https://github.com/omercnet/paseo-shared-browser/commit/2219adf3c11591a88dc54b899dece76003f313c4))
|
|
Binary file
|
|
Binary file
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Bundler",
|
|
6
|
-
"paths": {
|
|
7
|
-
"electron": ["./server/electron.d.ts"]
|
|
8
|
-
},
|
|
9
|
-
"lib": ["ES2024"],
|
|
10
|
-
"types": ["node", "react"],
|
|
11
|
-
"jsx": "react-jsx",
|
|
12
|
-
"strict": true,
|
|
13
|
-
"noUncheckedIndexedAccess": true,
|
|
14
|
-
"exactOptionalPropertyTypes": true,
|
|
15
|
-
"skipLibCheck": true,
|
|
16
|
-
"noEmit": true,
|
|
17
|
-
"esModuleInterop": true,
|
|
18
|
-
"allowSyntheticDefaultImports": true
|
|
19
|
-
},
|
|
20
|
-
"include": [
|
|
21
|
-
"index.client.tsx",
|
|
22
|
-
"index.server.ts",
|
|
23
|
-
"client/**/*.ts",
|
|
24
|
-
"client/**/*.tsx",
|
|
25
|
-
"server/**/*.ts",
|
|
26
|
-
"shared/**/*.ts",
|
|
27
|
-
"tests/**/*.ts"
|
|
28
|
-
]
|
|
29
|
-
}
|