taskchef 8.3.0 → 8.3.2
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/.codex-plugin/plugin.json +1 -1
- package/README.md +45 -49
- package/docs/dashboard-lifecycle.md +108 -194
- package/docs/spec.md +62 -88
- package/docs/workflows.md +29 -22
- package/package.json +1 -1
- package/src/dashboard/dist/app.js +19 -19
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/react/components/ModelSettings.test.tsx +50 -1
- package/src/dashboard/react/components/ModelSettings.tsx +8 -3
- package/src/dashboard-control.js +34 -0
- package/src/dashboard-manager.js +183 -842
- package/src/dashboard-session-process.js +35 -126
- package/src/dashboard-session.js +0 -5
- package/src/dashboard.js +28 -281
- package/src/mcp.js +3 -3
- package/src/dashboard-ownership.js +0 -251
|
@@ -1,174 +1,83 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
|
|
3
3
|
import { createDashboardServer } from "./dashboard.js";
|
|
4
|
-
import {
|
|
5
|
-
dashboardHandoffMetadata,
|
|
6
|
-
dashboardOwnerMetadata,
|
|
7
|
-
validDashboardControlSecret,
|
|
8
|
-
writeDashboardHandoff,
|
|
9
|
-
writeDashboardOwner,
|
|
10
|
-
} from "./dashboard-ownership.js";
|
|
11
|
-
import {
|
|
12
|
-
MAX_DASHBOARD_SESSION_PIDS,
|
|
13
|
-
createDashboardSessionLease,
|
|
14
|
-
validSessionPid,
|
|
15
|
-
} from "./dashboard-session.js";
|
|
4
|
+
import { createDashboardSessionLease, validSessionPid } from "./dashboard-session.js";
|
|
16
5
|
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
17
6
|
|
|
18
7
|
export async function runDashboardSessionProcess({
|
|
19
8
|
workspace = process.env.TASKCHEF_DASHBOARD_WORKSPACE,
|
|
20
9
|
host = process.env.TASKCHEF_DASHBOARD_HOST ?? "127.0.0.1",
|
|
21
10
|
port = Number(process.env.TASKCHEF_DASHBOARD_PORT ?? 3210),
|
|
22
|
-
secret = process.env.TASKCHEF_DASHBOARD_SECRET,
|
|
23
11
|
sessionPid = Number(process.env.TASKCHEF_DASHBOARD_SESSION_PID),
|
|
24
|
-
sessionPids,
|
|
25
12
|
taskchefVersion = TASKCHEF_VERSION,
|
|
26
13
|
serverVersion = DASHBOARD_SERVER_VERSION,
|
|
27
14
|
createServer = createDashboardServer,
|
|
28
|
-
writeOwner = writeDashboardOwner,
|
|
29
|
-
writeHandoff = writeDashboardHandoff,
|
|
30
15
|
createLease = createDashboardSessionLease,
|
|
31
16
|
processObject = process,
|
|
32
17
|
signal,
|
|
33
18
|
checkIntervalMs = Number(process.env.TASKCHEF_DASHBOARD_CHECK_INTERVAL_MS ?? 1_000),
|
|
34
19
|
exitGraceMs = Number(process.env.TASKCHEF_DASHBOARD_EXIT_GRACE_MS ?? 15_000),
|
|
35
20
|
} = {}) {
|
|
36
|
-
if (processObject.env)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!validDashboardControlSecret(secret)) {
|
|
40
|
-
throw new Error("dashboard session control credential is invalid");
|
|
21
|
+
if (processObject.env) {
|
|
22
|
+
delete processObject.env.TASKCHEF_DASHBOARD_SECRET;
|
|
23
|
+
delete processObject.env.TASKCHEF_DASHBOARD_SESSION_PIDS;
|
|
41
24
|
}
|
|
25
|
+
if (!workspace) throw new Error("dashboard session workspace is required");
|
|
42
26
|
if (!validSessionPid(sessionPid)) throw new Error("dashboard session PID is invalid");
|
|
43
|
-
if (!Array.isArray(sessionPids) || sessionPids.length > MAX_DASHBOARD_SESSION_PIDS
|
|
44
|
-
|| sessionPids.some((pid) => !validSessionPid(pid))
|
|
45
|
-
|| new Set([sessionPid, ...sessionPids]).size > MAX_DASHBOARD_SESSION_PIDS) {
|
|
46
|
-
throw new Error("dashboard transferred session PIDs are invalid");
|
|
47
|
-
}
|
|
48
27
|
|
|
49
28
|
const canonicalWorkspace = await realpath(workspace);
|
|
50
29
|
let server = null;
|
|
51
30
|
let lease = null;
|
|
52
31
|
let closePromise = null;
|
|
32
|
+
let closeChain = Promise.resolve();
|
|
33
|
+
let closedLease = null;
|
|
34
|
+
let closedServer = null;
|
|
53
35
|
let closing = false;
|
|
54
|
-
let publishingOwner = false;
|
|
55
|
-
let deferredClose = null;
|
|
56
36
|
const abortError = () => Object.assign(new Error("dashboard session startup was cancelled"), {
|
|
57
37
|
code: "TASKCHEF_DASHBOARD_START_TIMEOUT",
|
|
58
38
|
});
|
|
59
|
-
const startupClosedError = () => Object.assign(
|
|
60
|
-
new Error("dashboard session closed during startup"),
|
|
61
|
-
{ code: "TASKCHEF_DASHBOARD_START_EXIT" },
|
|
62
|
-
);
|
|
63
39
|
const removeListeners = () => {
|
|
64
40
|
processObject.off("SIGINT", close);
|
|
65
41
|
processObject.off("SIGTERM", close);
|
|
66
42
|
};
|
|
67
|
-
const performClose = async () => {
|
|
68
|
-
let cleanupError = null;
|
|
69
|
-
try {
|
|
70
|
-
lease?.close();
|
|
71
|
-
} catch (error) {
|
|
72
|
-
cleanupError = error;
|
|
73
|
-
}
|
|
74
|
-
await server?.close();
|
|
75
|
-
if (cleanupError) throw cleanupError;
|
|
76
|
-
};
|
|
77
43
|
const close = () => {
|
|
78
44
|
closing = true;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
45
|
+
closePromise = closeChain.then(async () => {
|
|
46
|
+
let failure = null;
|
|
47
|
+
if (lease && lease !== closedLease) {
|
|
48
|
+
closedLease = lease;
|
|
49
|
+
try { lease.close(); } catch (error) { failure = error; }
|
|
50
|
+
}
|
|
51
|
+
if (server && server !== closedServer) {
|
|
52
|
+
closedServer = server;
|
|
53
|
+
try { await server.close(); } catch (error) { failure ??= error; }
|
|
54
|
+
}
|
|
55
|
+
if (failure) throw failure;
|
|
56
|
+
}).finally(removeListeners);
|
|
57
|
+
closeChain = closePromise.catch(() => {});
|
|
86
58
|
return closePromise;
|
|
87
59
|
};
|
|
88
|
-
const onAbort = () => {
|
|
89
|
-
void close().catch(() => {});
|
|
90
|
-
};
|
|
60
|
+
const onAbort = () => { void close().catch(() => {}); };
|
|
91
61
|
if (signal?.aborted) throw abortError();
|
|
92
62
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
93
63
|
processObject.once("SIGINT", close);
|
|
94
64
|
processObject.once("SIGTERM", close);
|
|
95
|
-
const control = {
|
|
96
|
-
secret,
|
|
97
|
-
onShutdown: close,
|
|
98
|
-
onSession: (pid) => {
|
|
99
|
-
if (closing) {
|
|
100
|
-
throw Object.assign(new Error("dashboard session is retiring"), {
|
|
101
|
-
code: "TASKCHEF_DASHBOARD_SESSION_RETIRING",
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
lease.register(pid);
|
|
105
|
-
},
|
|
106
|
-
onHandoff: (pid) => {
|
|
107
|
-
if (closing) {
|
|
108
|
-
throw Object.assign(new Error("dashboard session is retiring"), {
|
|
109
|
-
code: "TASKCHEF_DASHBOARD_SESSION_RETIRING",
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
lease.register(pid);
|
|
113
|
-
return lease.snapshot();
|
|
114
|
-
},
|
|
115
|
-
onHandoffFinalized: ({ id, pids }) => writeHandoff(
|
|
116
|
-
canonicalWorkspace,
|
|
117
|
-
dashboardHandoffMetadata({
|
|
118
|
-
workspace: canonicalWorkspace,
|
|
119
|
-
host,
|
|
120
|
-
port: server.port,
|
|
121
|
-
taskchefVersion,
|
|
122
|
-
serverVersion,
|
|
123
|
-
id,
|
|
124
|
-
pids,
|
|
125
|
-
secret,
|
|
126
|
-
}),
|
|
127
|
-
),
|
|
128
|
-
};
|
|
129
65
|
|
|
130
66
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
throw startupClosedError();
|
|
144
|
-
}
|
|
67
|
+
const control = {
|
|
68
|
+
onShutdown: close,
|
|
69
|
+
onSession: (pid) => {
|
|
70
|
+
if (closing) throw Object.assign(new Error("dashboard session is retiring"), {
|
|
71
|
+
code: "TASKCHEF_DASHBOARD_SESSION_RETIRING",
|
|
72
|
+
});
|
|
73
|
+
lease.register(pid);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
server = await createServer({ workspace: canonicalWorkspace, host, port,
|
|
77
|
+
taskchefVersion, serverVersion, launcher: "session", control });
|
|
78
|
+
if (closing || signal?.aborted) throw abortError();
|
|
145
79
|
lease = createLease({ initialPid: sessionPid, checkIntervalMs, exitGraceMs, onExpire: close });
|
|
146
|
-
|
|
147
|
-
publishingOwner = true;
|
|
148
|
-
try {
|
|
149
|
-
await writeOwner(canonicalWorkspace, dashboardOwnerMetadata({
|
|
150
|
-
workspace: canonicalWorkspace,
|
|
151
|
-
host,
|
|
152
|
-
port: server.port,
|
|
153
|
-
taskchefVersion,
|
|
154
|
-
serverVersion,
|
|
155
|
-
launcher: "session",
|
|
156
|
-
secret,
|
|
157
|
-
}), { signal });
|
|
158
|
-
} finally {
|
|
159
|
-
publishingOwner = false;
|
|
160
|
-
if (deferredClose) {
|
|
161
|
-
const pending = deferredClose;
|
|
162
|
-
deferredClose = null;
|
|
163
|
-
void performClose().then(pending.resolve, pending.reject);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (closing) {
|
|
167
|
-
await closePromise;
|
|
168
|
-
if (signal?.aborted) throw abortError();
|
|
169
|
-
throw startupClosedError();
|
|
170
|
-
}
|
|
171
|
-
if (signal?.aborted) throw abortError();
|
|
80
|
+
if (closing || signal?.aborted) throw abortError();
|
|
172
81
|
signal?.removeEventListener("abort", onAbort);
|
|
173
82
|
return { server, lease, close };
|
|
174
83
|
} catch (error) {
|
package/src/dashboard-session.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const DEFAULT_CHECK_INTERVAL_MS = 1_000;
|
|
2
2
|
const DEFAULT_EXIT_GRACE_MS = 15_000;
|
|
3
3
|
export const MAX_DASHBOARD_SESSION_PIDS = 64;
|
|
4
|
-
export const MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS = MAX_DASHBOARD_SESSION_PIDS - 1;
|
|
5
4
|
|
|
6
5
|
export function processIsAlive(pid, processObject = process) {
|
|
7
6
|
try {
|
|
@@ -77,10 +76,6 @@ export function createDashboardSessionLease({
|
|
|
77
76
|
pids.add(pid);
|
|
78
77
|
absentSince = null;
|
|
79
78
|
},
|
|
80
|
-
snapshot() {
|
|
81
|
-
activePids();
|
|
82
|
-
return [...pids];
|
|
83
|
-
},
|
|
84
79
|
tick,
|
|
85
80
|
close() {
|
|
86
81
|
if (timer) clearInterval(timer);
|
package/src/dashboard.js
CHANGED
|
@@ -31,22 +31,11 @@ import { createUsageTracker } from "./usage-tracker.js";
|
|
|
31
31
|
import { readCcusageRuntimeInfo, readUsageStore, usageStorePath } from "./usage.js";
|
|
32
32
|
import { reportedWorkSummary } from "./reported-work.js";
|
|
33
33
|
import {
|
|
34
|
-
MAX_DASHBOARD_SESSION_PIDS,
|
|
35
|
-
MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS,
|
|
36
|
-
validSessionPid,
|
|
37
|
-
} from "./dashboard-session.js";
|
|
38
|
-
import {
|
|
39
|
-
DASHBOARD_CONTROL_CHALLENGE_PATH,
|
|
40
|
-
DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH,
|
|
41
34
|
DASHBOARD_CONTROL_SESSION_PATH,
|
|
42
|
-
DASHBOARD_CONTROL_HANDOFF_PATH,
|
|
43
35
|
DASHBOARD_CONTROL_SHUTDOWN_PATH,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
validDashboardControlSecret,
|
|
48
|
-
verifyDashboardControlProof,
|
|
49
|
-
} from "./dashboard-ownership.js";
|
|
36
|
+
exactDashboardIdentity,
|
|
37
|
+
validDashboardControlRequest,
|
|
38
|
+
} from "./dashboard-control.js";
|
|
50
39
|
|
|
51
40
|
const TASKS_FILE_NAME = "tasks.jsonl";
|
|
52
41
|
const STATIC_ROOT = fileURLToPath(new URL("./dashboard/", import.meta.url));
|
|
@@ -57,11 +46,6 @@ const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
|
57
46
|
const DEFAULT_MAX_TASKS = 2_000;
|
|
58
47
|
const DEFAULT_MAX_EVENT_CLIENTS = 16;
|
|
59
48
|
const DEFAULT_CALCULATING_USAGE_STALE_MS = 120_000;
|
|
60
|
-
const DASHBOARD_HANDOFF_PREPARE_TTL_MS = 5_000;
|
|
61
|
-
const DASHBOARD_HANDOFF_COMMIT_GRACE_MS = 250;
|
|
62
|
-
const DASHBOARD_HANDOFF_COMMIT_RETRY_MS = 1_000;
|
|
63
|
-
const DASHBOARD_CONTROL_NONCE_LIMIT = 4_096;
|
|
64
|
-
const DASHBOARD_CONTROL_NONCE_TTL_MS = 5 * 60_000;
|
|
65
49
|
const MAX_MANUAL_TRANSITION_BODY_BYTES = 4 * 1024;
|
|
66
50
|
export const DASHBOARD_HEALTH_PATH = "/api/health";
|
|
67
51
|
export const DASHBOARD_HEALTH_MAX_BYTES = 8 * 1024;
|
|
@@ -792,33 +776,6 @@ function publicMonitorError() {
|
|
|
792
776
|
};
|
|
793
777
|
}
|
|
794
778
|
|
|
795
|
-
export function createDashboardControlReplayCache({
|
|
796
|
-
maximum = DASHBOARD_CONTROL_NONCE_LIMIT,
|
|
797
|
-
ttlMs = DASHBOARD_CONTROL_NONCE_TTL_MS,
|
|
798
|
-
now = Date.now,
|
|
799
|
-
} = {}) {
|
|
800
|
-
if (!Number.isSafeInteger(maximum) || maximum <= 0) {
|
|
801
|
-
throw new Error("dashboard control replay limit must be a positive integer");
|
|
802
|
-
}
|
|
803
|
-
if (!Number.isFinite(ttlMs) || ttlMs <= 0 || typeof now !== "function") {
|
|
804
|
-
throw new Error("dashboard control replay window is invalid");
|
|
805
|
-
}
|
|
806
|
-
const entries = new Map();
|
|
807
|
-
return {
|
|
808
|
-
accept(nonce) {
|
|
809
|
-
const timestamp = now();
|
|
810
|
-
for (const [candidate, expiresAt] of entries) {
|
|
811
|
-
if (expiresAt > timestamp) break;
|
|
812
|
-
entries.delete(candidate);
|
|
813
|
-
}
|
|
814
|
-
if (entries.has(nonce) || entries.size >= maximum) return false;
|
|
815
|
-
entries.set(nonce, timestamp + ttlMs);
|
|
816
|
-
return true;
|
|
817
|
-
},
|
|
818
|
-
get size() { return entries.size; },
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
779
|
export async function createDashboardServer({
|
|
823
780
|
archiveEnabled = CODEX_CHAT_ARCHIVE_ENABLED,
|
|
824
781
|
archiveThread = archiveThreadInCodex,
|
|
@@ -840,7 +797,6 @@ export async function createDashboardServer({
|
|
|
840
797
|
resolveUsageProvider = readCcusageRuntimeInfo,
|
|
841
798
|
updateRole = updateModelRole,
|
|
842
799
|
control = null,
|
|
843
|
-
controlReplayCache = createDashboardControlReplayCache(),
|
|
844
800
|
} = {}) {
|
|
845
801
|
if (!LOOPBACK_HOSTS.has(host)) {
|
|
846
802
|
throw new Error("dashboard host must be a loopback address");
|
|
@@ -858,11 +814,9 @@ export async function createDashboardServer({
|
|
|
858
814
|
throw new Error("dashboard launcher must be mcp, session, or standalone");
|
|
859
815
|
}
|
|
860
816
|
if (control !== null && (!new Set(["mcp", "session"]).has(launcher)
|
|
861
|
-
|| !validDashboardControlSecret(control?.secret)
|
|
862
817
|
|| typeof control?.onShutdown !== "function"
|
|
863
|
-
|| (launcher === "session" &&
|
|
864
|
-
|
|
865
|
-
throw new Error("dashboard control requires a valid TaskChef ownership controller");
|
|
818
|
+
|| (launcher === "session" && typeof control?.onSession !== "function"))) {
|
|
819
|
+
throw new Error("dashboard control requires TaskChef session callbacks");
|
|
866
820
|
}
|
|
867
821
|
let requestHandler = (_request, response) => {
|
|
868
822
|
sendJson(response, 503, { message: "TaskChef dashboard is initializing." });
|
|
@@ -908,8 +862,6 @@ export async function createDashboardServer({
|
|
|
908
862
|
const clients = new Set();
|
|
909
863
|
const archiveRequests = new Set();
|
|
910
864
|
let usageProviderPromise = null;
|
|
911
|
-
let controlHandoff = null;
|
|
912
|
-
let controlHandoffTimer = null;
|
|
913
865
|
|
|
914
866
|
const broadcast = (event, value) => {
|
|
915
867
|
const payload = ssePayload(event, value);
|
|
@@ -970,37 +922,29 @@ export async function createDashboardServer({
|
|
|
970
922
|
return;
|
|
971
923
|
}
|
|
972
924
|
|
|
973
|
-
if (url.pathname === DASHBOARD_CONTROL_CHALLENGE_PATH && method === "GET") {
|
|
974
|
-
const nonce = url.searchParams.get("nonce");
|
|
975
|
-
if (!control || !validDashboardControlNonce(nonce)) {
|
|
976
|
-
sendJson(response, control ? 400 : 404, { message: "Not found." });
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
sendJson(response, 200, {
|
|
980
|
-
schemaVersion: 1,
|
|
981
|
-
nonce,
|
|
982
|
-
proof: dashboardControlProof(control.secret, "challenge", nonce),
|
|
983
|
-
});
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
925
|
if (url.pathname === DASHBOARD_CONTROL_SHUTDOWN_PATH && method === "POST") {
|
|
988
926
|
if (!control) {
|
|
989
927
|
sendJson(response, 404, { message: "Not found." });
|
|
990
928
|
return;
|
|
991
929
|
}
|
|
930
|
+
if (!validDashboardControlRequest(request, {
|
|
931
|
+
authority: allowedAuthority, origin: allowedOrigin, action: "shutdown",
|
|
932
|
+
})) {
|
|
933
|
+
sendJson(response, 403, { message: "Dashboard control request was refused." });
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
992
936
|
const body = await readBoundedJsonBody(request);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
sendJson(response,
|
|
937
|
+
if (!body || Object.keys(body).length !== 2 || body.reason !== "version-replacement"
|
|
938
|
+
|| !("expectedIdentity" in body)) {
|
|
939
|
+
sendJson(response, 400, { message: "Dashboard shutdown request was malformed." });
|
|
996
940
|
return;
|
|
997
941
|
}
|
|
998
|
-
if (!
|
|
999
|
-
sendJson(response, 409, {
|
|
942
|
+
if (!exactDashboardIdentity(identity, body.expectedIdentity)) {
|
|
943
|
+
sendJson(response, 409, { schemaVersion: 1, accepted: false, reason: "identity-mismatch" });
|
|
1000
944
|
return;
|
|
1001
945
|
}
|
|
1002
946
|
sendJson(response, 202, { schemaVersion: 1, accepted: true });
|
|
1003
|
-
setImmediate(() => { void control.onShutdown().catch(() => {}); });
|
|
947
|
+
setImmediate(() => { void Promise.resolve(control.onShutdown()).catch(() => {}); });
|
|
1004
948
|
return;
|
|
1005
949
|
}
|
|
1006
950
|
|
|
@@ -1009,25 +953,21 @@ export async function createDashboardServer({
|
|
|
1009
953
|
sendJson(response, 404, { message: "Not found." });
|
|
1010
954
|
return;
|
|
1011
955
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|| !verifyDashboardControlProof(
|
|
1017
|
-
control.secret, `session:${pid}`, nonce, body?.proof,
|
|
1018
|
-
)) {
|
|
1019
|
-
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
956
|
+
if (!validDashboardControlRequest(request, {
|
|
957
|
+
authority: allowedAuthority, origin: allowedOrigin, action: "session",
|
|
958
|
+
})) {
|
|
959
|
+
sendJson(response, 403, { message: "Dashboard control request was refused." });
|
|
1020
960
|
return;
|
|
1021
961
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
});
|
|
962
|
+
const body = await readBoundedJsonBody(request);
|
|
963
|
+
const pid = body?.pid;
|
|
964
|
+
if (!body || Object.keys(body).length !== 2 || !Number.isSafeInteger(pid) || pid <= 1
|
|
965
|
+
|| !("expectedIdentity" in body)) {
|
|
966
|
+
sendJson(response, 400, { message: "Dashboard session request was malformed." });
|
|
1027
967
|
return;
|
|
1028
968
|
}
|
|
1029
|
-
if (!
|
|
1030
|
-
sendJson(response, 409, {
|
|
969
|
+
if (!exactDashboardIdentity(identity, body.expectedIdentity)) {
|
|
970
|
+
sendJson(response, 409, { schemaVersion: 1, accepted: false, reason: "identity-mismatch" });
|
|
1031
971
|
return;
|
|
1032
972
|
}
|
|
1033
973
|
try {
|
|
@@ -1035,8 +975,6 @@ export async function createDashboardServer({
|
|
|
1035
975
|
sendJson(response, 200, {
|
|
1036
976
|
schemaVersion: 1,
|
|
1037
977
|
accepted: true,
|
|
1038
|
-
nonce,
|
|
1039
|
-
proof: dashboardControlProof(control.secret, `session-accepted:${pid}`, nonce),
|
|
1040
978
|
});
|
|
1041
979
|
} catch (error) {
|
|
1042
980
|
sendJson(response, 409, {
|
|
@@ -1049,197 +987,6 @@ export async function createDashboardServer({
|
|
|
1049
987
|
return;
|
|
1050
988
|
}
|
|
1051
989
|
|
|
1052
|
-
if (url.pathname === DASHBOARD_CONTROL_HANDOFF_PATH && method === "POST") {
|
|
1053
|
-
if (!control || typeof control.onHandoff !== "function") {
|
|
1054
|
-
sendJson(response, 404, { message: "Not found." });
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
const body = await readBoundedJsonBody(request);
|
|
1058
|
-
const nonce = body?.nonce;
|
|
1059
|
-
const pid = body?.pid;
|
|
1060
|
-
if (!Number.isSafeInteger(pid) || pid <= 1
|
|
1061
|
-
|| !verifyDashboardControlProof(
|
|
1062
|
-
control.secret, `handoff:${pid}`, nonce, body?.proof,
|
|
1063
|
-
)) {
|
|
1064
|
-
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
if (!controlReplayCache.accept(nonce)) {
|
|
1068
|
-
sendJson(response, 409, { message: "Dashboard control credential was already used." });
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
if (controlHandoff) {
|
|
1072
|
-
if (controlHandoff.pending || controlHandoff.finalizing || controlHandoff.finalized) {
|
|
1073
|
-
sendJson(response, 409, {
|
|
1074
|
-
message: "Dashboard handoff is already in progress.",
|
|
1075
|
-
reason: "retiring",
|
|
1076
|
-
});
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
try {
|
|
1080
|
-
const pids = controlHandoff.participants.has(pid)
|
|
1081
|
-
? controlHandoff.pids
|
|
1082
|
-
: await control.onHandoff(pid);
|
|
1083
|
-
if (!Array.isArray(pids) || pids.length > MAX_DASHBOARD_SESSION_PIDS
|
|
1084
|
-
|| pids.some((value) => !validSessionPid(value))
|
|
1085
|
-
|| new Set(pids).size !== pids.length
|
|
1086
|
-
|| !pids.includes(pid)) {
|
|
1087
|
-
throw new Error("dashboard handoff returned invalid session leases");
|
|
1088
|
-
}
|
|
1089
|
-
controlHandoff.pids = pids;
|
|
1090
|
-
controlHandoff.participants.add(pid);
|
|
1091
|
-
const { id } = controlHandoff;
|
|
1092
|
-
sendJson(response, 200, {
|
|
1093
|
-
schemaVersion: 1,
|
|
1094
|
-
accepted: true,
|
|
1095
|
-
id,
|
|
1096
|
-
pids,
|
|
1097
|
-
nonce,
|
|
1098
|
-
proof: dashboardControlProof(
|
|
1099
|
-
control.secret,
|
|
1100
|
-
`handoff-prepared:${pid}:${id}:${JSON.stringify(pids)}`,
|
|
1101
|
-
nonce,
|
|
1102
|
-
),
|
|
1103
|
-
});
|
|
1104
|
-
} catch {
|
|
1105
|
-
sendJson(response, 409, {
|
|
1106
|
-
message: "Dashboard handoff was refused.",
|
|
1107
|
-
reason: "refused",
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
controlHandoff = { pid, pending: true, committed: false };
|
|
1113
|
-
try {
|
|
1114
|
-
const pids = await control.onHandoff(pid);
|
|
1115
|
-
if (!Array.isArray(pids) || pids.length > MAX_DASHBOARD_SESSION_PIDS
|
|
1116
|
-
|| pids.some((value) => !validSessionPid(value))
|
|
1117
|
-
|| new Set(pids).size !== pids.length
|
|
1118
|
-
|| !pids.includes(pid)) {
|
|
1119
|
-
throw new Error("dashboard handoff returned invalid session leases");
|
|
1120
|
-
}
|
|
1121
|
-
const id = createDashboardControlNonce();
|
|
1122
|
-
controlHandoff = {
|
|
1123
|
-
id,
|
|
1124
|
-
pid,
|
|
1125
|
-
pids,
|
|
1126
|
-
participants: new Set([pid]),
|
|
1127
|
-
pending: false,
|
|
1128
|
-
committed: false,
|
|
1129
|
-
};
|
|
1130
|
-
controlHandoffTimer = setTimeout(() => {
|
|
1131
|
-
if (controlHandoff?.id === id && !controlHandoff.committed) controlHandoff = null;
|
|
1132
|
-
controlHandoffTimer = null;
|
|
1133
|
-
}, DASHBOARD_HANDOFF_PREPARE_TTL_MS);
|
|
1134
|
-
controlHandoffTimer.unref?.();
|
|
1135
|
-
sendJson(response, 200, {
|
|
1136
|
-
schemaVersion: 1,
|
|
1137
|
-
accepted: true,
|
|
1138
|
-
id,
|
|
1139
|
-
pids,
|
|
1140
|
-
nonce,
|
|
1141
|
-
proof: dashboardControlProof(
|
|
1142
|
-
control.secret,
|
|
1143
|
-
`handoff-prepared:${pid}:${id}:${JSON.stringify(pids)}`,
|
|
1144
|
-
nonce,
|
|
1145
|
-
),
|
|
1146
|
-
});
|
|
1147
|
-
} catch {
|
|
1148
|
-
if (controlHandoff?.pid === pid && controlHandoff.pending) controlHandoff = null;
|
|
1149
|
-
sendJson(response, 409, {
|
|
1150
|
-
message: "Dashboard handoff was refused.",
|
|
1151
|
-
reason: "refused",
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (url.pathname === DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH && method === "POST") {
|
|
1158
|
-
if (!control || typeof control.onHandoff !== "function") {
|
|
1159
|
-
sendJson(response, 404, { message: "Not found." });
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
const body = await readBoundedJsonBody(request);
|
|
1163
|
-
const nonce = body?.nonce;
|
|
1164
|
-
const id = body?.id;
|
|
1165
|
-
if (!validDashboardControlNonce(id)
|
|
1166
|
-
|| !verifyDashboardControlProof(
|
|
1167
|
-
control.secret, `handoff-commit:${id}`, nonce, body?.proof,
|
|
1168
|
-
)) {
|
|
1169
|
-
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
if (!controlReplayCache.accept(nonce)) {
|
|
1173
|
-
sendJson(response, 409, { message: "Dashboard control credential was already used." });
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
if (!controlHandoff || controlHandoff.id !== id) {
|
|
1177
|
-
sendJson(response, 409, {
|
|
1178
|
-
message: "Dashboard handoff preparation expired or did not match.",
|
|
1179
|
-
reason: "refused",
|
|
1180
|
-
});
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
if (!controlHandoff.commitPromise) {
|
|
1184
|
-
controlHandoff.committed = true;
|
|
1185
|
-
if (controlHandoffTimer) clearTimeout(controlHandoffTimer);
|
|
1186
|
-
controlHandoffTimer = null;
|
|
1187
|
-
controlHandoff.commitPromise = new Promise((resolve, reject) => {
|
|
1188
|
-
const finalizationTimer = setTimeout(async () => {
|
|
1189
|
-
try {
|
|
1190
|
-
controlHandoff.finalizing = true;
|
|
1191
|
-
const pids = await control.onHandoff(controlHandoff.pid);
|
|
1192
|
-
if (!Array.isArray(pids)
|
|
1193
|
-
|| pids.length > MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS
|
|
1194
|
-
|| pids.some((value) => !validSessionPid(value))
|
|
1195
|
-
|| new Set(pids).size !== pids.length
|
|
1196
|
-
|| !pids.includes(controlHandoff.pid)) {
|
|
1197
|
-
throw new Error("dashboard handoff returned invalid final session leases");
|
|
1198
|
-
}
|
|
1199
|
-
controlHandoff.pids = pids;
|
|
1200
|
-
await control.onHandoffFinalized?.({ id, pids });
|
|
1201
|
-
controlHandoff.finalized = true;
|
|
1202
|
-
resolve(pids);
|
|
1203
|
-
} catch (error) {
|
|
1204
|
-
reject(error);
|
|
1205
|
-
}
|
|
1206
|
-
}, DASHBOARD_HANDOFF_COMMIT_GRACE_MS);
|
|
1207
|
-
finalizationTimer.unref?.();
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
let pids;
|
|
1211
|
-
try {
|
|
1212
|
-
pids = await controlHandoff.commitPromise;
|
|
1213
|
-
} catch {
|
|
1214
|
-
if (controlHandoff?.id === id) controlHandoff = null;
|
|
1215
|
-
sendJson(response, 409, {
|
|
1216
|
-
message: "Dashboard handoff finalization was refused.",
|
|
1217
|
-
reason: "refused",
|
|
1218
|
-
});
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
sendJson(response, 202, {
|
|
1222
|
-
schemaVersion: 1,
|
|
1223
|
-
accepted: true,
|
|
1224
|
-
id,
|
|
1225
|
-
pids,
|
|
1226
|
-
nonce,
|
|
1227
|
-
proof: dashboardControlProof(
|
|
1228
|
-
control.secret,
|
|
1229
|
-
`handoff-committed:${id}:${JSON.stringify(pids)}`,
|
|
1230
|
-
nonce,
|
|
1231
|
-
),
|
|
1232
|
-
});
|
|
1233
|
-
if (!controlHandoff.shutdownScheduled) {
|
|
1234
|
-
controlHandoff.shutdownScheduled = true;
|
|
1235
|
-
const shutdownTimer = setTimeout(() => {
|
|
1236
|
-
void control.onShutdown().catch(() => {});
|
|
1237
|
-
}, DASHBOARD_HANDOFF_COMMIT_RETRY_MS);
|
|
1238
|
-
shutdownTimer.unref?.();
|
|
1239
|
-
}
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
990
|
if (url.pathname === "/api/settings" && method === "GET") {
|
|
1244
991
|
const profiles = [{ id: "personal", project: "Personal", ...await resolveRoles({ includeCatalog: true }) }];
|
|
1245
992
|
usageProviderPromise ??= Promise.resolve(resolveUsageProvider()).catch(() => ({
|
package/src/mcp.js
CHANGED
|
@@ -109,7 +109,7 @@ const preparationSchema = z.object({
|
|
|
109
109
|
|
|
110
110
|
const dashboardSchema = z.object({
|
|
111
111
|
action: z.enum(["started", "reused"]),
|
|
112
|
-
launcher: z.
|
|
112
|
+
launcher: z.enum(["session", "standalone", "mcp"]),
|
|
113
113
|
url: z.string().url(),
|
|
114
114
|
workspace: z.string(),
|
|
115
115
|
taskchefVersion: z.string(),
|
|
@@ -126,7 +126,7 @@ function toolResult(key, value, message) {
|
|
|
126
126
|
function dashboardAutostartDiagnostic(error) {
|
|
127
127
|
const message = typeof error?.message === "string" ? error.message : "";
|
|
128
128
|
if (typeof error?.staleTaskchefVersion === "string") {
|
|
129
|
-
return `TaskChef dashboard autostart skipped:
|
|
129
|
+
return `TaskChef dashboard autostart skipped: recognized older TaskChef ${error.staleTaskchefVersion} listener refused graceful shutdown; it was left running.`;
|
|
130
130
|
}
|
|
131
131
|
if (error?.code === "EADDRINUSE" || /port conflict|already in use/i.test(message)) {
|
|
132
132
|
return "TaskChef dashboard autostart skipped: port 127.0.0.1:3210 is unavailable; the listener was left untouched.";
|
|
@@ -204,7 +204,7 @@ export function createTaskChefMcpServer({
|
|
|
204
204
|
{
|
|
205
205
|
title: "Ensure TaskChef dashboard",
|
|
206
206
|
description:
|
|
207
|
-
"Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts or reuses the
|
|
207
|
+
"Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts or reuses the exact recognized TaskChef dashboard for this Codex session and canonical workspace; recognized older same-workspace listeners are gracefully restarted to the installed version, while newer and unknown listeners remain running.",
|
|
208
208
|
inputSchema: {},
|
|
209
209
|
outputSchema: { dashboard: dashboardSchema },
|
|
210
210
|
annotations: {
|