@cueloop/daemon 0.1.0-alpha.9 → 0.1.0-alpha.90
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/package.json +39 -22
- package/src/api.test.ts +1060 -65
- package/src/api.ts +1398 -89
- package/src/capabilities.test.ts +57 -0
- package/src/capabilities.ts +89 -0
- package/src/client.test.ts +24 -0
- package/src/client.ts +669 -86
- package/src/curate.ts +109 -0
- package/src/daemon-version.test.ts +142 -0
- package/src/diff-hot-reload.test.ts +419 -0
- package/src/diff-watcher.ts +293 -0
- package/src/gh.test.ts +59 -0
- package/src/gh.ts +74 -0
- package/src/ghostty-thread-surface-store.test.ts +49 -0
- package/src/ghostty-thread-surface-store.ts +101 -0
- package/src/ghostty-thread-surface.test.ts +398 -0
- package/src/ghostty-thread-surface.ts +267 -0
- package/src/harness-delivery.test.ts +268 -0
- package/src/harness-state-store.test.ts +93 -0
- package/src/harness-state-store.ts +213 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +92 -0
- package/src/herdr-thread-surface-store.test.ts +81 -0
- package/src/herdr-thread-surface-store.ts +64 -0
- package/src/herdr-thread-surface.test.ts +411 -0
- package/src/herdr-thread-surface.ts +355 -0
- package/src/index.ts +8 -9
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +66 -0
- package/src/jj-working-tree.ts +131 -0
- package/src/main.ts +4 -1
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +102 -0
- package/src/pr-poller.ts +73 -0
- package/src/project-files.test.ts +124 -0
- package/src/project-files.ts +56 -0
- package/src/protocol.ts +79 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +123 -0
- package/src/server.test.ts +453 -42
- package/src/server.ts +433 -88
- package/src/share-blob.test.ts +112 -0
- package/src/share-blob.ts +55 -0
- package/src/single-instance.test.ts +162 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +159 -0
- package/src/store.ts +264 -34
- package/src/testing/store-conformance.ts +194 -0
- package/src/thread-review.test.ts +483 -0
- package/src/thread-review.ts +398 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +282 -37
- package/src/validate.ts +398 -33
- package/src/vcs-source.test.ts +180 -0
- package/src/vcs-source.ts +228 -0
- package/src/version.ts +6 -0
- package/src/working-tree.ts +149 -0
- package/src/review.test.ts +0 -167
- package/src/review.ts +0 -174
package/src/server.ts
CHANGED
|
@@ -1,26 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The socket server: binds DaemonCore's methods to the unix socket, owns the
|
|
3
|
-
* pidfile, stale-socket cleanup, and the idle-exit timer
|
|
3
|
+
* pidfile, stale-socket cleanup, and the idle-exit timer. File
|
|
4
4
|
* permissions are the local auth: the socket and state dir are 0700/0600.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
chmodSync,
|
|
9
|
+
closeSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
fstatSync,
|
|
12
|
+
linkSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
openSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
statSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
8
20
|
import { DaemonCore, type DaemonEvent } from "./api";
|
|
9
21
|
import { DaemonError } from "./errors";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
22
|
+
import { DEFAULT_ROLE, roleAllowsMethod, type DaemonRole } from "./capabilities";
|
|
23
|
+
import { isKnownMethod, parseParams, type MethodName } from "./validate";
|
|
24
|
+
import {
|
|
25
|
+
BackpressureWriter,
|
|
26
|
+
LineBuffer,
|
|
27
|
+
parseRequestFrame,
|
|
28
|
+
type Request,
|
|
29
|
+
type Response,
|
|
30
|
+
} from "./protocol";
|
|
31
|
+
import { cueloopHome, lockPath, ownerTokenPath, pidPath, socketPath } from "./paths";
|
|
32
|
+
import { DAEMON_VERSION } from "./version";
|
|
33
|
+
import { randomBytes } from "node:crypto";
|
|
13
34
|
|
|
14
|
-
interface
|
|
35
|
+
interface Connection {
|
|
36
|
+
/** The author a non-owner connection acts as, bound in the handshake. */
|
|
37
|
+
author?: string;
|
|
15
38
|
write(data: string): void;
|
|
16
39
|
subscribed: boolean;
|
|
40
|
+
/** Capability role for this connection; the owner until a daemon.hello caps it. */
|
|
41
|
+
role: DaemonRole;
|
|
17
42
|
}
|
|
18
43
|
|
|
44
|
+
type MethodHandler = (connection: Connection, request: Request) => Response["result"];
|
|
45
|
+
|
|
19
46
|
export interface DaemonOptions {
|
|
20
47
|
home?: string;
|
|
21
48
|
/** Idle-exit delay; 0 disables (tests, foreground runs). */
|
|
22
49
|
idleExitMs?: number;
|
|
23
50
|
onIdleExit?: () => void;
|
|
51
|
+
/** The version the ping handshake reports; defaults to this build. Tests override it to pose as a stale daemon. */
|
|
52
|
+
version?: string;
|
|
24
53
|
}
|
|
25
54
|
|
|
26
55
|
/**
|
|
@@ -29,24 +58,29 @@ export interface DaemonOptions {
|
|
|
29
58
|
* so in-process ownership is tracked separately.
|
|
30
59
|
*/
|
|
31
60
|
const HELD_HOMES = new Set<string>();
|
|
61
|
+
const LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
32
62
|
|
|
33
63
|
export class DaemonServer {
|
|
34
64
|
readonly core: DaemonCore;
|
|
35
65
|
readonly home: string;
|
|
36
|
-
private
|
|
66
|
+
private connections = new Set<Connection>();
|
|
37
67
|
private server: ReturnType<typeof Bun.listen> | null = null;
|
|
38
68
|
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
39
69
|
private lockFd: number | null = null;
|
|
70
|
+
/** The secret that proves ownership; minted per daemon run, readable by the home's user only. */
|
|
71
|
+
private ownerToken = "";
|
|
40
72
|
private readonly idleExitMs: number;
|
|
41
73
|
private readonly onIdleExit: () => void;
|
|
74
|
+
private readonly version: string;
|
|
42
75
|
|
|
43
|
-
constructor(
|
|
44
|
-
this.home =
|
|
45
|
-
this.idleExitMs =
|
|
46
|
-
this.onIdleExit =
|
|
76
|
+
constructor(options: DaemonOptions = {}) {
|
|
77
|
+
this.home = options.home ?? cueloopHome();
|
|
78
|
+
this.idleExitMs = options.idleExitMs ?? 15 * 60 * 1000;
|
|
79
|
+
this.onIdleExit = options.onIdleExit ?? (() => process.exit(0));
|
|
80
|
+
this.version = options.version ?? DAEMON_VERSION;
|
|
47
81
|
mkdirSync(this.home, { recursive: true, mode: 0o700 });
|
|
48
82
|
this.core = new DaemonCore(this.home);
|
|
49
|
-
this.core.onEvent((
|
|
83
|
+
this.core.onEvent((event) => this.broadcast(event));
|
|
50
84
|
}
|
|
51
85
|
|
|
52
86
|
/**
|
|
@@ -59,18 +93,37 @@ export class DaemonServer {
|
|
|
59
93
|
private acquireLock(): boolean {
|
|
60
94
|
if (HELD_HOMES.has(this.home)) return false; // another instance here owns it
|
|
61
95
|
const path = lockPath(this.home);
|
|
96
|
+
|
|
62
97
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
98
|
+
const preparedPath = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
99
|
+
const fd = openSync(preparedPath, "wx", 0o600);
|
|
100
|
+
|
|
63
101
|
try {
|
|
64
|
-
//
|
|
65
|
-
|
|
102
|
+
// Publish the complete pid atomically. A competing daemon must never
|
|
103
|
+
// mistake a newly created, still-empty lock for a crashed owner.
|
|
66
104
|
writeFileSync(fd, String(process.pid));
|
|
105
|
+
linkSync(preparedPath, path);
|
|
67
106
|
this.lockFd = fd;
|
|
68
107
|
HELD_HOMES.add(this.home);
|
|
108
|
+
|
|
69
109
|
return true;
|
|
70
|
-
} catch {
|
|
110
|
+
} catch (error) {
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
113
|
+
|
|
71
114
|
let ownerPid = 0;
|
|
115
|
+
|
|
72
116
|
try {
|
|
73
|
-
|
|
117
|
+
let ownerText = readFileSync(path, "utf8").trim();
|
|
118
|
+
|
|
119
|
+
// Older daemon builds published the lock before writing their pid.
|
|
120
|
+
// Give that live owner time to finish instead of stealing its socket.
|
|
121
|
+
for (let probe = 0; !ownerText && probe < 25; probe++) {
|
|
122
|
+
Atomics.wait(LOCK_WAIT, 0, 0, 20);
|
|
123
|
+
ownerText = readFileSync(path, "utf8").trim();
|
|
124
|
+
}
|
|
125
|
+
if (!ownerText && Date.now() - statSync(path).mtimeMs < 5_000) return false;
|
|
126
|
+
ownerPid = Number(ownerText);
|
|
74
127
|
} catch {
|
|
75
128
|
// the owner vanished between open and read; retry
|
|
76
129
|
continue;
|
|
@@ -78,6 +131,7 @@ export class DaemonServer {
|
|
|
78
131
|
if (ownerPid && ownerPid !== process.pid) {
|
|
79
132
|
try {
|
|
80
133
|
process.kill(ownerPid, 0); // throws when the pid is gone
|
|
134
|
+
|
|
81
135
|
return false; // a live daemon owns this home
|
|
82
136
|
} catch {
|
|
83
137
|
// stale lock from a crashed daemon
|
|
@@ -90,14 +144,23 @@ export class DaemonServer {
|
|
|
90
144
|
} catch {
|
|
91
145
|
return false;
|
|
92
146
|
}
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(preparedPath, { force: true });
|
|
93
149
|
}
|
|
94
150
|
}
|
|
151
|
+
|
|
95
152
|
return false;
|
|
96
153
|
}
|
|
97
154
|
|
|
98
155
|
private releaseLock(): void {
|
|
99
156
|
HELD_HOMES.delete(this.home);
|
|
100
157
|
if (this.lockFd !== null) {
|
|
158
|
+
try {
|
|
159
|
+
if (statSync(lockPath(this.home)).ino === fstatSync(this.lockFd).ino)
|
|
160
|
+
rmSync(lockPath(this.home), { force: true });
|
|
161
|
+
} catch {
|
|
162
|
+
// the lock was already removed or replaced
|
|
163
|
+
}
|
|
101
164
|
try {
|
|
102
165
|
closeSync(this.lockFd);
|
|
103
166
|
} catch {
|
|
@@ -105,7 +168,6 @@ export class DaemonServer {
|
|
|
105
168
|
}
|
|
106
169
|
this.lockFd = null;
|
|
107
170
|
}
|
|
108
|
-
rmSync(lockPath(this.home), { force: true });
|
|
109
171
|
}
|
|
110
172
|
|
|
111
173
|
/**
|
|
@@ -115,27 +177,47 @@ export class DaemonServer {
|
|
|
115
177
|
start(): string | null {
|
|
116
178
|
if (!this.acquireLock()) return null;
|
|
117
179
|
const path = socketPath(this.home);
|
|
180
|
+
|
|
118
181
|
// safe now: holding the lock means no live daemon owns this home, so any
|
|
119
182
|
// socket file left behind is stale
|
|
120
183
|
if (existsSync(path)) rmSync(path, { force: true });
|
|
121
|
-
|
|
122
|
-
|
|
184
|
+
// the token exists before the socket does: the first client to connect
|
|
185
|
+
// must be able to prove ownership
|
|
186
|
+
this.ownerToken = randomBytes(32).toString("hex");
|
|
187
|
+
writeFileSync(ownerTokenPath(this.home), this.ownerToken, { mode: 0o600 });
|
|
188
|
+
chmodSync(ownerTokenPath(this.home), 0o600);
|
|
189
|
+
this.server = Bun.listen<{
|
|
190
|
+
buffer: LineBuffer;
|
|
191
|
+
connection: Connection;
|
|
192
|
+
writer: BackpressureWriter;
|
|
193
|
+
}>({
|
|
123
194
|
unix: path,
|
|
124
195
|
socket: {
|
|
125
|
-
open(socket) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
196
|
+
open: (socket) => {
|
|
197
|
+
const writer = new BackpressureWriter(socket);
|
|
198
|
+
const connection: Connection = {
|
|
199
|
+
write: (data) => writer.write(data),
|
|
200
|
+
subscribed: false,
|
|
201
|
+
role: DEFAULT_ROLE,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
socket.data = { buffer: new LineBuffer(), connection, writer };
|
|
205
|
+
this.connections.add(connection);
|
|
206
|
+
this.scheduleIdleCheck();
|
|
130
207
|
},
|
|
131
|
-
data(socket, data) {
|
|
208
|
+
data: (socket, data) => {
|
|
132
209
|
socket.data.buffer.push(data.toString(), (line) => {
|
|
133
|
-
void
|
|
210
|
+
void this.respondToRequestLine(socket.data.connection, line);
|
|
134
211
|
});
|
|
135
212
|
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
213
|
+
drain(socket) {
|
|
214
|
+
// drain can fire before open has attached data on some platforms;
|
|
215
|
+
// a throw here would take the whole daemon down with the socket
|
|
216
|
+
socket.data?.writer.drain();
|
|
217
|
+
},
|
|
218
|
+
close: (socket) => {
|
|
219
|
+
this.connections.delete(socket.data.connection);
|
|
220
|
+
this.scheduleIdleCheck();
|
|
139
221
|
},
|
|
140
222
|
error() {},
|
|
141
223
|
},
|
|
@@ -143,21 +225,26 @@ export class DaemonServer {
|
|
|
143
225
|
chmodSync(path, 0o600);
|
|
144
226
|
writeFileSync(pidPath(this.home), String(process.pid));
|
|
145
227
|
this.scheduleIdleCheck();
|
|
228
|
+
|
|
146
229
|
return path;
|
|
147
230
|
}
|
|
148
231
|
|
|
149
232
|
stop(): void {
|
|
233
|
+
this.core.dispose();
|
|
234
|
+
if (this.lockFd === null) return;
|
|
150
235
|
this.server?.stop(true);
|
|
151
236
|
this.server = null;
|
|
152
237
|
rmSync(socketPath(this.home), { force: true });
|
|
153
238
|
rmSync(pidPath(this.home), { force: true });
|
|
239
|
+
rmSync(ownerTokenPath(this.home), { force: true });
|
|
154
240
|
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
155
241
|
this.releaseLock();
|
|
156
242
|
}
|
|
157
243
|
|
|
158
|
-
private broadcast(
|
|
159
|
-
const frame = JSON.stringify(
|
|
160
|
-
|
|
244
|
+
private broadcast(event: DaemonEvent): void {
|
|
245
|
+
const frame = JSON.stringify(event) + "\n";
|
|
246
|
+
|
|
247
|
+
for (const connection of this.connections) if (connection.subscribed) connection.write(frame);
|
|
161
248
|
this.scheduleIdleCheck();
|
|
162
249
|
}
|
|
163
250
|
|
|
@@ -166,7 +253,7 @@ export class DaemonServer {
|
|
|
166
253
|
if (this.idleExitMs <= 0) return;
|
|
167
254
|
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
168
255
|
this.idleTimer = setTimeout(() => {
|
|
169
|
-
if (this.
|
|
256
|
+
if (this.connections.size === 0 && !this.core.hasPendingSessions()) {
|
|
170
257
|
this.stop();
|
|
171
258
|
this.onIdleExit();
|
|
172
259
|
} else {
|
|
@@ -175,76 +262,334 @@ export class DaemonServer {
|
|
|
175
262
|
}, this.idleExitMs);
|
|
176
263
|
}
|
|
177
264
|
|
|
178
|
-
private async
|
|
179
|
-
let
|
|
265
|
+
private async respondToRequestLine(connection: Connection, line: string): Promise<void> {
|
|
266
|
+
let request: Request;
|
|
267
|
+
|
|
180
268
|
try {
|
|
181
|
-
|
|
269
|
+
request = parseRequestFrame(line);
|
|
182
270
|
} catch {
|
|
183
|
-
|
|
271
|
+
connection.write(
|
|
272
|
+
JSON.stringify({ id: -1, error: { code: "bad_json", message: "unparseable request" } }) +
|
|
273
|
+
"\n",
|
|
274
|
+
);
|
|
275
|
+
|
|
184
276
|
return;
|
|
185
277
|
}
|
|
186
278
|
try {
|
|
187
|
-
const result = await this.dispatch(
|
|
188
|
-
|
|
279
|
+
const result = await this.dispatch(connection, request);
|
|
280
|
+
|
|
281
|
+
connection.write(JSON.stringify({ id: request.id, result }) + "\n");
|
|
189
282
|
} catch (err) {
|
|
190
283
|
const code = err instanceof DaemonError ? err.code : "internal";
|
|
191
284
|
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
-
|
|
285
|
+
|
|
286
|
+
connection.write(JSON.stringify({ id: request.id, error: { code, message } }) + "\n");
|
|
193
287
|
}
|
|
194
288
|
}
|
|
195
289
|
|
|
196
|
-
private
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
case "daemon.shutdown":
|
|
206
|
-
setTimeout(() => {
|
|
207
|
-
this.stop();
|
|
208
|
-
this.onIdleExit();
|
|
209
|
-
}, 10);
|
|
210
|
-
return {};
|
|
211
|
-
case "events.subscribe":
|
|
212
|
-
conn.subscribed = true;
|
|
213
|
-
return {};
|
|
214
|
-
case "session.create": {
|
|
215
|
-
const p = parseParams("session.create", req.params);
|
|
216
|
-
return core.sessionCreate({ workspace: p.workspace, artifact: p.artifact });
|
|
217
|
-
}
|
|
218
|
-
case "session.get":
|
|
219
|
-
return core.sessionGet(parseParams("session.get", req.params).id);
|
|
220
|
-
case "session.list":
|
|
221
|
-
return core.sessionList(parseParams("session.list", req.params).filter);
|
|
222
|
-
case "session.wait": {
|
|
223
|
-
const p = parseParams("session.wait", req.params);
|
|
224
|
-
return core.sessionWait(p.id, p.timeoutMs);
|
|
225
|
-
}
|
|
226
|
-
case "session.annotate": {
|
|
227
|
-
const p = parseParams("session.annotate", req.params);
|
|
228
|
-
return core.sessionAnnotate(p.id, p.annotation);
|
|
229
|
-
}
|
|
230
|
-
case "session.removeAnnotation": {
|
|
231
|
-
const p = parseParams("session.removeAnnotation", req.params);
|
|
232
|
-
return core.sessionRemoveAnnotation(p.id, p.annotationId);
|
|
290
|
+
private readonly handlers: Record<MethodName, MethodHandler> = {
|
|
291
|
+
"daemon.ping": () => ({ pid: process.pid, version: this.version }),
|
|
292
|
+
"daemon.hello": (connection, request) => {
|
|
293
|
+
const params = parseParams("daemon.hello", request.params);
|
|
294
|
+
|
|
295
|
+
// ownership is proven, never declared: the token lives in the home
|
|
296
|
+
// directory the daemon serves, readable by its user alone
|
|
297
|
+
if (params.role === "owner" && params.token !== this.ownerToken) {
|
|
298
|
+
throw new DaemonError("forbidden", "owner token required");
|
|
233
299
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
300
|
+
if (params.clientVersion !== this.version) {
|
|
301
|
+
throw new DaemonError(
|
|
302
|
+
"version_mismatch",
|
|
303
|
+
`daemon is version ${this.version}, but this client is ${params.clientVersion ?? "unknown"}; align cueloop versions before restarting the daemon`,
|
|
304
|
+
);
|
|
237
305
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
306
|
+
|
|
307
|
+
connection.role = params.role;
|
|
308
|
+
// identity is bound once, here; a non-owner never names it per call
|
|
309
|
+
if (params.role !== "owner" && params.author !== undefined) connection.author = params.author;
|
|
310
|
+
|
|
311
|
+
return {};
|
|
312
|
+
},
|
|
313
|
+
"daemon.shutdown": () => {
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
this.stop();
|
|
316
|
+
this.onIdleExit();
|
|
317
|
+
}, 10);
|
|
318
|
+
|
|
319
|
+
return {};
|
|
320
|
+
},
|
|
321
|
+
"events.subscribe": (connection) => {
|
|
322
|
+
connection.subscribed = true;
|
|
323
|
+
|
|
324
|
+
return {};
|
|
325
|
+
},
|
|
326
|
+
"session.create": (_connection, request) => {
|
|
327
|
+
const params = parseParams("session.create", request.params);
|
|
328
|
+
|
|
329
|
+
return this.core.sessionCreate({ workspace: params.workspace, artifact: params.artifact });
|
|
330
|
+
},
|
|
331
|
+
"session.get": (_connection, request) =>
|
|
332
|
+
this.core.sessionGet(parseParams("session.get", request.params).id),
|
|
333
|
+
"session.list": (_connection, request) =>
|
|
334
|
+
this.core.sessionList(parseParams("session.list", request.params).filter),
|
|
335
|
+
"session.wait": (_connection, request) => {
|
|
336
|
+
const params = parseParams("session.wait", request.params);
|
|
337
|
+
|
|
338
|
+
return this.core.sessionWait(params.id, params.timeoutMs);
|
|
339
|
+
},
|
|
340
|
+
"session.annotate": (_connection, request) => {
|
|
341
|
+
const params = parseParams("session.annotate", request.params);
|
|
342
|
+
|
|
343
|
+
return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
|
|
344
|
+
},
|
|
345
|
+
"session.comment": (_connection, request) => {
|
|
346
|
+
const params = parseParams("session.comment", request.params);
|
|
347
|
+
|
|
348
|
+
return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
|
|
349
|
+
},
|
|
350
|
+
"session.removeAnnotation": (connection, request) => {
|
|
351
|
+
const params = parseParams("session.removeAnnotation", request.params);
|
|
352
|
+
|
|
353
|
+
// the owner removes any comment; a non-owner only the ones of the author it is bound to
|
|
354
|
+
if (connection.role !== "owner" && connection.author === undefined) {
|
|
355
|
+
throw new DaemonError(
|
|
356
|
+
"forbidden",
|
|
357
|
+
"a non-owner connection removes comments as its bound author",
|
|
358
|
+
);
|
|
241
359
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
360
|
+
|
|
361
|
+
return this.core.sessionRemoveAnnotation(
|
|
362
|
+
params.id,
|
|
363
|
+
params.annotationId,
|
|
364
|
+
connection.role === "owner" ? undefined : connection.author,
|
|
365
|
+
);
|
|
366
|
+
},
|
|
367
|
+
"session.setParticipantName": (connection, request) => {
|
|
368
|
+
const params = parseParams("session.setParticipantName", request.params);
|
|
369
|
+
|
|
370
|
+
// the owner names anyone; a non-owner names only the author it is bound to
|
|
371
|
+
if (connection.role !== "owner" && params.author !== connection.author) {
|
|
372
|
+
throw new DaemonError("forbidden", "a non-owner connection names only its bound author");
|
|
245
373
|
}
|
|
246
|
-
|
|
247
|
-
|
|
374
|
+
|
|
375
|
+
return this.core.sessionSetParticipantName(params.id, params.author, params.name);
|
|
376
|
+
},
|
|
377
|
+
"session.setWorkingCopy": (_connection, request) => {
|
|
378
|
+
const params = parseParams("session.setWorkingCopy", request.params);
|
|
379
|
+
|
|
380
|
+
return this.core.sessionSetWorkingCopy(params.id, params.workingCopy, params.textCuts);
|
|
381
|
+
},
|
|
382
|
+
"session.cutBlock": (_connection, request) => {
|
|
383
|
+
const params = parseParams("session.cutBlock", request.params);
|
|
384
|
+
|
|
385
|
+
return this.core.sessionCutBlock(params.id, params.blockIndex);
|
|
386
|
+
},
|
|
387
|
+
"session.navigate": (_connection, request) => {
|
|
388
|
+
const params = parseParams("session.navigate", request.params);
|
|
389
|
+
|
|
390
|
+
return this.core.sessionNavigate(params.id, params.entryId, params.summary, params.branch);
|
|
391
|
+
},
|
|
392
|
+
"session.branch": (_connection, request) => {
|
|
393
|
+
const params = parseParams("session.branch", request.params);
|
|
394
|
+
|
|
395
|
+
return this.core.sessionBranch(params.id, params.name);
|
|
396
|
+
},
|
|
397
|
+
"session.switch": (_connection, request) => {
|
|
398
|
+
const params = parseParams("session.switch", request.params);
|
|
399
|
+
|
|
400
|
+
return this.core.sessionSwitch(params.id, params.branch);
|
|
401
|
+
},
|
|
402
|
+
"session.label": (_connection, request) => {
|
|
403
|
+
const params = parseParams("session.label", request.params);
|
|
404
|
+
|
|
405
|
+
return this.core.sessionLabel(params.id, params.label);
|
|
406
|
+
},
|
|
407
|
+
"session.fork": (_connection, request) => {
|
|
408
|
+
const params = parseParams("session.fork", request.params);
|
|
409
|
+
|
|
410
|
+
return this.core.sessionFork(params.id);
|
|
411
|
+
},
|
|
412
|
+
"session.restoreBlock": (_connection, request) => {
|
|
413
|
+
const params = parseParams("session.restoreBlock", request.params);
|
|
414
|
+
|
|
415
|
+
return this.core.sessionRestoreBlock(params.id, params.baseBlockIndex, params.line);
|
|
416
|
+
},
|
|
417
|
+
"session.curate": (_connection, request) => {
|
|
418
|
+
const params = parseParams("session.curate", request.params);
|
|
419
|
+
|
|
420
|
+
return this.core.sessionCurate(params.id, params.rejections);
|
|
421
|
+
},
|
|
422
|
+
"session.setAccess": (_connection, request) => {
|
|
423
|
+
const params = parseParams("session.setAccess", request.params);
|
|
424
|
+
|
|
425
|
+
return this.core.sessionSetAccess(params.id, params.githubLogins);
|
|
426
|
+
},
|
|
427
|
+
"session.setViewed": (_connection, request) => {
|
|
428
|
+
const params = parseParams("session.setViewed", request.params);
|
|
429
|
+
|
|
430
|
+
return this.core.sessionSetViewed(params.id, params.viewedPaths);
|
|
431
|
+
},
|
|
432
|
+
"session.setTitle": (_connection, request) => {
|
|
433
|
+
const params = parseParams("session.setTitle", request.params);
|
|
434
|
+
|
|
435
|
+
return this.core.sessionSetTitle(params.id, params.title);
|
|
436
|
+
},
|
|
437
|
+
"session.projectFiles": (_connection, request) =>
|
|
438
|
+
this.core.projectFiles(parseParams("session.projectFiles", request.params).id),
|
|
439
|
+
"session.fileContents": (_connection, request) => {
|
|
440
|
+
const params = parseParams("session.fileContents", request.params);
|
|
441
|
+
|
|
442
|
+
return this.core.fileContents(params.id, params.path);
|
|
443
|
+
},
|
|
444
|
+
"repo.files": (_connection, request) =>
|
|
445
|
+
this.core.repoFiles(parseParams("repo.files", request.params).cwd),
|
|
446
|
+
"repo.fileContents": (_connection, request) => {
|
|
447
|
+
const params = parseParams("repo.fileContents", request.params);
|
|
448
|
+
|
|
449
|
+
return this.core.repoFileContents(params.cwd, params.path);
|
|
450
|
+
},
|
|
451
|
+
"repo.changes": (_connection, request) =>
|
|
452
|
+
this.core.repoChanges(parseParams("repo.changes", request.params).cwd),
|
|
453
|
+
"repo.diff": (_connection, request) => {
|
|
454
|
+
const params = parseParams("repo.diff", request.params);
|
|
455
|
+
|
|
456
|
+
return this.core.repoDiff(params.cwd, params.vcs);
|
|
457
|
+
},
|
|
458
|
+
"session.workbench": (_connection, request) =>
|
|
459
|
+
this.core.workbenchSession(parseParams("session.workbench", request.params).cwd),
|
|
460
|
+
"session.refreshDiff": (_connection, request) => {
|
|
461
|
+
const params = parseParams("session.refreshDiff", request.params);
|
|
462
|
+
|
|
463
|
+
return this.core.sessionRefreshDiff(params.id);
|
|
464
|
+
},
|
|
465
|
+
"session.setShareId": (_connection, request) => {
|
|
466
|
+
const params = parseParams("session.setShareId", request.params);
|
|
467
|
+
|
|
468
|
+
return this.core.sessionSetShareId(params.id, params.shareId);
|
|
469
|
+
},
|
|
470
|
+
"session.setShares": (_connection, request) => {
|
|
471
|
+
const params = parseParams("session.setShares", request.params);
|
|
472
|
+
|
|
473
|
+
return this.core.sessionSetShares(params.id, params.shares);
|
|
474
|
+
},
|
|
475
|
+
"session.delete": (_connection, request) => {
|
|
476
|
+
this.core.sessionDelete(parseParams("session.delete", request.params).id);
|
|
477
|
+
|
|
478
|
+
return {};
|
|
479
|
+
},
|
|
480
|
+
"session.mergeShared": (_connection, request) => {
|
|
481
|
+
const params = parseParams("session.mergeShared", request.params);
|
|
482
|
+
|
|
483
|
+
return this.core.sessionMergeShared(params.id, {
|
|
484
|
+
annotations: params.annotations,
|
|
485
|
+
participants: params.participants,
|
|
486
|
+
removals: params.removals,
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
"session.sendMessage": (_connection, request) => {
|
|
490
|
+
const params = parseParams("session.sendMessage", request.params);
|
|
491
|
+
|
|
492
|
+
return this.core.sessionSendMessage(
|
|
493
|
+
params.id,
|
|
494
|
+
params.outcome,
|
|
495
|
+
params.summary,
|
|
496
|
+
params.actionBodies,
|
|
497
|
+
);
|
|
498
|
+
},
|
|
499
|
+
"harness.bind": (_connection, request) => {
|
|
500
|
+
const params = parseParams("harness.bind", request.params);
|
|
501
|
+
|
|
502
|
+
return this.core.harnessBind(params);
|
|
503
|
+
},
|
|
504
|
+
"harness.getBinding": (_connection, request) => {
|
|
505
|
+
const params = parseParams("harness.getBinding", request.params);
|
|
506
|
+
|
|
507
|
+
return this.core.harnessGetBinding(params.bindingId);
|
|
508
|
+
},
|
|
509
|
+
"harness.bindingsForSession": (_connection, request) => {
|
|
510
|
+
const params = parseParams("harness.bindingsForSession", request.params);
|
|
511
|
+
|
|
512
|
+
return this.core.harnessBindingsForSession(params.harness, params.harnessSessionId);
|
|
513
|
+
},
|
|
514
|
+
"harness.consumeApprovedRetry": (_connection, request) => {
|
|
515
|
+
const params = parseParams("harness.consumeApprovedRetry", request.params);
|
|
516
|
+
|
|
517
|
+
return this.core.harnessConsumeApprovedRetry(
|
|
518
|
+
params.bindingId,
|
|
519
|
+
params.messageId,
|
|
520
|
+
params.content,
|
|
521
|
+
);
|
|
522
|
+
},
|
|
523
|
+
"delivery.pending": (_connection, request) => {
|
|
524
|
+
const params = parseParams("delivery.pending", request.params);
|
|
525
|
+
|
|
526
|
+
return this.core.deliveryPending(params.bindingId);
|
|
527
|
+
},
|
|
528
|
+
"delivery.acknowledge": (_connection, request) => {
|
|
529
|
+
const params = parseParams("delivery.acknowledge", request.params);
|
|
530
|
+
|
|
531
|
+
return this.core.deliveryAcknowledge(params.deliveryId);
|
|
532
|
+
},
|
|
533
|
+
"session.submitRevision": (_connection, request) => {
|
|
534
|
+
const params = parseParams("session.submitRevision", request.params);
|
|
535
|
+
|
|
536
|
+
return this.core.sessionSubmitRevision(
|
|
537
|
+
params.id,
|
|
538
|
+
params.content,
|
|
539
|
+
params.addressedAnnotationIds,
|
|
540
|
+
params.files,
|
|
541
|
+
params.source,
|
|
542
|
+
);
|
|
543
|
+
},
|
|
544
|
+
"herdr.getThreadSurface": (_connection, request) =>
|
|
545
|
+
this.core.herdrGetThreadSurface(parseParams("herdr.getThreadSurface", request.params).id),
|
|
546
|
+
"herdr.setThreadSurface": (_connection, request) => {
|
|
547
|
+
const params = parseParams("herdr.setThreadSurface", request.params);
|
|
548
|
+
|
|
549
|
+
this.core.herdrSetThreadSurface(params.id, {
|
|
550
|
+
tabId: params.tabId,
|
|
551
|
+
paneId: params.paneId,
|
|
552
|
+
mode: params.mode,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return {};
|
|
556
|
+
},
|
|
557
|
+
"ghostty.getThreadSurface": (_connection, request) =>
|
|
558
|
+
this.core.ghosttyGetThreadSurface(parseParams("ghostty.getThreadSurface", request.params).id),
|
|
559
|
+
"ghostty.setThreadSurface": (_connection, request) => {
|
|
560
|
+
const params = parseParams("ghostty.setThreadSurface", request.params);
|
|
561
|
+
|
|
562
|
+
this.core.ghosttySetThreadSurface(params.id, { terminalId: params.terminalId });
|
|
563
|
+
|
|
564
|
+
return {};
|
|
565
|
+
},
|
|
566
|
+
"ghostty.claimThreadSurface": (_connection, request) =>
|
|
567
|
+
this.core.ghosttyClaimThreadSurface(
|
|
568
|
+
parseParams("ghostty.claimThreadSurface", request.params).id,
|
|
569
|
+
),
|
|
570
|
+
"ghostty.releaseThreadSurface": (_connection, request) => {
|
|
571
|
+
this.core.ghosttyReleaseThreadSurface(
|
|
572
|
+
parseParams("ghostty.releaseThreadSurface", request.params).id,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
return {};
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
private async dispatch(connection: Connection, request: Request): Promise<Response["result"]> {
|
|
580
|
+
// The wire is untrusted JSON: validate before DaemonCore sees anything.
|
|
581
|
+
if (!isKnownMethod(request.method)) {
|
|
582
|
+
throw new DaemonError("unknown_method", `unknown method ${request.method}`);
|
|
583
|
+
}
|
|
584
|
+
// Capability gate: a capped role (a review-side agent) cannot escalate past
|
|
585
|
+
// read + annotate, whatever primitive it sends.
|
|
586
|
+
if (!roleAllowsMethod(connection.role, request.method)) {
|
|
587
|
+
throw new DaemonError("forbidden", `role ${connection.role} cannot call ${request.method}`);
|
|
248
588
|
}
|
|
589
|
+
if (!Object.hasOwn(this.handlers, request.method)) {
|
|
590
|
+
throw new DaemonError("unknown_method", `unknown method ${request.method}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return this.handlers[request.method](connection, request);
|
|
249
594
|
}
|
|
250
595
|
}
|