@parall/daemon 1.46.0 → 1.48.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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Local (same-machine) control plane for the BYOC daemon — the ONLY human
3
+ * control surface for local browser profiles per
4
+ * docs/engineering-design/daemon-control-authorization-design.md §7.1.
5
+ *
6
+ * Transport: a per-user Unix domain socket under the daemon config dir
7
+ * (`<daemonConfigDir>/run/control.sock`), directory mode 0700 + socket mode
8
+ * 0600, so only the same OS user (the person who installed the daemon) can
9
+ * connect. The Parall Desktop shell — Electron main, never the renderer — is
10
+ * the intended client. The trust boundary is deliberately the OS user account:
11
+ * anything running as that user already owns `config.json` (the mck_), so the
12
+ * socket adds no new exposure; the platform-identity check (requester ==
13
+ * machines.created_by) is enforced by the supervisor's handler on top.
14
+ *
15
+ * Protocol: newline-delimited JSON, one request per line, one response per
16
+ * line, over a short-lived connection:
17
+ * → {"id":"1","command":"profile.open","profile_id":"brp_x","requester_user_id":"usr_y"}
18
+ * ← {"id":"1","ok":true,"result":{...}}
19
+ * ← {"id":"1","ok":false,"error":{"code":"OWNER_MISMATCH","message":"..."}}
20
+ * Unknown fields are ignored (forward compat); unknown commands are rejected.
21
+ * Windows support (named pipe) is deliberately deferred — the Desktop daemon
22
+ * module is darwin-only today (design §7.1).
23
+ */
24
+ export type LocalProfileAction = 'open' | 'stop' | 'reset';
25
+ /** Typed failure a handler may throw; anything else maps to INTERNAL. */
26
+ export declare class LocalControlError extends Error {
27
+ readonly code: string;
28
+ constructor(code: string, message: string);
29
+ }
30
+ export interface LocalControlHandlers {
31
+ /** Liveness + capability probe; the result rides back verbatim. */
32
+ ping(): Promise<Record<string, unknown>>;
33
+ /** Drive one local browser profile. Resolves when the action completed. */
34
+ profileControl(action: LocalProfileAction, profileId: string, requesterUserId: string): Promise<void>;
35
+ }
36
+ /** Socket path SSOT — the Desktop shell derives the same path from the shared
37
+ * config dir (PRLL_DAEMON_CONFIG_DIR namespacing keeps staging/prod apart). */
38
+ export declare function localControlSocketPath(env?: NodeJS.ProcessEnv): string;
39
+ export interface LocalControlServerOptions {
40
+ socketPath: string;
41
+ handlers: LocalControlHandlers;
42
+ log: {
43
+ info(msg: string): void;
44
+ warn(msg: string): void;
45
+ error(msg: string): void;
46
+ };
47
+ }
48
+ export declare class LocalControlServer {
49
+ private readonly opts;
50
+ private server;
51
+ private readonly sockets;
52
+ constructor(opts: LocalControlServerOptions);
53
+ start(): Promise<void>;
54
+ stop(): Promise<void>;
55
+ private handleConnection;
56
+ private handleLine;
57
+ private reply;
58
+ }
59
+ //# sourceMappingURL=local-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-control.d.ts","sourceRoot":"","sources":["../src/local-control.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,yEAAyE;AACzE,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACzC,2EAA2E;IAC3E,cAAc,CACZ,MAAM,EAAE,kBAAkB,EAC1B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAWD;gFACgF;AAChF,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEnF;AA6BD,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,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,qBAAa,kBAAkB;IAIjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAEpB,IAAI,EAAE,yBAAyB;IAEtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkEtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3B,OAAO,CAAC,gBAAgB;YA0BV,UAAU;IAuDxB,OAAO,CAAC,KAAK;CAiBd"}
@@ -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;
@@ -99,6 +102,16 @@ export declare class DaemonSupervisor {
99
102
  private handleFilesystemBrowse;
100
103
  private handleBrowserProfileLifecycle;
101
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;
102
115
  private enqueueBrowserProfileOp;
103
116
  private enqueueBrowserProfileRevive;
104
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"}
@@ -11,6 +11,7 @@ import { BrowserProfilePool, ClipProcessManager, ClipProvider, HubClient, } from
11
11
  import { agentClaudeHomeFor, agentHomeDirFor, agentStateDirFor, agentWorkspaceDirFor, } from './config.js';
12
12
  import { reconcileBrowserProfiles as runBrowserProfileReconcile } from './browser-profile-reconcile.js';
13
13
  import { listDirectory } from './filesystem.js';
14
+ import { startLocalProfileControl, } from './local-profile-control.js';
14
15
  import { ensureIsolatedHome, ensureSharedCredentialLink, } from './home-isolation.js';
15
16
  import { assertAgentKey, getRuntimeAdapter } from './runtimes.js';
16
17
  import { applyRuntimeBinaryEnv } from './runtime-bin-resolver.js';
@@ -99,6 +100,11 @@ export class DaemonSupervisor {
99
100
  running = false;
100
101
  machineId = null;
101
102
  machineOrgId = null;
103
+ // machines.created_by — the machine owner. The local control socket's profile
104
+ // commands are authorized against it (requester must BE the owner); kept
105
+ // fresh via bootstrap + refreshMachineConfig. Null until bootstrap → the
106
+ // local control plane fails closed.
107
+ machineCreatedBy = null;
102
108
  machineLlmSource = 'parall';
103
109
  // Whether this machine contributes its local clips as a hub provider. The
104
110
  // org admin toggles it via PATCH /machines/{id}/provider-enabled; default true.
@@ -117,6 +123,7 @@ export class DaemonSupervisor {
117
123
  // browserProfilePool replaces the old single browserProfileManager.
118
124
  healthGate = null;
119
125
  browserProfilePool = null;
126
+ localControl = null;
120
127
  clipManager = null;
121
128
  clipProvider = null;
122
129
  clipReconcileTimer = null;
@@ -215,6 +222,9 @@ export class DaemonSupervisor {
215
222
  await this.applyClipProviderState();
216
223
  this.startClipReconcileTimer();
217
224
  }
225
+ // Same-machine control plane (design daemon-control-authorization §7.1) —
226
+ // socket lifecycle + authorization live in local-profile-control.ts.
227
+ this.localControl = await startLocalProfileControl(this.localProfileControlDeps());
218
228
  // Report daemon version + self-update capability + detected runtime CLIs
219
229
  // via heartbeat (best-effort, off the critical path — detection spawns
220
230
  // `--version` probes). Capability tracks whether a service manager
@@ -360,6 +370,10 @@ export class DaemonSupervisor {
360
370
  }
361
371
  exits.push(this.terminateChild(state));
362
372
  }
373
+ if (this.localControl) {
374
+ exits.push(this.localControl.stop());
375
+ this.localControl = null;
376
+ }
363
377
  if (this.clipProvider) {
364
378
  exits.push(this.clipProvider.disconnect());
365
379
  this.clipProvider = null;
@@ -388,6 +402,7 @@ export class DaemonSupervisor {
388
402
  const machine = await this.client.getMachineSelf();
389
403
  this.machineId = machine.id;
390
404
  this.machineOrgId = machine.org_id;
405
+ this.machineCreatedBy = machine.created_by ?? null;
391
406
  this.machineLlmSource = machine.llm_source ?? 'parall';
392
407
  this.machineProviderEnabled = machine.provider_enabled ?? true;
393
408
  this.machineClipProviderUrl = machine.clip_provider_url ?? null;
@@ -807,6 +822,28 @@ export class DaemonSupervisor {
807
822
  ? this.enqueueBrowserProfileRevive(data.profile_id, op)
808
823
  : this.enqueueBrowserProfileOp(data.profile_id, op);
809
824
  }
825
+ /**
826
+ * What the supervisor lends the local control plane: identity, pool, queues.
827
+ * Getters, not values — owner and pool both change over the daemon's life
828
+ * (bootstrap / refresh / provider state), so the handler must read what is
829
+ * true AT CALL TIME; a snapshot could authorize against a stale owner.
830
+ *
831
+ * One definition, used by both `run()` and the authorization tests, so the
832
+ * matrix under test is wired exactly like production.
833
+ */
834
+ localProfileControlDeps() {
835
+ return {
836
+ log: this.log,
837
+ machineId: () => this.machineId,
838
+ orgId: () => this.machineOrgId,
839
+ owner: () => this.machineCreatedBy,
840
+ pool: () => this.browserProfilePool,
841
+ listProfiles: () => this.client.listMachineBrowserProfiles(),
842
+ enqueueRevive: (id, op) => this.enqueueBrowserProfileRevive(id, op),
843
+ enqueueOp: (id, op) => this.enqueueBrowserProfileOp(id, op),
844
+ wipeBeforeRevive: (pool, id, resetGen, gen) => this.wipeBeforeRevive(pool, id, resetGen, gen),
845
+ };
846
+ }
810
847
  enqueueBrowserProfileOp(profileId, op) {
811
848
  const previous = this.browserProfileOpQueues.get(profileId) ?? Promise.resolve();
812
849
  const next = previous
@@ -1168,6 +1205,11 @@ export class DaemonSupervisor {
1168
1205
  async refreshMachineConfig() {
1169
1206
  try {
1170
1207
  const machine = await this.client.getMachineSelf();
1208
+ // Fail closed, exactly like bootstrap: an absent created_by means the
1209
+ // server no longer proves an owner, so keeping the previous one would let
1210
+ // a revoked user keep driving profiles over the local socket. No owner =>
1211
+ // no local control, until a refresh proves one again.
1212
+ this.machineCreatedBy = machine.created_by ?? null;
1171
1213
  const newSource = machine.llm_source ?? 'parall';
1172
1214
  if (newSource !== this.machineLlmSource) {
1173
1215
  this.log.info(`machine config refreshed: llm_source ${this.machineLlmSource} → ${newSource}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/daemon",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "Parall local agent runtime — daemon supervisor + bridge runtimes, bundled as standalone JS files",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,11 +33,11 @@
33
33
  "@aws-sdk/client-s3": "3.984.0",
34
34
  "@pinixai/bb-browser-pro": "0.15.0",
35
35
  "ws": "^8.18.0",
36
- "@parall/agent-core": "1.46.0",
37
- "@parall/sdk": "1.46.0",
38
- "@parall/claude-agent": "1.46.0",
39
- "@parall/codex-agent": "1.46.0",
40
- "@parall/openclaw-agent": "1.46.0"
36
+ "@parall/sdk": "1.48.0",
37
+ "@parall/codex-agent": "1.48.0",
38
+ "@parall/openclaw-agent": "1.48.0",
39
+ "@parall/claude-agent": "1.48.0",
40
+ "@parall/agent-core": "1.48.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.0.0",