quadwork 1.2.2 → 1.2.3
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/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +1 -1
- package/out/__next._full.txt +1 -1
- package/out/__next._head.txt +1 -1
- package/out/__next._index.txt +1 -1
- package/out/__next._tree.txt +1 -1
- package/out/_not-found/__next._full.txt +1 -1
- package/out/_not-found/__next._head.txt +1 -1
- package/out/_not-found/__next._index.txt +1 -1
- package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/out/_not-found/__next._not-found.txt +1 -1
- package/out/_not-found/__next._tree.txt +1 -1
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +1 -1
- package/out/app-shell/__next._full.txt +1 -1
- package/out/app-shell/__next._head.txt +1 -1
- package/out/app-shell/__next._index.txt +1 -1
- package/out/app-shell/__next._tree.txt +1 -1
- package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
- package/out/app-shell/__next.app-shell.txt +1 -1
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +1 -1
- package/out/index.html +1 -1
- package/out/index.txt +1 -1
- package/out/project/_/__next._full.txt +1 -1
- package/out/project/_/__next._head.txt +1 -1
- package/out/project/_/__next._index.txt +1 -1
- package/out/project/_/__next._tree.txt +1 -1
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +1 -1
- package/out/project/_/__next.project.$d$id.txt +1 -1
- package/out/project/_/__next.project.txt +1 -1
- package/out/project/_/memory/__next._full.txt +1 -1
- package/out/project/_/memory/__next._head.txt +1 -1
- package/out/project/_/memory/__next._index.txt +1 -1
- package/out/project/_/memory/__next._tree.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.memory.txt +1 -1
- package/out/project/_/memory/__next.project.$d$id.txt +1 -1
- package/out/project/_/memory/__next.project.txt +1 -1
- package/out/project/_/memory.html +1 -1
- package/out/project/_/memory.txt +1 -1
- package/out/project/_/queue/__next._full.txt +1 -1
- package/out/project/_/queue/__next._head.txt +1 -1
- package/out/project/_/queue/__next._index.txt +1 -1
- package/out/project/_/queue/__next._tree.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
- package/out/project/_/queue/__next.project.$d$id.txt +1 -1
- package/out/project/_/queue/__next.project.txt +1 -1
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +1 -1
- package/out/project/_.html +1 -1
- package/out/project/_.txt +1 -1
- package/out/settings/__next._full.txt +1 -1
- package/out/settings/__next._head.txt +1 -1
- package/out/settings/__next._index.txt +1 -1
- package/out/settings/__next._tree.txt +1 -1
- package/out/settings/__next.settings.__PAGE__.txt +1 -1
- package/out/settings/__next.settings.txt +1 -1
- package/out/settings.html +1 -1
- package/out/settings.txt +1 -1
- package/out/setup/__next._full.txt +1 -1
- package/out/setup/__next._head.txt +1 -1
- package/out/setup/__next._index.txt +1 -1
- package/out/setup/__next._tree.txt +1 -1
- package/out/setup/__next.setup.__PAGE__.txt +1 -1
- package/out/setup/__next.setup.txt +1 -1
- package/out/setup.html +1 -1
- package/out/setup.txt +1 -1
- package/package.json +1 -1
- package/server/agentchattr-registry.js +105 -0
- package/server/index.js +125 -21
- /package/out/_next/static/{x1j09EJSQ1T6SDM0qG0LZ → 6W2vNw7Pp8z2_l_OJ2hqC}/_buildManifest.js +0 -0
- /package/out/_next/static/{x1j09EJSQ1T6SDM0qG0LZ → 6W2vNw7Pp8z2_l_OJ2hqC}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{x1j09EJSQ1T6SDM0qG0LZ → 6W2vNw7Pp8z2_l_OJ2hqC}/_ssgManifest.js +0 -0
package/server/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const pty = require("node-pty");
|
|
|
8
8
|
const { spawn } = require("child_process");
|
|
9
9
|
const { readConfig, resolveAgentCwd, resolveAgentCommand, resolveProjectChattr, resolveChattrSpawn, syncChattrToken, CONFIG_PATH } = require("./config");
|
|
10
10
|
const routes = require("./routes");
|
|
11
|
+
const { waitForAgentChattrReady, registerAgent, deregisterAgent } = require("./agentchattr-registry");
|
|
11
12
|
|
|
12
13
|
const net = require("net");
|
|
13
14
|
const config = readConfig();
|
|
@@ -207,6 +208,39 @@ const PERMISSION_FLAGS = {
|
|
|
207
208
|
* Generate a per-agent MCP config file for Claude (--mcp-config).
|
|
208
209
|
* Returns the absolute path to the written JSON file.
|
|
209
210
|
*/
|
|
211
|
+
/**
|
|
212
|
+
* Per-agent registration tokens persisted across QuadWork restarts so
|
|
213
|
+
* #242 stale-slot reclaim works after a crash. Without this the
|
|
214
|
+
* in-memory _tokenCache is empty on startup and the family-name
|
|
215
|
+
* deregister returns 403 (app.py:2123-2135).
|
|
216
|
+
*/
|
|
217
|
+
function _agentTokenPath(projectId, agentId) {
|
|
218
|
+
const configDir = path.join(os.homedir(), ".quadwork", projectId);
|
|
219
|
+
return path.join(configDir, `agent-token-${agentId}.txt`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readPersistedAgentToken(projectId, agentId) {
|
|
223
|
+
try {
|
|
224
|
+
return fs.readFileSync(_agentTokenPath(projectId, agentId), "utf8").trim() || null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writePersistedAgentToken(projectId, agentId, token) {
|
|
231
|
+
try {
|
|
232
|
+
const configDir = path.join(os.homedir(), ".quadwork", projectId);
|
|
233
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
234
|
+
fs.writeFileSync(_agentTokenPath(projectId, agentId), token, { mode: 0o600 });
|
|
235
|
+
} catch {
|
|
236
|
+
// non-fatal — stale-slot reclaim will degrade but registration still works
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function clearPersistedAgentToken(projectId, agentId) {
|
|
241
|
+
try { fs.unlinkSync(_agentTokenPath(projectId, agentId)); } catch {}
|
|
242
|
+
}
|
|
243
|
+
|
|
210
244
|
function writeMcpConfigFile(projectId, agentId, mcpHttpPort, token) {
|
|
211
245
|
const os = require("os");
|
|
212
246
|
const configDir = path.join(os.homedir(), ".quadwork", projectId);
|
|
@@ -233,12 +267,14 @@ function writeMcpConfigFile(projectId, agentId, mcpHttpPort, token) {
|
|
|
233
267
|
async function buildAgentArgs(projectId, agentId) {
|
|
234
268
|
const cfg = readConfig();
|
|
235
269
|
const project = cfg.projects?.find((p) => p.id === projectId);
|
|
236
|
-
if (!project) return [];
|
|
270
|
+
if (!project) return { args: [], acRegistrationName: null, acServerPort: null };
|
|
237
271
|
|
|
238
272
|
const agentCfg = project.agents?.[agentId] || {};
|
|
239
273
|
const command = agentCfg.command || "claude";
|
|
240
274
|
const cliBase = command.split("/").pop().split(" ")[0]; // extract base CLI name
|
|
241
275
|
const args = [];
|
|
276
|
+
let acRegistrationName = null;
|
|
277
|
+
let acServerPort = null;
|
|
242
278
|
|
|
243
279
|
// Permission bypass flags
|
|
244
280
|
if (agentCfg.auto_approve !== false) {
|
|
@@ -252,21 +288,63 @@ async function buildAgentArgs(projectId, agentId) {
|
|
|
252
288
|
if (mcpHttpPort) {
|
|
253
289
|
const injectMode = agentCfg.mcp_inject || (cliBase === "codex" ? "proxy_flag" : cliBase === "gemini" ? "env" : "flag");
|
|
254
290
|
if (injectMode === "flag") {
|
|
255
|
-
// Claude/Kimi:
|
|
256
|
-
|
|
291
|
+
// Claude/Kimi: register with AgentChattr to obtain a per-agent
|
|
292
|
+
// token (#239 — session_token is browser auth, not MCP auth) and
|
|
293
|
+
// write that into the per-agent MCP config file.
|
|
294
|
+
const chattrInfo = resolveProjectChattr(projectId);
|
|
295
|
+
acServerPort = Number(new URL(chattrInfo.url).port) || 8300;
|
|
296
|
+
await waitForAgentChattrReady(acServerPort);
|
|
297
|
+
// #242: best-effort deregister any stale registration of the
|
|
298
|
+
// canonical name (left over by a crashed previous QuadWork
|
|
299
|
+
// session) so the fresh register lands at slot 1 instead of
|
|
300
|
+
// head-2 / reviewer2-2. We need the previous agent's bearer
|
|
301
|
+
// token because app.py:2123 requires authenticated agent
|
|
302
|
+
// session for family names — load it from disk (persisted
|
|
303
|
+
// across restarts). Failures are non-fatal.
|
|
304
|
+
const stalePersistedToken = readPersistedAgentToken(projectId, agentId);
|
|
305
|
+
if (stalePersistedToken) {
|
|
306
|
+
await deregisterAgent(acServerPort, agentId, stalePersistedToken).catch(() => {});
|
|
307
|
+
clearPersistedAgentToken(projectId, agentId);
|
|
308
|
+
}
|
|
309
|
+
const registration = await registerAgent(acServerPort, agentId, agentCfg.display_name || null);
|
|
310
|
+
if (!registration) {
|
|
311
|
+
throw new Error(`Failed to register ${agentId}: ${registerAgent.lastError}`);
|
|
312
|
+
}
|
|
313
|
+
acRegistrationName = registration.name;
|
|
314
|
+
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
315
|
+
const mcpConfigPath = writeMcpConfigFile(projectId, agentId, mcpHttpPort, registration.token);
|
|
257
316
|
const flag = agentCfg.mcp_flag || "--mcp-config";
|
|
258
317
|
args.push(flag, mcpConfigPath);
|
|
259
318
|
} else if (injectMode === "proxy_flag") {
|
|
260
|
-
// Codex:
|
|
319
|
+
// Codex: register with AgentChattr first (#240) so the proxy
|
|
320
|
+
// injects a real per-agent token, not the global session token.
|
|
321
|
+
// Resolve via resolveProjectChattr so legacy/global-config
|
|
322
|
+
// projects without a per-project agentchattr_url still work.
|
|
323
|
+
const chattrInfo = resolveProjectChattr(projectId);
|
|
324
|
+
acServerPort = Number(new URL(chattrInfo.url).port) || 8300;
|
|
325
|
+
await waitForAgentChattrReady(acServerPort);
|
|
326
|
+
// #242: best-effort deregister stale canonical name first using
|
|
327
|
+
// the persisted bearer token from a previous session.
|
|
328
|
+
const stalePersistedToken = readPersistedAgentToken(projectId, agentId);
|
|
329
|
+
if (stalePersistedToken) {
|
|
330
|
+
await deregisterAgent(acServerPort, agentId, stalePersistedToken).catch(() => {});
|
|
331
|
+
clearPersistedAgentToken(projectId, agentId);
|
|
332
|
+
}
|
|
333
|
+
const registration = await registerAgent(acServerPort, agentId, agentCfg.display_name || null);
|
|
334
|
+
if (!registration) {
|
|
335
|
+
throw new Error(`Failed to register ${agentId}: ${registerAgent.lastError}`);
|
|
336
|
+
}
|
|
337
|
+
acRegistrationName = registration.name;
|
|
338
|
+
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
261
339
|
const upstreamUrl = `http://127.0.0.1:${mcpHttpPort}`;
|
|
262
|
-
const proxyUrl = await startMcpProxy(projectId, agentId, upstreamUrl, token);
|
|
340
|
+
const proxyUrl = await startMcpProxy(projectId, agentId, upstreamUrl, registration.token);
|
|
263
341
|
if (proxyUrl) {
|
|
264
342
|
args.push("-c", `mcp_servers.agentchattr.url="${proxyUrl}"`);
|
|
265
343
|
}
|
|
266
344
|
}
|
|
267
345
|
}
|
|
268
346
|
|
|
269
|
-
return args;
|
|
347
|
+
return { args, acRegistrationName, acServerPort };
|
|
270
348
|
}
|
|
271
349
|
|
|
272
350
|
/**
|
|
@@ -313,7 +391,8 @@ async function spawnAgentPty(project, agent) {
|
|
|
313
391
|
if (!cwd) return { ok: false, error: `Unknown agent: ${key}` };
|
|
314
392
|
|
|
315
393
|
const command = resolveAgentCommand(project, agent) || (process.env.SHELL || "/bin/zsh");
|
|
316
|
-
const
|
|
394
|
+
const built = await buildAgentArgs(project, agent);
|
|
395
|
+
const args = built.args;
|
|
317
396
|
const extraEnv = buildAgentEnv(project, agent);
|
|
318
397
|
|
|
319
398
|
try {
|
|
@@ -325,7 +404,16 @@ async function spawnAgentPty(project, agent) {
|
|
|
325
404
|
env: { ...process.env, ...extraEnv },
|
|
326
405
|
});
|
|
327
406
|
|
|
328
|
-
const session = {
|
|
407
|
+
const session = {
|
|
408
|
+
projectId: project,
|
|
409
|
+
agentId: agent,
|
|
410
|
+
term,
|
|
411
|
+
ws: null,
|
|
412
|
+
state: "running",
|
|
413
|
+
error: null,
|
|
414
|
+
acRegistrationName: built.acRegistrationName,
|
|
415
|
+
acServerPort: built.acServerPort,
|
|
416
|
+
};
|
|
329
417
|
agentSessions.set(key, session);
|
|
330
418
|
|
|
331
419
|
term.onExit(({ exitCode }) => {
|
|
@@ -349,8 +437,11 @@ async function spawnAgentPty(project, agent) {
|
|
|
349
437
|
}
|
|
350
438
|
}
|
|
351
439
|
|
|
352
|
-
// Helper: stop an agent session — kill PTY, close WS
|
|
353
|
-
|
|
440
|
+
// Helper: stop an agent session — kill PTY, close WS, deregister.
|
|
441
|
+
// Async because deregister must complete before a restart re-registers,
|
|
442
|
+
// otherwise the old slot stays occupied and a fresh register lands at
|
|
443
|
+
// head-2 instead of slot 1 (#241).
|
|
444
|
+
async function stopAgentSession(key) {
|
|
354
445
|
const session = agentSessions.get(key);
|
|
355
446
|
if (!session) {
|
|
356
447
|
agentSessions.set(key, { projectId: null, agentId: null, term: null, ws: null, state: "stopped", error: null });
|
|
@@ -366,6 +457,19 @@ function stopAgentSession(key) {
|
|
|
366
457
|
session.ws = null;
|
|
367
458
|
session.state = "stopped";
|
|
368
459
|
session.error = null;
|
|
460
|
+
// Best-effort deregister from AgentChattr (#241) so the slot frees
|
|
461
|
+
// and the next register lands at slot 1 instead of head-2.
|
|
462
|
+
if (session.acRegistrationName && session.acServerPort) {
|
|
463
|
+
try {
|
|
464
|
+
await deregisterAgent(session.acServerPort, session.acRegistrationName);
|
|
465
|
+
} catch {
|
|
466
|
+
// best-effort — failures are non-fatal
|
|
467
|
+
}
|
|
468
|
+
if (session.projectId && session.agentId) {
|
|
469
|
+
clearPersistedAgentToken(session.projectId, session.agentId);
|
|
470
|
+
}
|
|
471
|
+
session.acRegistrationName = null;
|
|
472
|
+
}
|
|
369
473
|
// Clean up MCP auth proxy if running
|
|
370
474
|
const [projectId, agentId] = key.split("/");
|
|
371
475
|
if (projectId && agentId) stopMcpProxy(projectId, agentId);
|
|
@@ -630,10 +734,10 @@ app.post("/api/agents/:project/:agent/start", async (req, res) => {
|
|
|
630
734
|
|
|
631
735
|
// --- Lifecycle: stop kills PTY + closes WS ---
|
|
632
736
|
|
|
633
|
-
app.post("/api/agents/:project/:agent/stop", (req, res) => {
|
|
737
|
+
app.post("/api/agents/:project/:agent/stop", async (req, res) => {
|
|
634
738
|
const { project, agent } = req.params;
|
|
635
739
|
const key = `${project}/${agent}`;
|
|
636
|
-
stopAgentSession(key);
|
|
740
|
+
await stopAgentSession(key);
|
|
637
741
|
res.json({ ok: true, state: "stopped" });
|
|
638
742
|
});
|
|
639
743
|
|
|
@@ -643,16 +747,16 @@ app.post("/api/agents/:project/:agent/restart", async (req, res) => {
|
|
|
643
747
|
const { project, agent } = req.params;
|
|
644
748
|
const key = `${project}/${agent}`;
|
|
645
749
|
|
|
646
|
-
|
|
750
|
+
// #241: must await deregister before respawn so the slot frees and
|
|
751
|
+
// the fresh register lands at slot 1 instead of head-2.
|
|
752
|
+
await stopAgentSession(key);
|
|
647
753
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
}, 500);
|
|
754
|
+
const result = await spawnAgentPty(project, agent);
|
|
755
|
+
if (result.ok) {
|
|
756
|
+
res.json({ ok: true, state: "running", pid: result.pid });
|
|
757
|
+
} else {
|
|
758
|
+
res.status(500).json({ ok: false, state: "error", error: result.error });
|
|
759
|
+
}
|
|
656
760
|
});
|
|
657
761
|
|
|
658
762
|
// --- Sessions tracking (for /api/projects dashboard) ---
|
|
File without changes
|
|
File without changes
|
|
File without changes
|