taskchef 8.2.2 → 8.3.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/.codex-plugin/plugin.json +1 -1
- package/README.md +53 -56
- package/docs/dashboard-lifecycle.md +108 -194
- package/docs/spec.md +75 -89
- package/docs/workflows.md +39 -22
- package/package.json +1 -1
- package/scripts/roles/resolve_roles.py +2 -2
- package/skills/taskchef-delegate/SKILL.md +11 -11
- package/skills/taskchef-delegate/references/model-roles.md +21 -44
- package/skills/taskchef-executor/SKILL.md +34 -0
- package/src/dashboard/dist/app.js +1 -1
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/react/components/ModelSettings.tsx +1 -1
- 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 +29 -282
- package/src/mcp.js +3 -3
- package/src/model-roles.js +3 -3
- package/src/dashboard-ownership.js +0 -251
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { open, rename, unlink } from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
export const DASHBOARD_CONTROL_VERSION = 1;
|
|
7
|
-
export const DASHBOARD_OWNER_FILE = ".taskchef-dashboard-owner.json";
|
|
8
|
-
export const DASHBOARD_HANDOFF_FILE = ".taskchef-dashboard-handoff.json";
|
|
9
|
-
export const DASHBOARD_CONTROL_CHALLENGE_PATH = "/api/control/challenge";
|
|
10
|
-
export const DASHBOARD_CONTROL_SHUTDOWN_PATH = "/api/control/shutdown";
|
|
11
|
-
export const DASHBOARD_CONTROL_SESSION_PATH = "/api/control/session";
|
|
12
|
-
export const DASHBOARD_CONTROL_HANDOFF_PATH = "/api/control/handoff";
|
|
13
|
-
export const DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH = "/api/control/handoff/commit";
|
|
14
|
-
|
|
15
|
-
const OWNER_MAX_BYTES = 4_096;
|
|
16
|
-
const SECRET_PATTERN = /^[a-f0-9]{64}$/;
|
|
17
|
-
const NONCE_PATTERN = /^[a-f0-9]{32,128}$/;
|
|
18
|
-
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
|
|
19
|
-
|
|
20
|
-
function ownerPath(workspace) {
|
|
21
|
-
return path.join(workspace, DASHBOARD_OWNER_FILE);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function handoffPath(workspace) {
|
|
25
|
-
return path.join(workspace, DASHBOARD_HANDOFF_FILE);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function syncDirectory(directory) {
|
|
29
|
-
const handle = await open(directory, constants.O_RDONLY);
|
|
30
|
-
try {
|
|
31
|
-
await handle.sync();
|
|
32
|
-
} finally {
|
|
33
|
-
await handle.close();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function exactKeys(value, keys) {
|
|
38
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
39
|
-
&& Object.keys(value).sort().join("\0") === [...keys].sort().join("\0");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function createDashboardControlSecret() {
|
|
43
|
-
return randomBytes(32).toString("hex");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function createDashboardControlNonce() {
|
|
47
|
-
return randomBytes(24).toString("hex");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function validDashboardControlSecret(secret) {
|
|
51
|
-
return typeof secret === "string" && SECRET_PATTERN.test(secret);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function dashboardControlProof(secret, action, nonce) {
|
|
55
|
-
return createHmac("sha256", Buffer.from(secret, "hex"))
|
|
56
|
-
.update(`taskchef-dashboard-control-v${DASHBOARD_CONTROL_VERSION}:${action}:${nonce}`)
|
|
57
|
-
.digest("hex");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function validDashboardControlNonce(nonce) {
|
|
61
|
-
return typeof nonce === "string" && NONCE_PATTERN.test(nonce);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function verifyDashboardControlProof(secret, action, nonce, proof) {
|
|
65
|
-
if (!validDashboardControlSecret(secret) || !validDashboardControlNonce(nonce)
|
|
66
|
-
|| typeof proof !== "string" || !SECRET_PATTERN.test(proof)) return false;
|
|
67
|
-
const expected = Buffer.from(dashboardControlProof(secret, action, nonce), "hex");
|
|
68
|
-
const supplied = Buffer.from(proof, "hex");
|
|
69
|
-
return expected.length === supplied.length && timingSafeEqual(expected, supplied);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function dashboardOwnerMetadata({
|
|
73
|
-
workspace,
|
|
74
|
-
host,
|
|
75
|
-
port,
|
|
76
|
-
taskchefVersion,
|
|
77
|
-
serverVersion,
|
|
78
|
-
launcher,
|
|
79
|
-
secret,
|
|
80
|
-
}) {
|
|
81
|
-
return {
|
|
82
|
-
schemaVersion: 1,
|
|
83
|
-
service: "taskchef-dashboard-owner",
|
|
84
|
-
controlVersion: DASHBOARD_CONTROL_VERSION,
|
|
85
|
-
workspace,
|
|
86
|
-
host,
|
|
87
|
-
port,
|
|
88
|
-
taskchefVersion,
|
|
89
|
-
serverVersion,
|
|
90
|
-
launcher,
|
|
91
|
-
secret,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function dashboardHandoffMetadata({
|
|
96
|
-
workspace,
|
|
97
|
-
host,
|
|
98
|
-
port,
|
|
99
|
-
taskchefVersion,
|
|
100
|
-
serverVersion,
|
|
101
|
-
id,
|
|
102
|
-
pids,
|
|
103
|
-
secret,
|
|
104
|
-
}) {
|
|
105
|
-
return {
|
|
106
|
-
schemaVersion: 1,
|
|
107
|
-
service: "taskchef-dashboard-handoff",
|
|
108
|
-
controlVersion: DASHBOARD_CONTROL_VERSION,
|
|
109
|
-
workspace,
|
|
110
|
-
host,
|
|
111
|
-
port,
|
|
112
|
-
taskchefVersion,
|
|
113
|
-
serverVersion,
|
|
114
|
-
launcher: "session",
|
|
115
|
-
id,
|
|
116
|
-
pids,
|
|
117
|
-
proof: dashboardControlProof(secret, `handoff-final:${JSON.stringify(pids)}`, id),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function validOwner(value) {
|
|
122
|
-
const keys = [
|
|
123
|
-
"schemaVersion", "service", "controlVersion", "workspace", "host", "port",
|
|
124
|
-
"taskchefVersion", "serverVersion", "launcher", "secret",
|
|
125
|
-
];
|
|
126
|
-
return exactKeys(value, keys)
|
|
127
|
-
&& value.schemaVersion === 1
|
|
128
|
-
&& value.service === "taskchef-dashboard-owner"
|
|
129
|
-
&& value.controlVersion === DASHBOARD_CONTROL_VERSION
|
|
130
|
-
&& typeof value.workspace === "string"
|
|
131
|
-
&& LOOPBACK_HOSTS.has(value.host)
|
|
132
|
-
&& Number.isInteger(value.port) && value.port >= 0 && value.port <= 65_535
|
|
133
|
-
&& typeof value.taskchefVersion === "string"
|
|
134
|
-
&& typeof value.serverVersion === "string"
|
|
135
|
-
&& new Set(["mcp", "session"]).has(value.launcher)
|
|
136
|
-
&& typeof value.secret === "string" && SECRET_PATTERN.test(value.secret);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function validHandoff(value) {
|
|
140
|
-
const keys = [
|
|
141
|
-
"schemaVersion", "service", "controlVersion", "workspace", "host", "port",
|
|
142
|
-
"taskchefVersion", "serverVersion", "launcher", "id", "pids", "proof",
|
|
143
|
-
];
|
|
144
|
-
return exactKeys(value, keys)
|
|
145
|
-
&& value.schemaVersion === 1
|
|
146
|
-
&& value.service === "taskchef-dashboard-handoff"
|
|
147
|
-
&& value.controlVersion === DASHBOARD_CONTROL_VERSION
|
|
148
|
-
&& typeof value.workspace === "string"
|
|
149
|
-
&& LOOPBACK_HOSTS.has(value.host)
|
|
150
|
-
&& Number.isInteger(value.port) && value.port >= 1 && value.port <= 65_535
|
|
151
|
-
&& typeof value.taskchefVersion === "string"
|
|
152
|
-
&& typeof value.serverVersion === "string"
|
|
153
|
-
&& value.launcher === "session"
|
|
154
|
-
&& validDashboardControlNonce(value.id)
|
|
155
|
-
&& Array.isArray(value.pids) && value.pids.length <= 64
|
|
156
|
-
&& value.pids.every((pid) => Number.isSafeInteger(pid) && pid > 1)
|
|
157
|
-
&& new Set(value.pids).size === value.pids.length
|
|
158
|
-
&& typeof value.proof === "string" && SECRET_PATTERN.test(value.proof);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function readPrivateJson(filePath) {
|
|
162
|
-
const handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
163
|
-
try {
|
|
164
|
-
const info = await handle.stat();
|
|
165
|
-
if (!info.isFile() || (info.mode & 0o777) !== 0o600) {
|
|
166
|
-
throw new Error("dashboard ownership record is not a private regular file");
|
|
167
|
-
}
|
|
168
|
-
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
169
|
-
throw new Error("dashboard ownership record has a different owner");
|
|
170
|
-
}
|
|
171
|
-
if (info.size > OWNER_MAX_BYTES) throw new Error("dashboard ownership record is too large");
|
|
172
|
-
const buffer = Buffer.alloc(OWNER_MAX_BYTES + 1);
|
|
173
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
174
|
-
if (bytesRead > OWNER_MAX_BYTES) throw new Error("dashboard ownership record is too large");
|
|
175
|
-
return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
176
|
-
} finally {
|
|
177
|
-
await handle.close();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export async function readDashboardOwner(workspace) {
|
|
182
|
-
const filePath = ownerPath(workspace);
|
|
183
|
-
const value = await readPrivateJson(filePath);
|
|
184
|
-
if (!validOwner(value) || value.workspace !== workspace) {
|
|
185
|
-
throw new Error("dashboard owner record is invalid");
|
|
186
|
-
}
|
|
187
|
-
return value;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function readDashboardHandoff(workspace) {
|
|
191
|
-
const value = await readPrivateJson(handoffPath(workspace));
|
|
192
|
-
if (!validHandoff(value) || value.workspace !== workspace) {
|
|
193
|
-
throw new Error("dashboard handoff record is invalid");
|
|
194
|
-
}
|
|
195
|
-
return value;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function writeDashboardOwner(workspace, value, { signal } = {}) {
|
|
199
|
-
if (!validOwner(value) || value.workspace !== workspace) {
|
|
200
|
-
throw new Error("dashboard owner record does not match its canonical workspace");
|
|
201
|
-
}
|
|
202
|
-
const filePath = ownerPath(workspace);
|
|
203
|
-
const temporary = `${filePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
204
|
-
const throwIfAborted = () => {
|
|
205
|
-
if (signal?.aborted) {
|
|
206
|
-
throw Object.assign(new Error("dashboard owner publication was cancelled"), {
|
|
207
|
-
code: "TASKCHEF_DASHBOARD_START_TIMEOUT",
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
try {
|
|
212
|
-
throwIfAborted();
|
|
213
|
-
const handle = await open(temporary, "wx", 0o600);
|
|
214
|
-
try {
|
|
215
|
-
await handle.writeFile(`${JSON.stringify(value)}\n`, { encoding: "utf8" });
|
|
216
|
-
throwIfAborted();
|
|
217
|
-
await handle.sync();
|
|
218
|
-
throwIfAborted();
|
|
219
|
-
} finally {
|
|
220
|
-
await handle.close();
|
|
221
|
-
}
|
|
222
|
-
throwIfAborted();
|
|
223
|
-
await rename(temporary, filePath);
|
|
224
|
-
await syncDirectory(workspace);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
await unlink(temporary).catch(() => {});
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export async function writeDashboardHandoff(workspace, value) {
|
|
232
|
-
if (!validHandoff(value) || value.workspace !== workspace) {
|
|
233
|
-
throw new Error("dashboard handoff record does not match its canonical workspace");
|
|
234
|
-
}
|
|
235
|
-
const filePath = handoffPath(workspace);
|
|
236
|
-
const temporary = `${filePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
237
|
-
try {
|
|
238
|
-
const handle = await open(temporary, "wx", 0o600);
|
|
239
|
-
try {
|
|
240
|
-
await handle.writeFile(`${JSON.stringify(value)}\n`, { encoding: "utf8" });
|
|
241
|
-
await handle.sync();
|
|
242
|
-
} finally {
|
|
243
|
-
await handle.close();
|
|
244
|
-
}
|
|
245
|
-
await rename(temporary, filePath);
|
|
246
|
-
await syncDirectory(workspace);
|
|
247
|
-
} catch (error) {
|
|
248
|
-
await unlink(temporary).catch(() => {});
|
|
249
|
-
throw error;
|
|
250
|
-
}
|
|
251
|
-
}
|