@cotal-ai/connector-claude-code 0.115.4 → 0.116.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +1 -1
- package/dist/extension.d.ts.map +1 -1
- package/dist/hook.cjs +3 -621
- package/dist/index.js +65 -24450
- package/dist/mcp.cjs +171 -79729
- package/dist/mcp.d.cts +10 -0
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/monitor.cjs +11 -414
- package/dist/seat.cjs +3 -0
- package/dist/seat.d.ts +2 -0
- package/dist/seat.d.ts.map +1 -0
- package/package.json +4 -4
package/.mcp.json
CHANGED
package/dist/extension.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgD,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAShI;;;;;;;;;4FAS4F;AAC5F,eAAO,MAAM,oBAAoB,8/DA4EvB,CAAC;
|
|
1
|
+
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgD,KAAK,SAAS,EAAoC,MAAM,gBAAgB,CAAC;AAShI;;;;;;;;;4FAS4F;AAC5F,eAAO,MAAM,oBAAoB,8/DA4EvB,CAAC;AAiDX;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,SAiN7B,CAAC"}
|
package/dist/hook.cjs
CHANGED
|
@@ -1,621 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var import_node_net = require("node:net");
|
|
5
|
-
|
|
6
|
-
// ../../packages/core/dist/launch-material.js
|
|
7
|
-
var import_node_fs2 = require("node:fs");
|
|
8
|
-
|
|
9
|
-
// ../../packages/core/dist/secret-fs.js
|
|
10
|
-
var import_node_fs = require("node:fs");
|
|
11
|
-
var import_node_child_process = require("node:child_process");
|
|
12
|
-
var import_node_path = require("node:path");
|
|
13
|
-
var isWin = process.platform === "win32";
|
|
14
|
-
function sys32(exe) {
|
|
15
|
-
const root = process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
16
|
-
return (0, import_node_path.join)(root, "System32", exe);
|
|
17
|
-
}
|
|
18
|
-
var cachedSid;
|
|
19
|
-
function ownerSid() {
|
|
20
|
-
if (cachedSid)
|
|
21
|
-
return cachedSid;
|
|
22
|
-
const out = (0, import_node_child_process.execFileSync)(sys32("whoami.exe"), ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
|
|
23
|
-
const sid = out.trim().split(",").pop()?.replace(/^"|"$/g, "").trim();
|
|
24
|
-
if (!sid || !/^S-1-/.test(sid))
|
|
25
|
-
throw new Error(`could not determine the current user's SID from \`whoami\` (got: ${out.trim()})`);
|
|
26
|
-
cachedSid = sid;
|
|
27
|
-
return sid;
|
|
28
|
-
}
|
|
29
|
-
var BROAD_SIDS = ["S-1-1-0", "S-1-5-11", "S-1-5-32-545"];
|
|
30
|
-
function hardenPrivate(path, kind) {
|
|
31
|
-
if (!isWin) {
|
|
32
|
-
if (kind === "file")
|
|
33
|
-
(0, import_node_fs.closeSync)(openPrivate(path, import_node_fs.constants.O_RDONLY));
|
|
34
|
-
else
|
|
35
|
-
(0, import_node_fs.chmodSync)(path, 448);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const perm = kind === "dir" ? "(OI)(CI)(F)" : "(F)";
|
|
39
|
-
const removeBroad = BROAD_SIDS.flatMap((sid) => ["/remove:g", `*${sid}`]);
|
|
40
|
-
const grant = (sid) => ["/grant:r", `*${sid}:${perm}`];
|
|
41
|
-
try {
|
|
42
|
-
(0, import_node_child_process.execFileSync)(sys32("icacls.exe"), [
|
|
43
|
-
path,
|
|
44
|
-
"/inheritance:r",
|
|
45
|
-
...removeBroad,
|
|
46
|
-
...grant(ownerSid()),
|
|
47
|
-
...grant("S-1-5-18"),
|
|
48
|
-
...grant("S-1-5-32-544")
|
|
49
|
-
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
50
|
-
} catch (e) {
|
|
51
|
-
throw new Error(`failed to harden ${kind} "${path}" to private via icacls: ${e.message.trim()}. Cotal will not leave a secret with a permissive ACL - ensure the path is on an NTFS volume and %SystemRoot%\\System32\\icacls.exe is available.`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function notPrivate(path, why) {
|
|
55
|
-
const refused = new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);
|
|
56
|
-
refused.code = "EPERM";
|
|
57
|
-
return refused;
|
|
58
|
-
}
|
|
59
|
-
function openPrivate(path, flags) {
|
|
60
|
-
if (isWin) {
|
|
61
|
-
const fd2 = (0, import_node_fs.openSync)(path, flags, 384);
|
|
62
|
-
try {
|
|
63
|
-
(0, import_node_fs.fchmodSync)(fd2, 384);
|
|
64
|
-
} catch (e) {
|
|
65
|
-
(0, import_node_fs.closeSync)(fd2);
|
|
66
|
-
throw e;
|
|
67
|
-
}
|
|
68
|
-
return fd2;
|
|
69
|
-
}
|
|
70
|
-
let fd;
|
|
71
|
-
try {
|
|
72
|
-
fd = (0, import_node_fs.openSync)(path, flags | import_node_fs.constants.O_NOFOLLOW | import_node_fs.constants.O_NONBLOCK, 384);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
if (e.code === "ELOOP")
|
|
75
|
-
throw notPrivate(path, "is a symlink");
|
|
76
|
-
throw e;
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
const info = (0, import_node_fs.fstatSync)(fd);
|
|
80
|
-
if (!info.isFile())
|
|
81
|
-
throw notPrivate(path, "is not a regular file");
|
|
82
|
-
if (info.uid !== process.getuid())
|
|
83
|
-
throw notPrivate(path, `is owned by uid ${info.uid}, not this user`);
|
|
84
|
-
(0, import_node_fs.fchmodSync)(fd, 384);
|
|
85
|
-
return fd;
|
|
86
|
-
} catch (e) {
|
|
87
|
-
(0, import_node_fs.closeSync)(fd);
|
|
88
|
-
throw e;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function writeSecretFile(path, data) {
|
|
92
|
-
writePrivate(path, data, false);
|
|
93
|
-
}
|
|
94
|
-
function writeBytes(path, data, exclusive) {
|
|
95
|
-
const fd = openPrivate(path, import_node_fs.constants.O_WRONLY | import_node_fs.constants.O_CREAT | (exclusive ? import_node_fs.constants.O_EXCL : 0));
|
|
96
|
-
try {
|
|
97
|
-
(0, import_node_fs.ftruncateSync)(fd);
|
|
98
|
-
(0, import_node_fs.writeFileSync)(fd, data);
|
|
99
|
-
} finally {
|
|
100
|
-
(0, import_node_fs.closeSync)(fd);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function writePrivate(path, data, exclusive) {
|
|
104
|
-
writeBytes(path, data, exclusive);
|
|
105
|
-
if (!isWin)
|
|
106
|
-
return;
|
|
107
|
-
try {
|
|
108
|
-
hardenPrivate(path, "file");
|
|
109
|
-
} catch (e) {
|
|
110
|
-
try {
|
|
111
|
-
(0, import_node_fs.unlinkSync)(path);
|
|
112
|
-
} catch {
|
|
113
|
-
}
|
|
114
|
-
throw e;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function writeSecretFileAtomic(path, data) {
|
|
118
|
-
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
119
|
-
try {
|
|
120
|
-
writeSecretFile(tmp, data);
|
|
121
|
-
(0, import_node_fs.renameSync)(tmp, path);
|
|
122
|
-
} catch (e) {
|
|
123
|
-
try {
|
|
124
|
-
(0, import_node_fs.unlinkSync)(tmp);
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
127
|
-
throw e;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function mkSecretDir(path) {
|
|
131
|
-
(0, import_node_fs.mkdirSync)(path, { recursive: true, mode: 448 });
|
|
132
|
-
hardenPrivate(path, "dir");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ../../packages/core/dist/launch-material.js
|
|
136
|
-
var LAUNCH_MATERIAL_ENV = "COTAL_LAUNCH_MATERIAL";
|
|
137
|
-
function readLaunchMaterial(path) {
|
|
138
|
-
let raw;
|
|
139
|
-
try {
|
|
140
|
-
if (process.platform !== "win32") {
|
|
141
|
-
const mode = (0, import_node_fs2.statSync)(path).mode & 511;
|
|
142
|
-
if (mode & 63)
|
|
143
|
-
throw new Error(`is readable beyond its owner (mode ${mode.toString(8)}) - refusing to read connection material out of a file other local users can open`);
|
|
144
|
-
}
|
|
145
|
-
raw = (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
146
|
-
} catch (e) {
|
|
147
|
-
throw new Error(`launch material: cannot read ${path} (${e instanceof Error ? e.message : String(e)})`);
|
|
148
|
-
}
|
|
149
|
-
let parsed;
|
|
150
|
-
try {
|
|
151
|
-
parsed = JSON.parse(raw);
|
|
152
|
-
} catch {
|
|
153
|
-
throw new Error(`launch material: ${path} is not valid JSON`);
|
|
154
|
-
}
|
|
155
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
156
|
-
throw new Error(`launch material: ${path} must contain a JSON object`);
|
|
157
|
-
return validate(parsed, path);
|
|
158
|
-
}
|
|
159
|
-
function validate(raw, path) {
|
|
160
|
-
const material = {};
|
|
161
|
-
const str = (key) => {
|
|
162
|
-
const v = raw[key];
|
|
163
|
-
if (v === void 0)
|
|
164
|
-
return;
|
|
165
|
-
if (typeof v !== "string" || !v.trim())
|
|
166
|
-
throw new Error(`launch material: ${path} has a ${key} that is not a non-empty string`);
|
|
167
|
-
material[key] = v;
|
|
168
|
-
};
|
|
169
|
-
str("servers");
|
|
170
|
-
str("creds");
|
|
171
|
-
str("token");
|
|
172
|
-
str("controlToken");
|
|
173
|
-
if (raw.eventsRequired !== void 0) {
|
|
174
|
-
if (raw.eventsRequired !== true)
|
|
175
|
-
throw new Error(`launch material: ${path} has eventsRequired that is not exactly true`);
|
|
176
|
-
material.eventsRequired = true;
|
|
177
|
-
}
|
|
178
|
-
if (raw.userAuth !== void 0) {
|
|
179
|
-
const u = raw.userAuth;
|
|
180
|
-
if (typeof u !== "object" || u === null || Array.isArray(u))
|
|
181
|
-
throw new Error(`launch material: ${path} has a userAuth that is not an object`);
|
|
182
|
-
const { owner: owner2, actor, sentinelCredsPath, bearerCmd } = u;
|
|
183
|
-
const named = { owner: owner2, actor, sentinelCredsPath };
|
|
184
|
-
for (const [k, v] of Object.entries(named))
|
|
185
|
-
if (typeof v !== "string" || !v.trim())
|
|
186
|
-
throw new Error(`launch material: ${path} has a userAuth.${k} that is not a non-empty string`);
|
|
187
|
-
if (!Array.isArray(bearerCmd) || bearerCmd.length === 0 || !bearerCmd.every((a) => typeof a === "string" && a))
|
|
188
|
-
throw new Error(`launch material: ${path} has a userAuth.bearerCmd that is not a non-empty array of strings`);
|
|
189
|
-
material.userAuth = {
|
|
190
|
-
owner: owner2,
|
|
191
|
-
actor,
|
|
192
|
-
sentinelCredsPath,
|
|
193
|
-
bearerCmd
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
if (Object.keys(material).length === 0)
|
|
197
|
-
throw new Error(`launch material: ${path} carries nothing this reader recognises. A launch that references material and supplies none is a broken launcher, not an open-mode launch, so it is refused rather than defaulted.`);
|
|
198
|
-
return material;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ../connector-core/dist/session-env.js
|
|
202
|
-
var DIRECT_MATERIAL_VARS = [
|
|
203
|
-
"COTAL_CREDS",
|
|
204
|
-
"COTAL_SERVERS",
|
|
205
|
-
"COTAL_TOKEN",
|
|
206
|
-
"COTAL_OWNER",
|
|
207
|
-
"COTAL_ACTOR",
|
|
208
|
-
"COTAL_SENTINEL_CREDS",
|
|
209
|
-
"COTAL_BEARER_CMD",
|
|
210
|
-
"COTAL_EVENTS_REQUIRED",
|
|
211
|
-
// The control token belongs here for a reason that is not symmetry. Once a launcher-spawned seat
|
|
212
|
-
// carries a material pointer in its environment, anything that INHERITS that environment and then
|
|
213
|
-
// sets COTAL_CONTROL_TOKEN by hand has two answers for one question, and controlFromEnv would
|
|
214
|
-
// silently prefer the inherited one - handing a process the OUTER seat's control endpoint while
|
|
215
|
-
// its own explicit token sat unused. That is not hypothetical: it is what a test harness spreading
|
|
216
|
-
// `...process.env` does, and this whole change exists because that spread used to be invisible.
|
|
217
|
-
"COTAL_CONTROL_TOKEN",
|
|
218
|
-
// A join link is connection material in one string: it carries the server, the auth and the space.
|
|
219
|
-
// Left off this list, a launch with both a material file and a link resolved the conflict by
|
|
220
|
-
// precedence and said nothing, which is the same silent answer to "who is this session" that the
|
|
221
|
-
// credential pair is refused for.
|
|
222
|
-
"COTAL_LINK"
|
|
223
|
-
];
|
|
224
|
-
function readMaterial(env) {
|
|
225
|
-
const path = env[LAUNCH_MATERIAL_ENV]?.trim();
|
|
226
|
-
if (!path)
|
|
227
|
-
return void 0;
|
|
228
|
-
const direct = DIRECT_MATERIAL_VARS.filter((k) => env[k]?.trim());
|
|
229
|
-
if (direct.length)
|
|
230
|
-
throw new Error(`COTAL config: this launch carries connection material BOTH as ${LAUNCH_MATERIAL_ENV} and as ${direct.join(", ")}. One launch carries one identity plane - drop the direct variables, or drop the material file.`);
|
|
231
|
-
return readLaunchMaterial(path);
|
|
232
|
-
}
|
|
233
|
-
function controlFromEnv(env = process.env) {
|
|
234
|
-
const path = env.COTAL_CONTROL_SOCKET?.trim();
|
|
235
|
-
const token = readMaterial(env)?.controlToken ?? env.COTAL_CONTROL_TOKEN?.trim();
|
|
236
|
-
if (path && token)
|
|
237
|
-
return { path, token };
|
|
238
|
-
if (!path && !token)
|
|
239
|
-
return void 0;
|
|
240
|
-
if (path)
|
|
241
|
-
throw new Error("COTAL config: COTAL_CONTROL_SOCKET is set but no control token could be resolved - neither the launch material nor COTAL_CONTROL_TOKEN carries one. Half a pair is not a control endpoint, so this launch is refused rather than started without the control plane it was configured to have.");
|
|
242
|
-
throw new Error("COTAL config: a control token was supplied but COTAL_CONTROL_SOCKET is unset, so there is no socket to authenticate against. Half a pair is not a control endpoint, so this launch is refused rather than started without the control plane it was configured to have.");
|
|
243
|
-
}
|
|
244
|
-
function hasIdentity(env = process.env) {
|
|
245
|
-
return Boolean(env.COTAL_NAME?.trim() || env.COTAL_LINK?.trim() || env.COTAL_AGENT_FILE?.trim());
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ../connector-core/dist/control.js
|
|
249
|
-
var HANDOFF_RECEIPT = '{"handoff":"ok"}\n';
|
|
250
|
-
var MAX_FRAME_BYTES = 1 << 20;
|
|
251
|
-
|
|
252
|
-
// ../connector-core/dist/relay.js
|
|
253
|
-
var TIMEOUT_MS = 2e3;
|
|
254
|
-
var CONNECT_RETRY_INITIAL_MS = 25;
|
|
255
|
-
var CONNECT_RETRY_MAX_MS = 250;
|
|
256
|
-
var isRetryableConnectError = (error) => {
|
|
257
|
-
const code = error instanceof Error ? error.code : void 0;
|
|
258
|
-
return ["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE", "EAGAIN", "EBUSY"].includes(code ?? "");
|
|
259
|
-
};
|
|
260
|
-
var isSessionStart = (event) => event !== null && typeof event === "object" && event.hook_event_name === "SessionStart";
|
|
261
|
-
var warned = false;
|
|
262
|
-
function warnOnce(message) {
|
|
263
|
-
if (warned)
|
|
264
|
-
return;
|
|
265
|
-
warned = true;
|
|
266
|
-
try {
|
|
267
|
-
process.stderr.write(`[cotal-connector] ${message}
|
|
268
|
-
`);
|
|
269
|
-
} catch {
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
function readStdin() {
|
|
273
|
-
return new Promise((resolve) => {
|
|
274
|
-
if (process.stdin.isTTY)
|
|
275
|
-
return resolve("");
|
|
276
|
-
let d = "";
|
|
277
|
-
process.stdin.setEncoding("utf8");
|
|
278
|
-
process.stdin.on("data", (c) => d += c);
|
|
279
|
-
process.stdin.on("end", () => resolve(d));
|
|
280
|
-
process.stdin.on("error", () => resolve(d));
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
function done(out, confirm) {
|
|
284
|
-
const exit = () => process.exit(0);
|
|
285
|
-
const t = out.trim();
|
|
286
|
-
if (!t)
|
|
287
|
-
return exit();
|
|
288
|
-
process.stdout.on("error", () => exit());
|
|
289
|
-
process.stdout.write(t + "\n", (err) => {
|
|
290
|
-
if (err || !confirm)
|
|
291
|
-
return exit();
|
|
292
|
-
confirm(exit);
|
|
293
|
-
});
|
|
294
|
-
setTimeout(exit, 1e3);
|
|
295
|
-
}
|
|
296
|
-
async function runHookRelay(answer, observe) {
|
|
297
|
-
const managed = hasIdentity();
|
|
298
|
-
if (!managed && !answer && !observe)
|
|
299
|
-
return done("");
|
|
300
|
-
const raw = (await readStdin()).trim() || "{}";
|
|
301
|
-
let event = {};
|
|
302
|
-
try {
|
|
303
|
-
event = JSON.parse(raw);
|
|
304
|
-
} catch {
|
|
305
|
-
}
|
|
306
|
-
let local;
|
|
307
|
-
try {
|
|
308
|
-
local = answer?.(event, { managed });
|
|
309
|
-
} catch {
|
|
310
|
-
}
|
|
311
|
-
if (local)
|
|
312
|
-
return done(JSON.stringify(local));
|
|
313
|
-
try {
|
|
314
|
-
await observe?.(event, { managed });
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
if (!managed)
|
|
318
|
-
return done("");
|
|
319
|
-
let control;
|
|
320
|
-
try {
|
|
321
|
-
control = controlFromEnv();
|
|
322
|
-
} catch {
|
|
323
|
-
warnOnce("this session's control endpoint could not be resolved: the launch material is missing, unreadable, readable beyond its owner, malformed, or contradicted by direct COTAL_ variables. Lifecycle relays are disabled for this session, so presence will not advance and queued peer messages will not be injected. Check COTAL_LAUNCH_MATERIAL and COTAL_CONTROL_SOCKET.");
|
|
324
|
-
return done("");
|
|
325
|
-
}
|
|
326
|
-
if (!control)
|
|
327
|
-
return done("");
|
|
328
|
-
const { path, token } = control;
|
|
329
|
-
const startupCritical = isSessionStart(event);
|
|
330
|
-
let sock;
|
|
331
|
-
let settled = false;
|
|
332
|
-
let retryDelayMs = CONNECT_RETRY_INITIAL_MS;
|
|
333
|
-
const drop = (candidate = sock) => {
|
|
334
|
-
try {
|
|
335
|
-
candidate?.destroy();
|
|
336
|
-
} catch {
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
const settle = () => {
|
|
340
|
-
if (settled)
|
|
341
|
-
return false;
|
|
342
|
-
settled = true;
|
|
343
|
-
clearTimeout(timer);
|
|
344
|
-
return true;
|
|
345
|
-
};
|
|
346
|
-
const finish = (out) => {
|
|
347
|
-
if (!settle())
|
|
348
|
-
return;
|
|
349
|
-
drop();
|
|
350
|
-
done(out);
|
|
351
|
-
};
|
|
352
|
-
const finishWithReceipt = (out, candidate) => {
|
|
353
|
-
if (!settle())
|
|
354
|
-
return;
|
|
355
|
-
done(out, (then) => {
|
|
356
|
-
try {
|
|
357
|
-
candidate.write(HANDOFF_RECEIPT, () => {
|
|
358
|
-
drop(candidate);
|
|
359
|
-
then();
|
|
360
|
-
});
|
|
361
|
-
} catch {
|
|
362
|
-
then();
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
};
|
|
366
|
-
const timer = setTimeout(() => finish(""), TIMEOUT_MS);
|
|
367
|
-
const retryLater = () => {
|
|
368
|
-
const wait = retryDelayMs;
|
|
369
|
-
retryDelayMs = Math.min(retryDelayMs * 2, CONNECT_RETRY_MAX_MS);
|
|
370
|
-
setTimeout(dial, wait);
|
|
371
|
-
};
|
|
372
|
-
const dial = () => {
|
|
373
|
-
if (settled)
|
|
374
|
-
return;
|
|
375
|
-
const candidate = (0, import_node_net.connect)(path);
|
|
376
|
-
sock = candidate;
|
|
377
|
-
let connected = false;
|
|
378
|
-
let reply = "";
|
|
379
|
-
candidate.setEncoding("utf8");
|
|
380
|
-
candidate.on("connect", () => {
|
|
381
|
-
if (settled || sock !== candidate)
|
|
382
|
-
return;
|
|
383
|
-
connected = true;
|
|
384
|
-
try {
|
|
385
|
-
candidate.write(JSON.stringify({ token, event, handoff: true }) + "\n");
|
|
386
|
-
} catch {
|
|
387
|
-
finish("");
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
candidate.on("data", (d) => {
|
|
391
|
-
if (settled || sock !== candidate)
|
|
392
|
-
return;
|
|
393
|
-
reply += d;
|
|
394
|
-
const nl = reply.indexOf("\n");
|
|
395
|
-
if (nl >= 0)
|
|
396
|
-
finishWithReceipt(reply.slice(0, nl), candidate);
|
|
397
|
-
});
|
|
398
|
-
candidate.on("error", (error) => {
|
|
399
|
-
if (settled || sock !== candidate)
|
|
400
|
-
return;
|
|
401
|
-
if (!connected && startupCritical && isRetryableConnectError(error)) {
|
|
402
|
-
drop(candidate);
|
|
403
|
-
retryLater();
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
finish("");
|
|
407
|
-
});
|
|
408
|
-
candidate.on("end", () => {
|
|
409
|
-
if (settled || sock !== candidate)
|
|
410
|
-
return;
|
|
411
|
-
finish(reply);
|
|
412
|
-
});
|
|
413
|
-
};
|
|
414
|
-
dial();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// src/one-path.ts
|
|
418
|
-
var LOCAL_SERVERS = /* @__PURE__ */ new Set(["cotal", "plugin_cotal_cotal"]);
|
|
419
|
-
var COTAL_TOOL = /^mcp__(.+)__(cotal_[a-z0-9]+(?:_[a-z0-9]+)*)$/;
|
|
420
|
-
function hostedCotalTool(toolName) {
|
|
421
|
-
const m = typeof toolName === "string" ? COTAL_TOOL.exec(toolName) : null;
|
|
422
|
-
return m && !LOCAL_SERVERS.has(m[1]) ? m[2] : void 0;
|
|
423
|
-
}
|
|
424
|
-
var ONE_PATH_REASON = "This session is already on a Cotal mesh through its local Cotal plugin, and a session uses one Cotal path. Use the local cotal_* tools (mcp__cotal__...). To use the hosted Cotal connector, start a Claude Code session that Cotal did not launch.";
|
|
425
|
-
var otherCotalPath = (event, session) => {
|
|
426
|
-
if (!session.managed) return void 0;
|
|
427
|
-
const ev = event;
|
|
428
|
-
if (ev?.hook_event_name !== "PreToolUse" || hostedCotalTool(ev.tool_name) === void 0) return void 0;
|
|
429
|
-
return {
|
|
430
|
-
hookSpecificOutput: {
|
|
431
|
-
hookEventName: "PreToolUse",
|
|
432
|
-
permissionDecision: "deny",
|
|
433
|
-
permissionDecisionReason: ONE_PATH_REASON
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// src/connection-owner.ts
|
|
439
|
-
var import_node_fs3 = require("node:fs");
|
|
440
|
-
var import_node_path2 = require("node:path");
|
|
441
|
-
var CONNECTED = /^connection_id: (\S+)$/m;
|
|
442
|
-
var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
443
|
-
var FORKED_REASON = "This connection_id was opened by another Claude Code session. This session is a fork or a copy of that one, so it needs its own connection: call cotal_connect, then use the connection_id it returns.";
|
|
444
|
-
function connectedId(response) {
|
|
445
|
-
const blocks = Array.isArray(response) ? response : [];
|
|
446
|
-
for (const b of blocks) {
|
|
447
|
-
const text = b?.text;
|
|
448
|
-
const id = typeof text === "string" ? CONNECTED.exec(text)?.[1] : void 0;
|
|
449
|
-
if (id !== void 0) return SAFE_ID.test(id) ? id : void 0;
|
|
450
|
-
}
|
|
451
|
-
return void 0;
|
|
452
|
-
}
|
|
453
|
-
function connectionOwner(dataDir) {
|
|
454
|
-
const dir = dataDir ? (0, import_node_path2.join)(dataDir, "hosted-connections") : void 0;
|
|
455
|
-
return (event, session) => {
|
|
456
|
-
if (session.managed || !dir) return void 0;
|
|
457
|
-
const ev = event;
|
|
458
|
-
const tool = hostedCotalTool(ev?.tool_name);
|
|
459
|
-
const sessionId = ev?.session_id;
|
|
460
|
-
if (tool === void 0 || typeof sessionId !== "string" || !sessionId) return void 0;
|
|
461
|
-
const given = ev?.tool_input?.connection_id;
|
|
462
|
-
const id = typeof given === "string" && SAFE_ID.test(given) ? given : void 0;
|
|
463
|
-
const owner2 = () => {
|
|
464
|
-
try {
|
|
465
|
-
return (0, import_node_fs3.readFileSync)((0, import_node_path2.join)(dir, id), "utf8");
|
|
466
|
-
} catch {
|
|
467
|
-
return void 0;
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
if (ev?.hook_event_name === "PostToolUse" && tool === "cotal_connect") {
|
|
471
|
-
const opened = connectedId(ev.tool_response);
|
|
472
|
-
if (opened === void 0) return void 0;
|
|
473
|
-
(0, import_node_fs3.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
474
|
-
(0, import_node_fs3.writeFileSync)((0, import_node_path2.join)(dir, opened), sessionId, { mode: 384 });
|
|
475
|
-
return void 0;
|
|
476
|
-
}
|
|
477
|
-
if (id === void 0) return void 0;
|
|
478
|
-
if (ev?.hook_event_name === "PostToolUse" && tool === "cotal_disconnect") {
|
|
479
|
-
if (owner2() === sessionId) (0, import_node_fs3.rmSync)((0, import_node_path2.join)(dir, id), { force: true });
|
|
480
|
-
return void 0;
|
|
481
|
-
}
|
|
482
|
-
if (ev?.hook_event_name !== "PreToolUse") return void 0;
|
|
483
|
-
const recorded = owner2();
|
|
484
|
-
if (recorded === void 0 || recorded === sessionId) return void 0;
|
|
485
|
-
return {
|
|
486
|
-
hookSpecificOutput: {
|
|
487
|
-
hookEventName: "PreToolUse",
|
|
488
|
-
permissionDecision: "deny",
|
|
489
|
-
permissionDecisionReason: FORKED_REASON
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// src/native-binding.ts
|
|
496
|
-
var import_node_fs5 = require("node:fs");
|
|
497
|
-
var import_node_net2 = require("node:net");
|
|
498
|
-
var import_node_os2 = require("node:os");
|
|
499
|
-
var import_node_path4 = require("node:path");
|
|
500
|
-
|
|
501
|
-
// src/wake-feed.ts
|
|
502
|
-
var import_node_fs4 = require("node:fs");
|
|
503
|
-
var import_node_os = require("node:os");
|
|
504
|
-
var import_node_path3 = require("node:path");
|
|
505
|
-
var WAKE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
506
|
-
var WIN_PIPES = "\\\\.\\pipe\\";
|
|
507
|
-
var WIN_PREFIX = "cotal-claude-wake-";
|
|
508
|
-
function wakeDir() {
|
|
509
|
-
if (process.platform === "win32") return WIN_PIPES;
|
|
510
|
-
const uid = process.getuid();
|
|
511
|
-
const dir = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), `cotal-claude-wake-${uid}`);
|
|
512
|
-
(0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
513
|
-
const st = (0, import_node_fs4.lstatSync)(dir);
|
|
514
|
-
if (!st.isDirectory() || st.uid !== uid || (st.mode & 63) !== 0) throw new Error(`${dir} is not a private directory of this user's`);
|
|
515
|
-
return dir;
|
|
516
|
-
}
|
|
517
|
-
var entryName = (claudePid, serverPid) => process.platform === "win32" ? `${WIN_PREFIX}${claudePid}-${serverPid}` : `${claudePid}-${serverPid}.sock`;
|
|
518
|
-
var ENTRY = process.platform === "win32" ? new RegExp(`^${WIN_PREFIX}(\\d+)-(\\d+)$`) : /^(\d+)-(\d+)\.sock$/;
|
|
519
|
-
function alive(pid) {
|
|
520
|
-
try {
|
|
521
|
-
process.kill(pid, 0);
|
|
522
|
-
return true;
|
|
523
|
-
} catch (e) {
|
|
524
|
-
return e.code === "EPERM";
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function wakeSocketPath(claudePid, serverPid = process.pid) {
|
|
528
|
-
const dir = wakeDir();
|
|
529
|
-
return process.platform === "win32" ? `${dir}${entryName(claudePid, serverPid)}` : (0, import_node_path3.join)(dir, entryName(claudePid, serverPid));
|
|
530
|
-
}
|
|
531
|
-
function wakeSockets(claudePid) {
|
|
532
|
-
const dir = wakeDir();
|
|
533
|
-
return (0, import_node_fs4.readdirSync)(dir).flatMap((name) => {
|
|
534
|
-
const m = ENTRY.exec(name);
|
|
535
|
-
return m?.[1] === String(claudePid) && alive(Number(m[2])) ? [wakeSocketPath(claudePid, Number(m[2]))] : [];
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
function nativeSocketPath(feed) {
|
|
539
|
-
return `${feed}.native`;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// src/native-binding.ts
|
|
543
|
-
function stateFile(session) {
|
|
544
|
-
if (!WAKE_ID.test(session)) throw new Error("invalid native session identity");
|
|
545
|
-
const root = (0, import_node_path4.join)((0, import_node_os2.tmpdir)(), `cotal-claude-native-${process.getuid?.() ?? "local"}`);
|
|
546
|
-
const st = (0, import_node_fs5.lstatSync)(root, { throwIfNoEntry: false });
|
|
547
|
-
if (st && (!st.isDirectory() || process.getuid && st.uid !== process.getuid()))
|
|
548
|
-
throw new Error("native session state is not this user's private directory");
|
|
549
|
-
mkSecretDir(root);
|
|
550
|
-
return (0, import_node_path4.join)(root, `${session}.json`);
|
|
551
|
-
}
|
|
552
|
-
function readRecord(file) {
|
|
553
|
-
const st = (0, import_node_fs5.lstatSync)(file, { throwIfNoEntry: false });
|
|
554
|
-
if (!st) return void 0;
|
|
555
|
-
if (!st.isFile() || st.size > 16384 || process.getuid && (st.uid !== process.getuid() || (st.mode & 63) !== 0))
|
|
556
|
-
throw new Error("native session state is not an owner-private bounded regular file");
|
|
557
|
-
const value = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
|
|
558
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid native session state");
|
|
559
|
-
return value;
|
|
560
|
-
}
|
|
561
|
-
function transcriptMatches(session, path) {
|
|
562
|
-
return typeof path === "string" && (0, import_node_path4.isAbsolute)(path) && (0, import_node_path4.basename)(path) === `${session}.jsonl`;
|
|
563
|
-
}
|
|
564
|
-
async function forward(event, hostPid) {
|
|
565
|
-
const paths = wakeSockets(hostPid);
|
|
566
|
-
if (!paths.length) throw new Error("no local native lifecycle server");
|
|
567
|
-
await Promise.all(paths.map((path) => new Promise((resolve, reject) => {
|
|
568
|
-
const socket = (0, import_node_net2.createConnection)(nativeSocketPath(path));
|
|
569
|
-
let reply = "";
|
|
570
|
-
socket.setEncoding("utf8");
|
|
571
|
-
socket.setTimeout(2e3, () => socket.destroy(new Error("native lifecycle relay timed out")));
|
|
572
|
-
socket.on("connect", () => socket.write(`${JSON.stringify({ op: "native", event })}
|
|
573
|
-
`));
|
|
574
|
-
socket.on("data", (data) => {
|
|
575
|
-
reply += data;
|
|
576
|
-
if (reply.length > 128) socket.destroy();
|
|
577
|
-
});
|
|
578
|
-
socket.on("error", reject);
|
|
579
|
-
socket.on("close", () => {
|
|
580
|
-
try {
|
|
581
|
-
if (JSON.parse(reply).ok === true) resolve();
|
|
582
|
-
else reject(new Error("native lifecycle binding refused"));
|
|
583
|
-
} catch {
|
|
584
|
-
reject(new Error("native lifecycle binding was not confirmed"));
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
})));
|
|
588
|
-
}
|
|
589
|
-
async function observePersonalHook(value) {
|
|
590
|
-
if (!value || typeof value !== "object") return;
|
|
591
|
-
const event = value;
|
|
592
|
-
if (event.agent_id !== void 0 || typeof event.session_id !== "string" || !WAKE_ID.test(event.session_id) || !transcriptMatches(event.session_id, event.transcript_path) || typeof event.hook_event_name !== "string") return;
|
|
593
|
-
if (!["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolBatch", "Notification", "Stop", "StopFailure", "SessionEnd"].includes(event.hook_event_name)) return;
|
|
594
|
-
const file = stateFile(event.session_id);
|
|
595
|
-
const previous = readRecord(file);
|
|
596
|
-
if (previous && previous.transcript_path !== event.transcript_path)
|
|
597
|
-
throw new Error("native command hook changed its session transcript");
|
|
598
|
-
const safe = {};
|
|
599
|
-
for (const key of ["hook_event_name", "session_id", "transcript_path", "cwd", "source", "notification_type", "error", "model"])
|
|
600
|
-
if (typeof event[key] === "string" && event[key].length <= 4096) safe[key] = event[key];
|
|
601
|
-
if (JSON.stringify(safe).length > 8192) throw new Error("native command-hook metadata is too large");
|
|
602
|
-
writeSecretFileAtomic(file, JSON.stringify(safe));
|
|
603
|
-
const target = readRecord(`${file}.host`);
|
|
604
|
-
if (target) {
|
|
605
|
-
if (!Number.isInteger(target.hostPid) || target.hostPid <= 0) throw new Error("invalid native lifecycle host");
|
|
606
|
-
await forward(safe, target.hostPid);
|
|
607
|
-
}
|
|
608
|
-
if (event.hook_event_name === "SessionEnd") {
|
|
609
|
-
(0, import_node_fs5.unlinkSync)(file);
|
|
610
|
-
if (target) (0, import_node_fs5.unlinkSync)(`${file}.host`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// src/hook.ts
|
|
615
|
-
var owner = connectionOwner(process.env.CLAUDE_PLUGIN_DATA);
|
|
616
|
-
void runHookRelay(
|
|
617
|
-
(event, session) => otherCotalPath(event, session) ?? owner(event, session),
|
|
618
|
-
async (event, session) => {
|
|
619
|
-
if (!session.managed) await observePersonalHook(event);
|
|
620
|
-
}
|
|
621
|
-
).catch(() => process.exit(0));
|
|
1
|
+
"use strict";var import_node_net=require("node:net");var import_node_fs2=require("node:fs");var import_node_fs=require("node:fs");var import_node_child_process=require("node:child_process");var import_node_path=require("node:path");var isWin=process.platform==="win32";function sys32(exe){const root=process.env.SystemRoot||process.env.windir||"C:\\Windows";return(0,import_node_path.join)(root,"System32",exe)}var cachedSid;function ownerSid(){if(cachedSid)return cachedSid;const out=(0,import_node_child_process.execFileSync)(sys32("whoami.exe"),["/user","/fo","csv","/nh"],{encoding:"utf8"});const sid=out.trim().split(",").pop()?.replace(/^"|"$/g,"").trim();if(!sid||!/^S-1-/.test(sid))throw new Error(`could not determine the current user's SID from \`whoami\` (got: ${out.trim()})`);cachedSid=sid;return sid}var BROAD_SIDS=["S-1-1-0","S-1-5-11","S-1-5-32-545"];function hardenPrivate(path,kind){if(!isWin){if(kind==="file")(0,import_node_fs.closeSync)(openPrivate(path,import_node_fs.constants.O_RDONLY));else(0,import_node_fs.chmodSync)(path,448);return}const perm=kind==="dir"?"(OI)(CI)(F)":"(F)";const removeBroad=BROAD_SIDS.flatMap(sid=>["/remove:g",`*${sid}`]);const grant=sid=>["/grant:r",`*${sid}:${perm}`];try{(0,import_node_child_process.execFileSync)(sys32("icacls.exe"),[path,"/inheritance:r",...removeBroad,...grant(ownerSid()),...grant("S-1-5-18"),...grant("S-1-5-32-544")],{stdio:["ignore","ignore","pipe"]})}catch(e){throw new Error(`failed to harden ${kind} "${path}" to private via icacls: ${e.message.trim()}. Cotal will not leave a secret with a permissive ACL - ensure the path is on an NTFS volume and %SystemRoot%\\System32\\icacls.exe is available.`)}}function notPrivate(path,why){const refused=new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);refused.code="EPERM";return refused}function openPrivate(path,flags){if(isWin){const fd2=(0,import_node_fs.openSync)(path,flags,384);try{(0,import_node_fs.fchmodSync)(fd2,384)}catch(e){(0,import_node_fs.closeSync)(fd2);throw e}return fd2}let fd;try{fd=(0,import_node_fs.openSync)(path,flags|import_node_fs.constants.O_NOFOLLOW|import_node_fs.constants.O_NONBLOCK,384)}catch(e){if(e.code==="ELOOP")throw notPrivate(path,"is a symlink");throw e}try{const info=(0,import_node_fs.fstatSync)(fd);if(!info.isFile())throw notPrivate(path,"is not a regular file");if(info.uid!==process.getuid())throw notPrivate(path,`is owned by uid ${info.uid}, not this user`);(0,import_node_fs.fchmodSync)(fd,384);return fd}catch(e){(0,import_node_fs.closeSync)(fd);throw e}}function writeSecretFile(path,data){writePrivate(path,data,false)}function writeBytes(path,data,exclusive){const fd=openPrivate(path,import_node_fs.constants.O_WRONLY|import_node_fs.constants.O_CREAT|(exclusive?import_node_fs.constants.O_EXCL:0));try{(0,import_node_fs.ftruncateSync)(fd);(0,import_node_fs.writeFileSync)(fd,data)}finally{(0,import_node_fs.closeSync)(fd)}}function writePrivate(path,data,exclusive){writeBytes(path,data,exclusive);if(!isWin)return;try{hardenPrivate(path,"file")}catch(e){try{(0,import_node_fs.unlinkSync)(path)}catch{}throw e}}function writeSecretFileAtomic(path,data){const tmp=`${path}.${process.pid}.${Date.now()}.tmp`;try{writeSecretFile(tmp,data);(0,import_node_fs.renameSync)(tmp,path)}catch(e){try{(0,import_node_fs.unlinkSync)(tmp)}catch{}throw e}}function mkSecretDir(path){(0,import_node_fs.mkdirSync)(path,{recursive:true,mode:448});hardenPrivate(path,"dir")}var LAUNCH_MATERIAL_ENV="COTAL_LAUNCH_MATERIAL";function readLaunchMaterial(path){let raw;try{if(process.platform!=="win32"){const mode=(0,import_node_fs2.statSync)(path).mode&511;if(mode&63)throw new Error(`is readable beyond its owner (mode ${mode.toString(8)}) - refusing to read connection material out of a file other local users can open`)}raw=(0,import_node_fs2.readFileSync)(path,"utf8")}catch(e){throw new Error(`launch material: cannot read ${path} (${e instanceof Error?e.message:String(e)})`)}let parsed;try{parsed=JSON.parse(raw)}catch{throw new Error(`launch material: ${path} is not valid JSON`)}if(typeof parsed!=="object"||parsed===null||Array.isArray(parsed))throw new Error(`launch material: ${path} must contain a JSON object`);return validate(parsed,path)}function validate(raw,path){const material={};const str=key=>{const v=raw[key];if(v===void 0)return;if(typeof v!=="string"||!v.trim())throw new Error(`launch material: ${path} has a ${key} that is not a non-empty string`);material[key]=v};str("servers");str("creds");str("token");str("controlToken");if(raw.eventsRequired!==void 0){if(raw.eventsRequired!==true)throw new Error(`launch material: ${path} has eventsRequired that is not exactly true`);material.eventsRequired=true}if(raw.userAuth!==void 0){const u=raw.userAuth;if(typeof u!=="object"||u===null||Array.isArray(u))throw new Error(`launch material: ${path} has a userAuth that is not an object`);const{owner:owner2,actor,sentinelCredsPath,bearerCmd}=u;const named={owner:owner2,actor,sentinelCredsPath};for(const[k,v]of Object.entries(named))if(typeof v!=="string"||!v.trim())throw new Error(`launch material: ${path} has a userAuth.${k} that is not a non-empty string`);if(!Array.isArray(bearerCmd)||bearerCmd.length===0||!bearerCmd.every(a=>typeof a==="string"&&a))throw new Error(`launch material: ${path} has a userAuth.bearerCmd that is not a non-empty array of strings`);material.userAuth={owner:owner2,actor,sentinelCredsPath,bearerCmd}}if(Object.keys(material).length===0)throw new Error(`launch material: ${path} carries nothing this reader recognises. A launch that references material and supplies none is a broken launcher, not an open-mode launch, so it is refused rather than defaulted.`);return material}var DIRECT_MATERIAL_VARS=["COTAL_CREDS","COTAL_SERVERS","COTAL_TOKEN","COTAL_OWNER","COTAL_ACTOR","COTAL_SENTINEL_CREDS","COTAL_BEARER_CMD","COTAL_EVENTS_REQUIRED","COTAL_CONTROL_TOKEN","COTAL_LINK"];function readMaterial(env){const path=env[LAUNCH_MATERIAL_ENV]?.trim();if(!path)return void 0;const direct=DIRECT_MATERIAL_VARS.filter(k=>env[k]?.trim());if(direct.length)throw new Error(`COTAL config: this launch carries connection material BOTH as ${LAUNCH_MATERIAL_ENV} and as ${direct.join(", ")}. One launch carries one identity plane - drop the direct variables, or drop the material file.`);return readLaunchMaterial(path)}function controlFromEnv(env=process.env){const path=env.COTAL_CONTROL_SOCKET?.trim();const token=readMaterial(env)?.controlToken??env.COTAL_CONTROL_TOKEN?.trim();if(path&&token)return{path,token};if(!path&&!token)return void 0;if(path)throw new Error("COTAL config: COTAL_CONTROL_SOCKET is set but no control token could be resolved - neither the launch material nor COTAL_CONTROL_TOKEN carries one. Half a pair is not a control endpoint, so this launch is refused rather than started without the control plane it was configured to have.");throw new Error("COTAL config: a control token was supplied but COTAL_CONTROL_SOCKET is unset, so there is no socket to authenticate against. Half a pair is not a control endpoint, so this launch is refused rather than started without the control plane it was configured to have.")}function hasIdentity(env=process.env){return Boolean(env.COTAL_NAME?.trim()||env.COTAL_LINK?.trim()||env.COTAL_AGENT_FILE?.trim())}var HANDOFF_RECEIPT='{"handoff":"ok"}\n';var MAX_FRAME_BYTES=1<<20;var TIMEOUT_MS=2e3;var CONNECT_RETRY_INITIAL_MS=25;var CONNECT_RETRY_MAX_MS=250;var isRetryableConnectError=error=>{const code=error instanceof Error?error.code:void 0;return["ENOENT","ECONNREFUSED","ECONNRESET","EPIPE","EAGAIN","EBUSY"].includes(code??"")};var isSessionStart=event=>event!==null&&typeof event==="object"&&event.hook_event_name==="SessionStart";var warned=false;function warnOnce(message){if(warned)return;warned=true;try{process.stderr.write(`[cotal-connector] ${message}
|
|
2
|
+
`)}catch{}}function readStdin(){return new Promise(resolve=>{if(process.stdin.isTTY)return resolve("");let d="";process.stdin.setEncoding("utf8");process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>resolve(d));process.stdin.on("error",()=>resolve(d))})}function done(out,confirm){const exit=()=>process.exit(0);const t=out.trim();if(!t)return exit();process.stdout.on("error",()=>exit());process.stdout.write(t+"\n",err=>{if(err||!confirm)return exit();confirm(exit)});setTimeout(exit,1e3)}async function runHookRelay(answer,observe){const managed=hasIdentity();if(!managed&&!answer&&!observe)return done("");const raw=(await readStdin()).trim()||"{}";let event={};try{event=JSON.parse(raw)}catch{}let local;try{local=answer?.(event,{managed})}catch{}if(local)return done(JSON.stringify(local));try{await observe?.(event,{managed})}catch{}if(!managed)return done("");let control;try{control=controlFromEnv()}catch{warnOnce("this session's control endpoint could not be resolved: the launch material is missing, unreadable, readable beyond its owner, malformed, or contradicted by direct COTAL_ variables. Lifecycle relays are disabled for this session, so presence will not advance and queued peer messages will not be injected. Check COTAL_LAUNCH_MATERIAL and COTAL_CONTROL_SOCKET.");return done("")}if(!control)return done("");const{path,token}=control;const startupCritical=isSessionStart(event);let sock;let settled=false;let retryDelayMs=CONNECT_RETRY_INITIAL_MS;const drop=(candidate=sock)=>{try{candidate?.destroy()}catch{}};const settle=()=>{if(settled)return false;settled=true;clearTimeout(timer);return true};const finish=out=>{if(!settle())return;drop();done(out)};const finishWithReceipt=(out,candidate)=>{if(!settle())return;done(out,then=>{try{candidate.write(HANDOFF_RECEIPT,()=>{drop(candidate);then()})}catch{then()}})};const timer=setTimeout(()=>finish(""),TIMEOUT_MS);const retryLater=()=>{const wait=retryDelayMs;retryDelayMs=Math.min(retryDelayMs*2,CONNECT_RETRY_MAX_MS);setTimeout(dial,wait)};const dial=()=>{if(settled)return;const candidate=(0,import_node_net.connect)(path);sock=candidate;let connected=false;let reply="";candidate.setEncoding("utf8");candidate.on("connect",()=>{if(settled||sock!==candidate)return;connected=true;try{candidate.write(JSON.stringify({token,event,handoff:true})+"\n")}catch{finish("")}});candidate.on("data",d=>{if(settled||sock!==candidate)return;reply+=d;const nl=reply.indexOf("\n");if(nl>=0)finishWithReceipt(reply.slice(0,nl),candidate)});candidate.on("error",error=>{if(settled||sock!==candidate)return;if(!connected&&startupCritical&&isRetryableConnectError(error)){drop(candidate);retryLater();return}finish("")});candidate.on("end",()=>{if(settled||sock!==candidate)return;finish(reply)})};dial()}var LOCAL_SERVERS=new Set(["cotal","plugin_cotal_cotal"]);var COTAL_TOOL=/^mcp__(.+)__(cotal_[a-z0-9]+(?:_[a-z0-9]+)*)$/;function hostedCotalTool(toolName){const m=typeof toolName==="string"?COTAL_TOOL.exec(toolName):null;return m&&!LOCAL_SERVERS.has(m[1])?m[2]:void 0}var ONE_PATH_REASON="This session is already on a Cotal mesh through its local Cotal plugin, and a session uses one Cotal path. Use the local cotal_* tools (mcp__cotal__...). To use the hosted Cotal connector, start a Claude Code session that Cotal did not launch.";var otherCotalPath=(event,session)=>{if(!session.managed)return void 0;const ev=event;if(ev?.hook_event_name!=="PreToolUse"||hostedCotalTool(ev.tool_name)===void 0)return void 0;return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:ONE_PATH_REASON}}};var import_node_fs3=require("node:fs");var import_node_path2=require("node:path");var CONNECTED=/^connection_id: (\S+)$/m;var SAFE_ID=/^[A-Za-z0-9_-]{1,128}$/;var FORKED_REASON="This connection_id was opened by another Claude Code session. This session is a fork or a copy of that one, so it needs its own connection: call cotal_connect, then use the connection_id it returns.";function connectedId(response){const blocks=Array.isArray(response)?response:[];for(const b of blocks){const text=b?.text;const id=typeof text==="string"?CONNECTED.exec(text)?.[1]:void 0;if(id!==void 0)return SAFE_ID.test(id)?id:void 0}return void 0}function connectionOwner(dataDir){const dir=dataDir?(0,import_node_path2.join)(dataDir,"hosted-connections"):void 0;return(event,session)=>{if(session.managed||!dir)return void 0;const ev=event;const tool=hostedCotalTool(ev?.tool_name);const sessionId=ev?.session_id;if(tool===void 0||typeof sessionId!=="string"||!sessionId)return void 0;const given=ev?.tool_input?.connection_id;const id=typeof given==="string"&&SAFE_ID.test(given)?given:void 0;const owner2=()=>{try{return(0,import_node_fs3.readFileSync)((0,import_node_path2.join)(dir,id),"utf8")}catch{return void 0}};if(ev?.hook_event_name==="PostToolUse"&&tool==="cotal_connect"){const opened=connectedId(ev.tool_response);if(opened===void 0)return void 0;(0,import_node_fs3.mkdirSync)(dir,{recursive:true,mode:448});(0,import_node_fs3.writeFileSync)((0,import_node_path2.join)(dir,opened),sessionId,{mode:384});return void 0}if(id===void 0)return void 0;if(ev?.hook_event_name==="PostToolUse"&&tool==="cotal_disconnect"){if(owner2()===sessionId)(0,import_node_fs3.rmSync)((0,import_node_path2.join)(dir,id),{force:true});return void 0}if(ev?.hook_event_name!=="PreToolUse")return void 0;const recorded=owner2();if(recorded===void 0||recorded===sessionId)return void 0;return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:FORKED_REASON}}}}var import_node_fs5=require("node:fs");var import_node_net2=require("node:net");var import_node_os2=require("node:os");var import_node_path4=require("node:path");var import_node_fs4=require("node:fs");var import_node_os=require("node:os");var import_node_path3=require("node:path");var WAKE_ID=/^[A-Za-z0-9_-]{1,128}$/;var WIN_PIPES="\\\\.\\pipe\\";var WIN_PREFIX="cotal-claude-wake-";function wakeDir(){if(process.platform==="win32")return WIN_PIPES;const uid=process.getuid();const dir=(0,import_node_path3.join)((0,import_node_os.tmpdir)(),`cotal-claude-wake-${uid}`);(0,import_node_fs4.mkdirSync)(dir,{recursive:true,mode:448});const st=(0,import_node_fs4.lstatSync)(dir);if(!st.isDirectory()||st.uid!==uid||(st.mode&63)!==0)throw new Error(`${dir} is not a private directory of this user's`);return dir}var entryName=(claudePid,serverPid)=>process.platform==="win32"?`${WIN_PREFIX}${claudePid}-${serverPid}`:`${claudePid}-${serverPid}.sock`;var ENTRY=process.platform==="win32"?new RegExp(`^${WIN_PREFIX}(\\d+)-(\\d+)$`):/^(\d+)-(\d+)\.sock$/;function alive(pid){try{process.kill(pid,0);return true}catch(e){return e.code==="EPERM"}}function wakeSocketPath(claudePid,serverPid=process.pid){const dir=wakeDir();return process.platform==="win32"?`${dir}${entryName(claudePid,serverPid)}`:(0,import_node_path3.join)(dir,entryName(claudePid,serverPid))}function wakeSockets(claudePid){const dir=wakeDir();return(0,import_node_fs4.readdirSync)(dir).flatMap(name=>{const m=ENTRY.exec(name);return m?.[1]===String(claudePid)&&alive(Number(m[2]))?[wakeSocketPath(claudePid,Number(m[2]))]:[]})}function nativeSocketPath(feed){return`${feed}.native`}function stateFile(session){if(!WAKE_ID.test(session))throw new Error("invalid native session identity");const root=(0,import_node_path4.join)((0,import_node_os2.tmpdir)(),`cotal-claude-native-${process.getuid?.()??"local"}`);const st=(0,import_node_fs5.lstatSync)(root,{throwIfNoEntry:false});if(st&&(!st.isDirectory()||process.getuid&&st.uid!==process.getuid()))throw new Error("native session state is not this user's private directory");mkSecretDir(root);return(0,import_node_path4.join)(root,`${session}.json`)}function readRecord(file){const st=(0,import_node_fs5.lstatSync)(file,{throwIfNoEntry:false});if(!st)return void 0;if(!st.isFile()||st.size>16384||process.getuid&&(st.uid!==process.getuid()||(st.mode&63)!==0))throw new Error("native session state is not an owner-private bounded regular file");const value=JSON.parse((0,import_node_fs5.readFileSync)(file,"utf8"));if(!value||typeof value!=="object"||Array.isArray(value))throw new Error("invalid native session state");return value}function transcriptMatches(session,path){return typeof path==="string"&&(0,import_node_path4.isAbsolute)(path)&&(0,import_node_path4.basename)(path)===`${session}.jsonl`}async function forward(event,hostPid){const paths=wakeSockets(hostPid);if(!paths.length)throw new Error("no local native lifecycle server");await Promise.all(paths.map(path=>new Promise((resolve,reject)=>{const socket=(0,import_node_net2.createConnection)(nativeSocketPath(path));let reply="";socket.setEncoding("utf8");socket.setTimeout(2e3,()=>socket.destroy(new Error("native lifecycle relay timed out")));socket.on("connect",()=>socket.write(`${JSON.stringify({op:"native",event})}
|
|
3
|
+
`));socket.on("data",data=>{reply+=data;if(reply.length>128)socket.destroy()});socket.on("error",reject);socket.on("close",()=>{try{if(JSON.parse(reply).ok===true)resolve();else reject(new Error("native lifecycle binding refused"))}catch{reject(new Error("native lifecycle binding was not confirmed"))}})})))}async function observePersonalHook(value){if(!value||typeof value!=="object")return;const event=value;if(event.agent_id!==void 0||typeof event.session_id!=="string"||!WAKE_ID.test(event.session_id)||!transcriptMatches(event.session_id,event.transcript_path)||typeof event.hook_event_name!=="string")return;if(!["SessionStart","UserPromptSubmit","PreToolUse","PostToolBatch","Notification","Stop","StopFailure","SessionEnd"].includes(event.hook_event_name))return;const file=stateFile(event.session_id);const previous=readRecord(file);if(previous&&previous.transcript_path!==event.transcript_path)throw new Error("native command hook changed its session transcript");const safe={};for(const key of["hook_event_name","session_id","transcript_path","cwd","source","notification_type","error","model"])if(typeof event[key]==="string"&&event[key].length<=4096)safe[key]=event[key];if(JSON.stringify(safe).length>8192)throw new Error("native command-hook metadata is too large");writeSecretFileAtomic(file,JSON.stringify(safe));const target=readRecord(`${file}.host`);if(target){if(!Number.isInteger(target.hostPid)||target.hostPid<=0)throw new Error("invalid native lifecycle host");await forward(safe,target.hostPid)}if(event.hook_event_name==="SessionEnd"){(0,import_node_fs5.unlinkSync)(file);if(target)(0,import_node_fs5.unlinkSync)(`${file}.host`)}}var owner=connectionOwner(process.env.CLAUDE_PLUGIN_DATA);void runHookRelay((event,session)=>otherCotalPath(event,session)??owner(event,session),async(event,session)=>{if(!session.managed)await observePersonalHook(event)}).catch(()=>process.exit(0));
|