@omercnet/paseo-shared-browser 0.4.2-next.133.1 → 0.4.2-next.146.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 +5 -6
- package/client/browser.tsx +99 -17
- package/package.json +3 -3
- package/paseo-plugin.json +2 -3
- package/server/agent-browser-runtime.ts +4 -0
- package/server/runtime-owner.ts +9 -2
package/README.md
CHANGED
|
@@ -148,9 +148,9 @@ User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
|
|
|
148
148
|
isolation boundary between local OS users.
|
|
149
149
|
- `agent-browser` IPC metadata and workspace profile directories are owner-only on POSIX systems.
|
|
150
150
|
The plugin rejects a non-loopback CDP endpoint.
|
|
151
|
-
- Viewer and control tokens coordinate clients already paired to the same Paseo daemon.
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
154
154
|
opaque, workspace-bound credential.
|
|
155
155
|
- Downloads, uploads, clipboard synchronization, media permissions, extensions, native passkeys,
|
|
156
156
|
and platform authenticators are not exposed by this plugin.
|
|
@@ -176,6 +176,5 @@ persistence, and archive teardown.
|
|
|
176
176
|
Release Please maintains the version, changelog, component tag, and GitHub release from
|
|
177
177
|
Conventional Commits in the monorepo.
|
|
178
178
|
|
|
179
|
-
Both the Paseo daemon and app must satisfy
|
|
180
|
-
and
|
|
181
|
-
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
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-shared-browser",
|
|
3
|
-
"version": "0.4.2-next.
|
|
3
|
+
"version": "0.4.2-next.146.1",
|
|
4
4
|
"description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"check:write": "biome check --write .",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
44
|
"prepare:runtime": "node scripts/prepare-runtime.mjs",
|
|
45
|
-
"test": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.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",
|
|
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": {
|
package/paseo-plugin.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shared-browser",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
},
|
|
3
|
+
"description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
|
|
4
|
+
"requirements": { "paseo": "^0.9.0" },
|
|
6
5
|
"build": [["node", "scripts/prepare-dependencies.mjs"], ["npm", "run", "prepare:runtime"]]
|
|
7
6
|
}
|
|
@@ -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
|
|
|
@@ -189,6 +190,7 @@ export class AgentBrowserRuntime {
|
|
|
189
190
|
readonly ipcDirectory: string;
|
|
190
191
|
readonly session: string;
|
|
191
192
|
private readonly initialUrl: string;
|
|
193
|
+
private readonly headed: boolean;
|
|
192
194
|
private readonly timeoutMs: number;
|
|
193
195
|
private readonly environment: NodeJS.ProcessEnv;
|
|
194
196
|
private connection: CdpConnection | null = null;
|
|
@@ -212,6 +214,7 @@ export class AgentBrowserRuntime {
|
|
|
212
214
|
throw new AgentBrowserIncompatibleError("agent-browser session name is invalid");
|
|
213
215
|
}
|
|
214
216
|
this.session = options.session;
|
|
217
|
+
this.headed = options.headed ?? false;
|
|
215
218
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
216
219
|
this.environment = runtimeEnvironment(this.ipcDirectory);
|
|
217
220
|
this.initialUrl = options.initialUrl ?? "about:blank";
|
|
@@ -236,6 +239,7 @@ export class AgentBrowserRuntime {
|
|
|
236
239
|
this.profilePath,
|
|
237
240
|
"--executable-path",
|
|
238
241
|
this.executablePath,
|
|
242
|
+
...(this.headed ? ["--headed"] : []),
|
|
239
243
|
...(chromiumArguments ? ["--args", chromiumArguments] : []),
|
|
240
244
|
"--json",
|
|
241
245
|
"open",
|
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 };
|