@parall/daemon 1.45.0 → 1.47.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.
Files changed (55) hide show
  1. package/bundle/manifest.json +15 -15
  2. package/bundle/parall-browser-pod.js +29796 -379
  3. package/bundle/parall-channel-exec.js +2 -0
  4. package/bundle/parall-claude-agent.js +26161 -277
  5. package/bundle/parall-codex-agent.js +26720 -335
  6. package/bundle/parall-daemon.js +31760 -2001
  7. package/bundle/parall-openclaw-agent.js +1 -0
  8. package/dist/browser-pod.d.ts +23 -1
  9. package/dist/browser-pod.d.ts.map +1 -1
  10. package/dist/browser-pod.js +104 -34
  11. package/dist/browser-profile-reconcile.d.ts +21 -0
  12. package/dist/browser-profile-reconcile.d.ts.map +1 -0
  13. package/dist/browser-profile-reconcile.js +188 -0
  14. package/dist/clip-runtime/browser-cdp.d.ts +40 -0
  15. package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
  16. package/dist/clip-runtime/browser-cdp.js +218 -0
  17. package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
  18. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  19. package/dist/clip-runtime/browser-profile-manager.js +316 -182
  20. package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
  21. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
  22. package/dist/clip-runtime/browser-profile-pool.js +18 -1
  23. package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
  24. package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
  25. package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
  26. package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
  27. package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
  28. package/dist/clip-runtime/browser-proxy-state.js +149 -0
  29. package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
  30. package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
  31. package/dist/clip-runtime/browser-quiescence.js +136 -0
  32. package/dist/clip-runtime/browser-readiness.d.ts +64 -0
  33. package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
  34. package/dist/clip-runtime/browser-readiness.js +161 -0
  35. package/dist/clip-runtime/browser-state-store.d.ts +13 -2
  36. package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
  37. package/dist/clip-runtime/browser-state-store.js +15 -6
  38. package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
  39. package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
  40. package/dist/clip-runtime/browser-target-registry.js +297 -0
  41. package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
  42. package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
  43. package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
  44. package/dist/daemon-main.d.ts.map +1 -1
  45. package/dist/daemon-main.js +3 -1
  46. package/dist/local-control.d.ts +59 -0
  47. package/dist/local-control.d.ts.map +1 -0
  48. package/dist/local-control.js +230 -0
  49. package/dist/local-profile-control.d.ts +77 -0
  50. package/dist/local-profile-control.d.ts.map +1 -0
  51. package/dist/local-profile-control.js +108 -0
  52. package/dist/supervisor.d.ts +19 -0
  53. package/dist/supervisor.d.ts.map +1 -1
  54. package/dist/supervisor.js +90 -187
  55. package/package.json +8 -6
@@ -0,0 +1,230 @@
1
+ import { chmodSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
2
+ import * as net from 'node:net';
3
+ import * as path from 'node:path';
4
+ import { daemonConfigDir } from './daemon-paths.js';
5
+ /** Typed failure a handler may throw; anything else maps to INTERNAL. */
6
+ export class LocalControlError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = 'LocalControlError';
12
+ }
13
+ }
14
+ const MAX_LINE_BYTES = 16 * 1024;
15
+ /** Socket path SSOT — the Desktop shell derives the same path from the shared
16
+ * config dir (PRLL_DAEMON_CONFIG_DIR namespacing keeps staging/prod apart). */
17
+ export function localControlSocketPath(env = process.env) {
18
+ return path.join(daemonConfigDir(env), 'run', 'control.sock');
19
+ }
20
+ /**
21
+ * Is something actually listening on this socket path? Used to tell a crashed
22
+ * daemon's leftover socket (safe to unlink) from a live one (must never be
23
+ * stolen — see start()). Fail-closed: only a definitive connection refusal
24
+ * (nothing is bound) counts as stale; any other error is treated as live so an
25
+ * ambiguous probe never licenses a unlink.
26
+ */
27
+ function isSocketLive(sockPath) {
28
+ return new Promise((resolve) => {
29
+ const probe = net.connect(sockPath);
30
+ const done = (live) => {
31
+ probe.removeAllListeners();
32
+ probe.destroy();
33
+ clearTimeout(timer);
34
+ resolve(live);
35
+ };
36
+ // A bound-but-wedged peer may accept without ever replying; treat the
37
+ // silence as live (do not steal the path from a process that exists).
38
+ const timer = setTimeout(() => done(true), 1000);
39
+ probe.once('connect', () => done(true));
40
+ probe.once('error', (err) => {
41
+ // ECONNREFUSED (nobody listening) / ENOENT (already gone) ⇒ stale.
42
+ done(!(err.code === 'ECONNREFUSED' || err.code === 'ENOENT'));
43
+ });
44
+ });
45
+ }
46
+ export class LocalControlServer {
47
+ opts;
48
+ server = null;
49
+ sockets = new Set();
50
+ constructor(opts) {
51
+ this.opts = opts;
52
+ }
53
+ async start() {
54
+ if (this.server)
55
+ throw new Error('local control server already started');
56
+ const sockPath = this.opts.socketPath;
57
+ const dir = path.dirname(sockPath);
58
+ // 0700 dir is the primary boundary; the socket chmod below is the backstop.
59
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
60
+ // Fail closed if the directory cannot be restricted: the 0700 dir is the
61
+ // primary boundary (a group/world-writable dir lets another user rename or
62
+ // replace the socket regardless of the socket file's own 0600), so a
63
+ // control plane we cannot isolate must not serve at all — same posture as
64
+ // the socket chmod below.
65
+ try {
66
+ chmodSync(dir, 0o700);
67
+ }
68
+ catch (err) {
69
+ throw new Error(`could not restrict control socket directory: ${String(err)}`);
70
+ }
71
+ // Reclaim a socket left behind by a crash — but ONLY after proving nothing
72
+ // is listening. Unlinking is not safe by default: on Unix it removes the
73
+ // PATHNAME, not the live socket, so a blind unlink would silently steal the
74
+ // path from a running daemon (its existing connections survive, new clients
75
+ // reach us instead) and listen() would then succeed rather than reporting
76
+ // EADDRINUSE. Probe first: connect-refused ⇒ nobody is listening ⇒ stale.
77
+ if (existsSync(sockPath)) {
78
+ let st;
79
+ try {
80
+ st = statSync(sockPath);
81
+ }
82
+ catch (err) {
83
+ throw new Error(`could not inspect existing control socket: ${String(err)}`);
84
+ }
85
+ // A non-socket file is unknown state — never delete it; let listen() fail.
86
+ if (st.isSocket()) {
87
+ if (await isSocketLive(sockPath)) {
88
+ throw new Error(`another process is already serving the local control socket at ${sockPath}`);
89
+ }
90
+ rmSync(sockPath, { force: true });
91
+ }
92
+ }
93
+ const server = net.createServer((socket) => this.handleConnection(socket));
94
+ this.server = server;
95
+ await new Promise((resolve, reject) => {
96
+ const onError = (err) => {
97
+ this.server = null;
98
+ reject(err);
99
+ };
100
+ server.once('error', onError);
101
+ server.listen(sockPath, () => {
102
+ server.removeListener('error', onError);
103
+ try {
104
+ chmodSync(sockPath, 0o600);
105
+ }
106
+ catch (err) {
107
+ // Fail closed: a control socket we cannot restrict must not serve.
108
+ server.close();
109
+ this.server = null;
110
+ reject(new Error(`could not restrict control socket permissions: ${String(err)}`));
111
+ return;
112
+ }
113
+ resolve();
114
+ });
115
+ });
116
+ server.on('error', (err) => this.opts.log.warn(`local control server error: ${String(err)}`));
117
+ this.opts.log.info(`local control socket listening at ${sockPath}`);
118
+ }
119
+ async stop() {
120
+ const server = this.server;
121
+ if (!server)
122
+ return;
123
+ this.server = null;
124
+ for (const s of this.sockets)
125
+ s.destroy();
126
+ this.sockets.clear();
127
+ await new Promise((resolve) => server.close(() => resolve()));
128
+ try {
129
+ rmSync(this.opts.socketPath, { force: true });
130
+ }
131
+ catch {
132
+ // best-effort cleanup
133
+ }
134
+ }
135
+ handleConnection(socket) {
136
+ this.sockets.add(socket);
137
+ socket.on('close', () => this.sockets.delete(socket));
138
+ socket.on('error', () => socket.destroy());
139
+ socket.setEncoding('utf8');
140
+ let buffer = '';
141
+ socket.on('data', (chunk) => {
142
+ buffer += chunk;
143
+ if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) {
144
+ // end() (half-close after flush), NOT destroy(): the reply must reach
145
+ // the client, and destroy() can discard the still-buffered write.
146
+ this.reply(socket, undefined, { code: 'INVALID_REQUEST', message: 'request too large' });
147
+ socket.end();
148
+ return;
149
+ }
150
+ let newline = buffer.indexOf('\n');
151
+ while (newline !== -1) {
152
+ const line = buffer.slice(0, newline).trim();
153
+ buffer = buffer.slice(newline + 1);
154
+ if (line)
155
+ void this.handleLine(socket, line);
156
+ newline = buffer.indexOf('\n');
157
+ }
158
+ });
159
+ }
160
+ async handleLine(socket, line) {
161
+ let req;
162
+ try {
163
+ const parsed = JSON.parse(line);
164
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
165
+ throw new Error('not an object');
166
+ }
167
+ req = parsed;
168
+ }
169
+ catch {
170
+ // end() not destroy() — flush the error reply before closing (see above).
171
+ this.reply(socket, undefined, { code: 'INVALID_REQUEST', message: 'invalid JSON request' });
172
+ socket.end();
173
+ return;
174
+ }
175
+ const id = typeof req.id === 'string' ? req.id : undefined;
176
+ try {
177
+ switch (req.command) {
178
+ case 'ping': {
179
+ const result = await this.opts.handlers.ping();
180
+ this.reply(socket, id, undefined, result);
181
+ return;
182
+ }
183
+ case 'profile.open':
184
+ case 'profile.stop':
185
+ case 'profile.reset': {
186
+ const action = req.command.slice('profile.'.length);
187
+ const profileId = typeof req.profile_id === 'string' ? req.profile_id.trim() : '';
188
+ const requester = typeof req.requester_user_id === 'string' ? req.requester_user_id.trim() : '';
189
+ if (!profileId || !requester) {
190
+ throw new LocalControlError('INVALID_REQUEST', 'profile_id and requester_user_id are required');
191
+ }
192
+ await this.opts.handlers.profileControl(action, profileId, requester);
193
+ this.reply(socket, id, undefined, { status: 'ok' });
194
+ return;
195
+ }
196
+ default:
197
+ throw new LocalControlError('UNKNOWN_COMMAND', `unknown command ${String(req.command)}`);
198
+ }
199
+ }
200
+ catch (err) {
201
+ if (err instanceof LocalControlError) {
202
+ this.reply(socket, id, { code: err.code, message: err.message });
203
+ }
204
+ else {
205
+ this.opts.log.warn(`local control command failed: ${String(err)}`);
206
+ this.reply(socket, id, {
207
+ code: 'INTERNAL',
208
+ message: err instanceof Error ? err.message : String(err),
209
+ });
210
+ }
211
+ }
212
+ }
213
+ reply(socket, id, error, result) {
214
+ if (socket.destroyed)
215
+ return;
216
+ const payload = { ok: !error };
217
+ if (id !== undefined)
218
+ payload.id = id;
219
+ if (error)
220
+ payload.error = error;
221
+ if (result !== undefined)
222
+ payload.result = result;
223
+ try {
224
+ socket.write(`${JSON.stringify(payload)}\n`);
225
+ }
226
+ catch {
227
+ socket.destroy();
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,77 @@
1
+ import type { MachineBrowserProfile } from '@parall/sdk';
2
+ import type { BrowserProfilePool } from './clip-runtime/browser-profile-pool.js';
3
+ import { LocalControlServer, type LocalProfileAction } from './local-control.js';
4
+ /**
5
+ * Same-machine browser-profile control (design daemon-control-authorization
6
+ * §7.1) — the daemon's local control PLANE: the socket server's lifecycle plus
7
+ * the authorization + drive logic behind profile.open/stop/reset. The socket
8
+ * transport itself lives in local-control.ts; the supervisor owns the pool, the
9
+ * queues, and the machine identity and lends them here.
10
+ */
11
+ /**
12
+ * What the handler needs from the supervisor. Identity and pool are getters,
13
+ * not values: both change over the daemon's life (bootstrap / refresh /
14
+ * provider state), and the handler must read whatever is true AT CALL TIME —
15
+ * a snapshot captured at wiring time could authorize against a stale owner.
16
+ */
17
+ export interface LocalProfileControlDeps {
18
+ /** machines.created_by; null until bootstrap proves one — then fail closed. */
19
+ owner(): string | null;
20
+ /** null when the browser-profile runtime is disabled on this daemon. */
21
+ pool(): BrowserProfilePool | null;
22
+ /** mck_-scoped GET /machines/me/browser-profiles — the assignment SSOT. */
23
+ listProfiles(): Promise<MachineBrowserProfile[]>;
24
+ /** Queue a revive (tracked for reconnect-reconcile fencing). */
25
+ enqueueRevive(profileId: string, op: () => Promise<void>): Promise<void>;
26
+ /** Queue a non-revive op (stop / reset). */
27
+ enqueueOp(profileId: string, op: () => Promise<void>): Promise<void>;
28
+ wipeBeforeRevive(pool: BrowserProfilePool, profileId: string, resetGen: number, generation: number | undefined): Promise<void>;
29
+ }
30
+ /** Extra facts the ping probe reports back to the Desktop shell. */
31
+ export interface LocalProfileControlPlaneDeps extends LocalProfileControlDeps {
32
+ machineId(): string | null;
33
+ orgId(): string | null;
34
+ log: {
35
+ info(msg: string): void;
36
+ warn(msg: string): void;
37
+ error(msg: string): void;
38
+ };
39
+ }
40
+ /**
41
+ * Start the same-machine control plane: a per-user Unix socket the owner's
42
+ * Desktop drives local browser profiles through.
43
+ *
44
+ * Local machines only — a K8s daemon has no same-machine human, and win32 has
45
+ * no Unix socket support here (design §7.1 defers the named pipe). Returns null
46
+ * when the plane does not apply OR fails to bind: failure is non-fatal, since
47
+ * the daemon's agent/clip duties are independent and the Desktop's ping probe
48
+ * simply reports the capability absent. The caller owns stop().
49
+ */
50
+ export declare function startLocalProfileControl(deps: LocalProfileControlPlaneDeps): Promise<LocalControlServer | null>;
51
+ /**
52
+ * TRUST MODEL — be precise about what each layer actually proves:
53
+ * - The 0600 socket + 0700 dir prove "same OS user on this machine". That
54
+ * is the REAL isolation boundary, and it is a hard one: that user already
55
+ * owns config.json (the mck_), so they could drive this machine anyway.
56
+ * - `requesterUserId` is supplied by the Desktop shell from the identity the
57
+ * web layer reported at auth:report-state. The shell holds no tokens (by
58
+ * design), so this is the app's assertion — NOT an independently verified
59
+ * identity. It is enforced here so the honest cases are right (a different
60
+ * Parall account signed into this computer's Desktop is refused, and a
61
+ * bridge call cannot name an arbitrary user), but it is defense in depth
62
+ * on top of the OS-user boundary, not a substitute for it.
63
+ * - The profile must be listed by the server FOR THIS MACHINE (the
64
+ * mck_-scoped /machines/me/browser-profiles is the SSOT — it also delivers
65
+ * the current lifecycle/reset generations, so status reports land under the
66
+ * server's fence exactly like the WS event path). This one IS
67
+ * server-authoritative.
68
+ * All three fail closed.
69
+ *
70
+ * The action then flows through the SAME fence + queue discipline as the WS
71
+ * lifecycle path (pre-fence on stop/reset, reset-before-open fail-closed,
72
+ * sinceSeq stale-revive capture) — but unlike the WS handler it PROPAGATES
73
+ * failures, so the Desktop caller gets a real error instead of a log line.
74
+ * Status converges server-side through the pool's normal reportStatus.
75
+ */
76
+ export declare function handleLocalProfileControl(deps: LocalProfileControlDeps, action: LocalProfileAction, profileId: string, requesterUserId: string): Promise<void>;
77
+ //# sourceMappingURL=local-profile-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-profile-control.d.ts","sourceRoot":"","sources":["../src/local-profile-control.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAC;AACjF,OAAO,EAEL,kBAAkB,EAClB,KAAK,kBAAkB,EAExB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,+EAA+E;IAC/E,KAAK,IAAI,MAAM,GAAG,IAAI,CAAC;IACvB,wEAAwE;IACxE,IAAI,IAAI,kBAAkB,GAAG,IAAI,CAAC;IAClC,2EAA2E;IAC3E,YAAY,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACjD,gEAAgE;IAChE,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,4CAA4C;IAC5C,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,gBAAgB,CACd,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,oEAAoE;AACpE,MAAM,WAAW,4BAA6B,SAAQ,uBAAuB;IAC3E,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,IAAI,MAAM,GAAG,IAAI,CAAC;IACvB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACrF;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAsBpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,kBAAkB,EAC1B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CA2Df"}
@@ -0,0 +1,108 @@
1
+ import { LocalControlError, LocalControlServer, localControlSocketPath, } from './local-control.js';
2
+ /**
3
+ * Start the same-machine control plane: a per-user Unix socket the owner's
4
+ * Desktop drives local browser profiles through.
5
+ *
6
+ * Local machines only — a K8s daemon has no same-machine human, and win32 has
7
+ * no Unix socket support here (design §7.1 defers the named pipe). Returns null
8
+ * when the plane does not apply OR fails to bind: failure is non-fatal, since
9
+ * the daemon's agent/clip duties are independent and the Desktop's ping probe
10
+ * simply reports the capability absent. The caller owns stop().
11
+ */
12
+ export async function startLocalProfileControl(deps) {
13
+ if (process.env.KUBERNETES_SERVICE_HOST || process.platform === 'win32')
14
+ return null;
15
+ const server = new LocalControlServer({
16
+ socketPath: localControlSocketPath(),
17
+ log: deps.log,
18
+ handlers: {
19
+ ping: async () => ({
20
+ machine_id: deps.machineId(),
21
+ org_id: deps.orgId(),
22
+ profile_control: deps.pool() !== null,
23
+ }),
24
+ profileControl: (action, profileId, requesterUserId) => handleLocalProfileControl(deps, action, profileId, requesterUserId),
25
+ },
26
+ });
27
+ try {
28
+ await server.start();
29
+ return server;
30
+ }
31
+ catch (err) {
32
+ deps.log.warn(`local control socket unavailable: ${String(err)}`);
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * TRUST MODEL — be precise about what each layer actually proves:
38
+ * - The 0600 socket + 0700 dir prove "same OS user on this machine". That
39
+ * is the REAL isolation boundary, and it is a hard one: that user already
40
+ * owns config.json (the mck_), so they could drive this machine anyway.
41
+ * - `requesterUserId` is supplied by the Desktop shell from the identity the
42
+ * web layer reported at auth:report-state. The shell holds no tokens (by
43
+ * design), so this is the app's assertion — NOT an independently verified
44
+ * identity. It is enforced here so the honest cases are right (a different
45
+ * Parall account signed into this computer's Desktop is refused, and a
46
+ * bridge call cannot name an arbitrary user), but it is defense in depth
47
+ * on top of the OS-user boundary, not a substitute for it.
48
+ * - The profile must be listed by the server FOR THIS MACHINE (the
49
+ * mck_-scoped /machines/me/browser-profiles is the SSOT — it also delivers
50
+ * the current lifecycle/reset generations, so status reports land under the
51
+ * server's fence exactly like the WS event path). This one IS
52
+ * server-authoritative.
53
+ * All three fail closed.
54
+ *
55
+ * The action then flows through the SAME fence + queue discipline as the WS
56
+ * lifecycle path (pre-fence on stop/reset, reset-before-open fail-closed,
57
+ * sinceSeq stale-revive capture) — but unlike the WS handler it PROPAGATES
58
+ * failures, so the Desktop caller gets a real error instead of a log line.
59
+ * Status converges server-side through the pool's normal reportStatus.
60
+ */
61
+ export async function handleLocalProfileControl(deps, action, profileId, requesterUserId) {
62
+ const owner = deps.owner();
63
+ if (!owner || !requesterUserId || requesterUserId !== owner) {
64
+ throw new LocalControlError('OWNER_MISMATCH', 'Only the machine owner can control local browser profiles');
65
+ }
66
+ const pool = deps.pool();
67
+ if (!pool) {
68
+ throw new LocalControlError('RUNTIME_DISABLED', 'Browser profile runtime is disabled on this daemon');
69
+ }
70
+ let profiles;
71
+ try {
72
+ profiles = await deps.listProfiles();
73
+ }
74
+ catch (err) {
75
+ throw new LocalControlError('SERVER_UNAVAILABLE', `could not verify the profile with the server: ${String(err)}`);
76
+ }
77
+ const profile = profiles.find((p) => p.id === profileId && p.machine_id);
78
+ if (!profile) {
79
+ throw new LocalControlError('PROFILE_NOT_ON_MACHINE', 'Browser profile is not assigned to this machine');
80
+ }
81
+ const generation = profile.lifecycle_generation;
82
+ if (action === 'open') {
83
+ // Mirror the WS open path: a reset recorded on the server but not yet
84
+ // applied locally must wipe BEFORE the open (fail-closed), and the fence
85
+ // generation is captured after any pre-fence so a concurrent stop wins.
86
+ const resetGen = profile.reset_generation ?? 0;
87
+ const wipeGen = resetGen > pool.appliedResetGeneration(profileId) ? resetGen : null;
88
+ if (wipeGen !== null)
89
+ pool.fence(profileId);
90
+ const sinceSeq = pool.stopSeqOf(profileId);
91
+ await deps.enqueueRevive(profileId, async () => {
92
+ if (wipeGen !== null)
93
+ await deps.wipeBeforeRevive(pool, profileId, wipeGen, generation);
94
+ await pool.openProfile(profileId, undefined, sinceSeq, generation);
95
+ });
96
+ return;
97
+ }
98
+ // stop / reset: pre-fence synchronously (reject hub invokes racing the
99
+ // queued teardown), same contract as the WS event receipt.
100
+ pool.fence(profileId);
101
+ if (action === 'stop') {
102
+ await deps.enqueueOp(profileId, () => pool.stopProfile(profileId, generation));
103
+ return;
104
+ }
105
+ // Local reset wipes immediately; record the server's current
106
+ // reset_generation as applied so a reconnect reconcile won't re-wipe.
107
+ await deps.enqueueOp(profileId, () => pool.resetProfile(profileId, generation, profile.reset_generation));
108
+ }
@@ -1,6 +1,7 @@
1
1
  import { type GatewayLogger } from '@parall/agent-core';
2
2
  import { type ParallClient } from '@parall/sdk';
3
3
  import { type ClaudeDaemonConfig } from './config.js';
4
+ import { type LocalProfileControlPlaneDeps } from './local-profile-control.js';
4
5
  import type { DaemonUpdater } from './updater.js';
5
6
  import type { PendingUpdateHealthGate } from './update-health-gate.js';
6
7
  /**
@@ -39,6 +40,7 @@ export declare class DaemonSupervisor {
39
40
  private running;
40
41
  private machineId;
41
42
  private machineOrgId;
43
+ private machineCreatedBy;
42
44
  private machineLlmSource;
43
45
  private machineProviderEnabled;
44
46
  private machineClipProviderUrl;
@@ -47,6 +49,7 @@ export declare class DaemonSupervisor {
47
49
  private updater;
48
50
  private healthGate;
49
51
  private browserProfilePool;
52
+ private localControl;
50
53
  private clipManager;
51
54
  private clipProvider;
52
55
  private clipReconcileTimer;
@@ -81,6 +84,12 @@ export declare class DaemonSupervisor {
81
84
  * proxy-configured profile (the caller reports `error` and retries next tick).
82
85
  */
83
86
  private resolveBrowserProfileProxy;
87
+ /** Proxy readiness probe endpoint for BYOC profiles (browser-readiness.ts): an
88
+ * explicit override, else the api-server's Parall-owned public `/generate_204`.
89
+ * The probe navigates a sacrificial tab THROUGH the profile's proxy, so the
90
+ * target must be public + proxy-resolvable — `config.apiUrl` is the public API
91
+ * host for BYOC. Falls back to a well-known public 204 if apiUrl is unusable. */
92
+ private resolveBrowserProxyProbeUrl;
84
93
  /**
85
94
  * Detects a legacy flat state layout (no agents/ subdir) and migrates it
86
95
  * into the per-agent directory for the owning agent. Ownership is determined
@@ -93,6 +102,16 @@ export declare class DaemonSupervisor {
93
102
  private handleFilesystemBrowse;
94
103
  private handleBrowserProfileLifecycle;
95
104
  private enqueueBrowserProfileLifecycle;
105
+ /**
106
+ * What the supervisor lends the local control plane: identity, pool, queues.
107
+ * Getters, not values — owner and pool both change over the daemon's life
108
+ * (bootstrap / refresh / provider state), so the handler must read what is
109
+ * true AT CALL TIME; a snapshot could authorize against a stale owner.
110
+ *
111
+ * One definition, used by both `run()` and the authorization tests, so the
112
+ * matrix under test is wired exactly like production.
113
+ */
114
+ localProfileControlDeps(): LocalProfileControlPlaneDeps;
96
115
  private enqueueBrowserProfileOp;
97
116
  private enqueueBrowserProfileRevive;
98
117
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAIA,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,EAgBL,KAAK,YAAY,EAElB,MAAM,aAAa,CAAC;AAWrB,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,KAAK,4BAA4B,EAElC,MAAM,4BAA4B,CAAC;AAcpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAsBvE;;;;GAIG;AACH,iBAAS,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAa3E;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAC;AA4B5B;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;IA+EzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAhFtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAM3D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAoC;IAM3E,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAA6B;IAC1E,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAqC;IAC/E,OAAO,CAAC,QAAQ,CAAC,6BAA6B,CAAqC;IACnF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IAKrD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;IAQ1D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyB;IAC1D,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAuB;IAK3C,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,gBAAgB,CAAoB;IAG5C,OAAO,CAAC,sBAAsB,CAAQ;IAItC,OAAO,CAAC,sBAAsB,CAAuB;IAGrD,OAAO,CAAC,uBAAuB,CAAuB;IACtD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,OAAO,CAA8B;IAI7C,OAAO,CAAC,UAAU,CAAwC;IAC1D,OAAO,CAAC,kBAAkB,CAAmC;IAC7D,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,qBAAqB,CAA8B;IAI3D,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,kBAAkB,CAA+B;IACzD,OAAO,CAAC,qBAAqB,CAA8B;IAE3D,OAAO,CAAC,gBAAgB,CAAyC;IAIjE,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,YAAY,CAAuB;gBAGxB,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,aAAa;IAGrC,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAIxC;wFACoF;IACpF,aAAa,CAAC,IAAI,EAAE,uBAAuB,GAAG,IAAI;IAIlD,yEAAyE;IACnE,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAuN7C,sEAAsE;IAChE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAkEb,kBAAkB;YAyClB,aAAa;YAsGb,gBAAgB;IAiB9B,OAAO,CAAC,wBAAwB;IAkBhC;;;;;;;;;OASG;YACW,0BAA0B;IAgBxC;;;;sFAIkF;IAClF,OAAO,CAAC,2BAA2B;IAYnC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;YA6CX,kBAAkB;YAqBlB,mBAAmB;YAgBnB,mBAAmB;YAqBnB,sBAAsB;YAwBtB,6BAA6B;IAkG3C,OAAO,CAAC,8BAA8B;IAkCtC;;;;;;;;OAQG;IACH,uBAAuB,IAAI,4BAA4B;IAcvD,OAAO,CAAC,uBAAuB;IA4B/B,OAAO,CAAC,2BAA2B;IAYnC;;;;;OAKG;YACW,0BAA0B;IAiCxC,OAAO,CAAC,2BAA2B;YAIrB,cAAc;YAId,qBAAqB;YAgBrB,wBAAwB;IAoDtC,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,sBAAsB;IAM9B;;;;;;;OAOG;IACH,OAAO,CAAC,uBAAuB;IA0B/B,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,uBAAuB;IAyB/B,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,mBAAmB;YAab,mBAAmB;IAyCjC,OAAO,CAAC,wBAAwB;YAelB,6BAA6B;IAqC3C,OAAO,CAAC,2BAA2B;IAYnC,OAAO,CAAC,wBAAwB;YAUlB,oBAAoB;IAwClC;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IASpB;;;;;;;;OAQG;YACW,sBAAsB;YA4CtB,kBAAkB;IAahC;;;;;;;;;OASG;YACW,wBAAwB;IAwDtC;;;;;;;OAOG;YACW,qBAAqB;IAcnC,OAAO,CAAC,+BAA+B;IAYvC,OAAO,CAAC,4BAA4B;YAQtB,eAAe;YAgDf,UAAU;IA+BxB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;YAgBhB,cAAc;IAoE5B,OAAO,CAAC,UAAU;IAsKlB;;;;;OAKG;YACW,cAAc;CAwB7B"}