@h-rig/server 0.0.6-alpha.0
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/README.md +14 -0
- package/dist/src/bootstrap.js +161 -0
- package/dist/src/index.js +13153 -0
- package/dist/src/inspector/agent-runtime.js +1077 -0
- package/dist/src/inspector/analysis.js +41 -0
- package/dist/src/inspector/discovery.js +137 -0
- package/dist/src/inspector/journal.js +518 -0
- package/dist/src/inspector/mission.js +562 -0
- package/dist/src/inspector/prompt.js +97 -0
- package/dist/src/inspector/provider-session.js +65 -0
- package/dist/src/inspector/reconcile.js +118 -0
- package/dist/src/inspector/review.js +13 -0
- package/dist/src/inspector/service.js +1759 -0
- package/dist/src/inspector/skills.js +155 -0
- package/dist/src/inspector/tools.js +1592 -0
- package/dist/src/inspector/types.js +1 -0
- package/dist/src/inspector/upstream-sync.js +479 -0
- package/dist/src/orchestration.js +402 -0
- package/dist/src/remote.js +123 -0
- package/dist/src/scheduler.js +84 -0
- package/dist/src/server-helpers/broadcasters.js +161 -0
- package/dist/src/server-helpers/conversation-snapshot.js +382 -0
- package/dist/src/server-helpers/event-emitter.js +41 -0
- package/dist/src/server-helpers/github-auth-store.js +155 -0
- package/dist/src/server-helpers/github-credentials.js +38 -0
- package/dist/src/server-helpers/github-project-status-sync.js +196 -0
- package/dist/src/server-helpers/github-projects.js +147 -0
- package/dist/src/server-helpers/github-reconciler.js +89 -0
- package/dist/src/server-helpers/http-router.js +3781 -0
- package/dist/src/server-helpers/http-utils.js +135 -0
- package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
- package/dist/src/server-helpers/inspector-jobs.js +4145 -0
- package/dist/src/server-helpers/issue-analysis.js +362 -0
- package/dist/src/server-helpers/normalizers.js +31 -0
- package/dist/src/server-helpers/notifications.js +96 -0
- package/dist/src/server-helpers/orchestration-ops.js +287 -0
- package/dist/src/server-helpers/orchestration.js +39 -0
- package/dist/src/server-helpers/plugin-host-cache.js +86 -0
- package/dist/src/server-helpers/project-fs-ops.js +194 -0
- package/dist/src/server-helpers/project-registry.js +124 -0
- package/dist/src/server-helpers/queue-state.js +78 -0
- package/dist/src/server-helpers/remote-checkout.js +140 -0
- package/dist/src/server-helpers/remote-snapshots.js +119 -0
- package/dist/src/server-helpers/run-io.js +262 -0
- package/dist/src/server-helpers/run-mutations.js +1784 -0
- package/dist/src/server-helpers/run-steering.js +176 -0
- package/dist/src/server-helpers/run-writers.js +75 -0
- package/dist/src/server-helpers/server-paths.js +27 -0
- package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
- package/dist/src/server-helpers/snapshot-service.js +1143 -0
- package/dist/src/server-helpers/summaries.js +126 -0
- package/dist/src/server-helpers/task-config.js +50 -0
- package/dist/src/server-helpers/task-projection.js +98 -0
- package/dist/src/server-helpers/terminal-runtime.js +156 -0
- package/dist/src/server-helpers/terminal-sessions.js +22 -0
- package/dist/src/server-helpers/validation-failure.js +31 -0
- package/dist/src/server-helpers/ws-router.js +1308 -0
- package/dist/src/server.js +12628 -0
- package/dist/src/websocket.js +63 -0
- package/package.json +33 -0
|
@@ -0,0 +1,3781 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
7
|
+
import { basename, dirname as dirname6, isAbsolute as isAbsolute2, resolve as resolve11 } from "path";
|
|
8
|
+
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
9
|
+
|
|
10
|
+
// packages/server/src/server.ts
|
|
11
|
+
import {
|
|
12
|
+
listAuthorityArtifactRoots,
|
|
13
|
+
listAuthorityRuns as listAuthorityRuns6,
|
|
14
|
+
readJsonFile as readJsonFile4,
|
|
15
|
+
readJsonlFile as readJsonlFile3
|
|
16
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
17
|
+
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus3 } from "@rig/runtime/control-plane/state-sync/types";
|
|
18
|
+
import {
|
|
19
|
+
readWorkspaceSummary
|
|
20
|
+
} from "@rig/runtime/control-plane/native/workspace-ops";
|
|
21
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot6 } from "@rig/runtime/control-plane/native/utils";
|
|
22
|
+
|
|
23
|
+
// packages/server/src/bootstrap.ts
|
|
24
|
+
import { RIG_DEFINITION_DIRNAME, resolveMonorepoRoot } from "@rig/runtime";
|
|
25
|
+
|
|
26
|
+
// packages/server/src/websocket.ts
|
|
27
|
+
import {
|
|
28
|
+
WS_CHANNELS
|
|
29
|
+
} from "@rig/contracts";
|
|
30
|
+
function handleWebSocketUpgrade(args) {
|
|
31
|
+
if (!args.server || args.req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const url = new URL(args.req.url);
|
|
35
|
+
const token = url.searchParams.get("token");
|
|
36
|
+
if (args.authToken && token !== args.authToken) {
|
|
37
|
+
return Response.json({ ok: false, error: "Unauthorized WebSocket connection" }, { status: 401 });
|
|
38
|
+
}
|
|
39
|
+
const upgraded = args.server.upgrade(args.req, {
|
|
40
|
+
data: { connectedAt: args.connectedAt ?? new Date().toISOString() }
|
|
41
|
+
});
|
|
42
|
+
if (upgraded) {
|
|
43
|
+
return new Response(null);
|
|
44
|
+
}
|
|
45
|
+
return Response.json({ ok: false, error: "WebSocket upgrade failed" }, { status: 400 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// packages/server/src/server-helpers/normalizers.ts
|
|
49
|
+
function normalizeString(value) {
|
|
50
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
51
|
+
}
|
|
52
|
+
function normalizeStringArray(value) {
|
|
53
|
+
return Array.isArray(value) ? value.map((entry) => normalizeString(entry)).filter((entry) => entry !== null) : [];
|
|
54
|
+
}
|
|
55
|
+
function normalizeRuntimeAdapter(value) {
|
|
56
|
+
const normalized = normalizeString(value)?.toLowerCase();
|
|
57
|
+
if (!normalized) {
|
|
58
|
+
return "pi";
|
|
59
|
+
}
|
|
60
|
+
if (normalized === "codex" || normalized === "codex-cli" || normalized === "codex-app-server" || normalized === "gpt-codex") {
|
|
61
|
+
return "codex";
|
|
62
|
+
}
|
|
63
|
+
if (normalized === "pi" || normalized === "rig-pi" || normalized === "@rig/pi") {
|
|
64
|
+
return "pi";
|
|
65
|
+
}
|
|
66
|
+
return "claude-code";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// packages/server/src/server-helpers/run-io.ts
|
|
70
|
+
import { dirname, resolve } from "path";
|
|
71
|
+
import { closeSync, existsSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
|
|
72
|
+
import { createReadStream } from "fs";
|
|
73
|
+
import { createInterface } from "readline";
|
|
74
|
+
import {
|
|
75
|
+
listAuthorityRuns,
|
|
76
|
+
readAuthorityRun,
|
|
77
|
+
readJsonlFile,
|
|
78
|
+
resolveAuthorityRunDir
|
|
79
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
80
|
+
function matchesRunFilter(entry, filter) {
|
|
81
|
+
return (!filter.runId || entry.runId === filter.runId) && (!filter.taskId || entry.taskId === filter.taskId);
|
|
82
|
+
}
|
|
83
|
+
function readApprovalsForRuns(projectRoot, runs, filter = {}) {
|
|
84
|
+
return runs.filter((entry) => matchesRunFilter(entry, filter)).flatMap((entry) => readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, entry.runId), "approvals.jsonl")).flatMap((record) => {
|
|
85
|
+
if (!record || typeof record !== "object") {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const value = record;
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
runId: entry.runId,
|
|
92
|
+
taskId: entry.taskId,
|
|
93
|
+
requestId: normalizeString(value.requestId) ?? normalizeString(value.id),
|
|
94
|
+
status: normalizeString(value.status),
|
|
95
|
+
record: value
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
function readApprovals(projectRoot, filter = {}) {
|
|
101
|
+
return readApprovalsForRuns(projectRoot, listAuthorityRuns(projectRoot), filter);
|
|
102
|
+
}
|
|
103
|
+
function readUserInputsForRuns(projectRoot, runs, filter = {}) {
|
|
104
|
+
return runs.filter((entry) => matchesRunFilter(entry, filter)).flatMap((entry) => readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, entry.runId), "user-input.jsonl")).flatMap((record) => {
|
|
105
|
+
if (!record || typeof record !== "object") {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const value = record;
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
runId: entry.runId,
|
|
112
|
+
taskId: entry.taskId,
|
|
113
|
+
requestId: normalizeString(value.requestId) ?? normalizeString(value.id),
|
|
114
|
+
status: normalizeString(value.status),
|
|
115
|
+
record: value
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
function readUserInputs(projectRoot, filter = {}) {
|
|
121
|
+
return readUserInputsForRuns(projectRoot, listAuthorityRuns(projectRoot), filter);
|
|
122
|
+
}
|
|
123
|
+
function writeRunJsonlFile(projectRoot, runId, fileName, rows) {
|
|
124
|
+
const filePath = resolve(resolveAuthorityRunDir(projectRoot, runId), fileName);
|
|
125
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
126
|
+
const next = rows.map((row) => JSON.stringify(row)).join(`
|
|
127
|
+
`);
|
|
128
|
+
writeFileSync(filePath, next.length > 0 ? `${next}
|
|
129
|
+
` : "", "utf8");
|
|
130
|
+
}
|
|
131
|
+
function resolveApproval(projectRoot, input) {
|
|
132
|
+
const approvalsPath = resolve(resolveAuthorityRunDir(projectRoot, input.runId), "approvals.jsonl");
|
|
133
|
+
const approvals = readJsonlFile(approvalsPath);
|
|
134
|
+
const resolvedAt = new Date().toISOString();
|
|
135
|
+
const rows = approvals.map((entry) => entry.requestId === input.requestId || entry.id === input.requestId ? {
|
|
136
|
+
...entry,
|
|
137
|
+
status: "resolved",
|
|
138
|
+
decision: input.decision,
|
|
139
|
+
note: input.note ?? null,
|
|
140
|
+
resolvedAt
|
|
141
|
+
} : entry);
|
|
142
|
+
writeRunJsonlFile(projectRoot, input.runId, "approvals.jsonl", rows);
|
|
143
|
+
return { ok: true, runId: input.runId, requestId: input.requestId, decision: input.decision };
|
|
144
|
+
}
|
|
145
|
+
function respondToUserInput(projectRoot, input) {
|
|
146
|
+
const requestsPath = resolve(resolveAuthorityRunDir(projectRoot, input.runId), "user-input.jsonl");
|
|
147
|
+
const requests = readJsonlFile(requestsPath);
|
|
148
|
+
const resolvedAt = new Date().toISOString();
|
|
149
|
+
const rows = requests.map((entry) => entry.requestId === input.requestId || entry.id === input.requestId ? {
|
|
150
|
+
...entry,
|
|
151
|
+
status: "resolved",
|
|
152
|
+
answers: input.answers,
|
|
153
|
+
respondedAt: resolvedAt,
|
|
154
|
+
resolvedAt
|
|
155
|
+
} : entry);
|
|
156
|
+
writeRunJsonlFile(projectRoot, input.runId, "user-input.jsonl", rows);
|
|
157
|
+
return { ok: true, runId: input.runId, requestId: input.requestId, answers: input.answers };
|
|
158
|
+
}
|
|
159
|
+
function isGenericRunFailure(value) {
|
|
160
|
+
return typeof value === "string" && /^Task run failed \([^)]*\)$/i.test(value.trim());
|
|
161
|
+
}
|
|
162
|
+
function summarizeUsefulRunError(projectRoot, runId, fallback) {
|
|
163
|
+
const logs = readJsonlFileTail(runLogsPath(projectRoot, runId), { limit: 80, maxBytes: 512 * 1024 }).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
164
|
+
const errorLines = logs.filter((entry) => normalizeString(entry.tone) === "error" || normalizeString(entry.status) === "failed").map((entry) => normalizeString(entry.detail)).filter((entry) => Boolean(entry && !isGenericRunFailure(entry)));
|
|
165
|
+
const taskSourceFailure = errorLines.find((line) => /failed to update task source/i.test(line));
|
|
166
|
+
if (taskSourceFailure)
|
|
167
|
+
return `Task source update failed: ${taskSourceFailure}`;
|
|
168
|
+
const moduleFailure = errorLines.find((line) => /cannot find module/i.test(line));
|
|
169
|
+
if (moduleFailure)
|
|
170
|
+
return `Runtime module resolution failed: ${moduleFailure}`;
|
|
171
|
+
const providerFailure = errorLines.find((line) => /no api key found|unauthorized|authentication failed|invalid api key/i.test(line));
|
|
172
|
+
if (providerFailure)
|
|
173
|
+
return `Provider authentication failed: ${providerFailure}`;
|
|
174
|
+
const nonGeneric = errorLines.at(-1);
|
|
175
|
+
return nonGeneric ?? (typeof fallback === "string" ? fallback : null);
|
|
176
|
+
}
|
|
177
|
+
function readRunDetails(projectRoot, runId) {
|
|
178
|
+
const run = readAuthorityRun(projectRoot, runId);
|
|
179
|
+
if (!run) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const usefulErrorText = isGenericRunFailure(run.errorText) ? summarizeUsefulRunError(projectRoot, runId, run.errorText) : null;
|
|
183
|
+
return {
|
|
184
|
+
run: usefulErrorText ? { ...run, errorText: usefulErrorText } : run,
|
|
185
|
+
timeline: readJsonlFile(resolve(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl")),
|
|
186
|
+
approvals: readApprovals(projectRoot, { runId }),
|
|
187
|
+
userInputs: readUserInputs(projectRoot, { runId })
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function runTimelinePath(projectRoot, runId) {
|
|
191
|
+
return resolve(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl");
|
|
192
|
+
}
|
|
193
|
+
function runLogsPath(projectRoot, runId) {
|
|
194
|
+
return resolve(resolveAuthorityRunDir(projectRoot, runId), "logs.jsonl");
|
|
195
|
+
}
|
|
196
|
+
function parseJsonlRecords(lines) {
|
|
197
|
+
return lines.map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
198
|
+
try {
|
|
199
|
+
return [JSON.parse(line)];
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function readJsonlFileTail(path, options) {
|
|
206
|
+
const limit = Math.max(0, Math.trunc(options.limit));
|
|
207
|
+
if (limit === 0 || !existsSync(path))
|
|
208
|
+
return [];
|
|
209
|
+
const maxBytes = Math.max(1024, Math.trunc(options.maxBytes ?? 256 * 1024));
|
|
210
|
+
const size = statSync(path).size;
|
|
211
|
+
if (size === 0)
|
|
212
|
+
return [];
|
|
213
|
+
const start = Math.max(0, size - maxBytes);
|
|
214
|
+
const length = size - start;
|
|
215
|
+
const buffer = Buffer.alloc(length);
|
|
216
|
+
const fd = openSync(path, "r");
|
|
217
|
+
try {
|
|
218
|
+
readSync(fd, buffer, 0, length, start);
|
|
219
|
+
} finally {
|
|
220
|
+
closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
const text = buffer.toString("utf8");
|
|
223
|
+
const lines = text.split(/\r?\n/);
|
|
224
|
+
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
225
|
+
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
226
|
+
}
|
|
227
|
+
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
228
|
+
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
229
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
230
|
+
const cursor = options.cursor == null ? null : Number.parseInt(options.cursor, 10);
|
|
231
|
+
const logsPath = runLogsPath(projectRoot, runId);
|
|
232
|
+
if ((options.cursor == null || !Number.isFinite(cursor)) && existsSync(logsPath)) {
|
|
233
|
+
const size = statSync(logsPath).size;
|
|
234
|
+
if (size > INITIAL_RUN_LOG_TAIL_MAX_BYTES) {
|
|
235
|
+
const tail = readJsonlFileTail(logsPath, {
|
|
236
|
+
limit,
|
|
237
|
+
maxBytes: INITIAL_RUN_LOG_TAIL_MAX_BYTES
|
|
238
|
+
}).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
239
|
+
return {
|
|
240
|
+
entries: tail.toReversed(),
|
|
241
|
+
nextCursor: null,
|
|
242
|
+
hasMore: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const endExclusive = cursor == null || !Number.isFinite(cursor) ? Number.POSITIVE_INFINITY : Math.max(0, cursor);
|
|
247
|
+
const window = [];
|
|
248
|
+
let index = 0;
|
|
249
|
+
let hasMore = false;
|
|
250
|
+
const stream = createReadStream(logsPath, { encoding: "utf8" });
|
|
251
|
+
stream.on("error", (error) => {
|
|
252
|
+
if (error.code !== "ENOENT") {
|
|
253
|
+
stream.destroy(error);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
257
|
+
try {
|
|
258
|
+
for await (const line of lines) {
|
|
259
|
+
const currentIndex = index;
|
|
260
|
+
index += 1;
|
|
261
|
+
if (currentIndex >= endExclusive) {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
const trimmed = line.trim();
|
|
265
|
+
if (trimmed.length === 0) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(trimmed);
|
|
270
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
271
|
+
window.push({ index: currentIndex, entry: parsed });
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
window.push({
|
|
275
|
+
index: currentIndex,
|
|
276
|
+
entry: { title: "Unparsed log line", detail: line, tone: "info" }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (window.length > limit) {
|
|
280
|
+
window.shift();
|
|
281
|
+
hasMore = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const code = error.code;
|
|
286
|
+
if (code !== "ENOENT") {
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
entries: window.toReversed().map((item) => item.entry),
|
|
292
|
+
nextCursor: hasMore ? String(window[0]?.index ?? 0) : null,
|
|
293
|
+
hasMore
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function remoteArtifactsRoot(projectRoot, runId) {
|
|
297
|
+
return resolve(resolveAuthorityRunDir(projectRoot, runId), "remote-artifacts");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// packages/server/src/server-helpers/server-paths.ts
|
|
301
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
302
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot2 } from "@rig/runtime/control-plane/native/utils";
|
|
303
|
+
function resolveServerAuthorityPaths(projectRoot) {
|
|
304
|
+
const taskWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
305
|
+
const explicitStateDir = process.env.RIG_STATE_DIR?.trim();
|
|
306
|
+
const explicitLogsDir = process.env.RIG_LOGS_DIR?.trim();
|
|
307
|
+
const explicitSessionFile = process.env.RIG_SESSION_FILE?.trim();
|
|
308
|
+
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
309
|
+
const stateRoot = taskWorkspace ? resolve2(taskWorkspace, ".rig") : explicitStateDir ? dirname2(resolve2(explicitStateDir)) : explicitLogsDir ? dirname2(resolve2(explicitLogsDir)) : explicitSessionFile ? dirname2(dirname2(resolve2(explicitSessionFile))) : resolve2(monorepoRoot, ".rig");
|
|
310
|
+
const stateDir = explicitStateDir ? resolve2(explicitStateDir) : resolve2(stateRoot, "state");
|
|
311
|
+
const logsDir = explicitLogsDir ? resolve2(explicitLogsDir) : resolve2(stateRoot, "logs");
|
|
312
|
+
const sessionFile = explicitSessionFile ? resolve2(explicitSessionFile) : resolve2(stateRoot, "session", "session.json");
|
|
313
|
+
const artifactsDir = taskWorkspace ? resolve2(taskWorkspace, "artifacts") : resolve2(monorepoRoot, "artifacts");
|
|
314
|
+
return {
|
|
315
|
+
stateRoot,
|
|
316
|
+
stateDir,
|
|
317
|
+
logsDir,
|
|
318
|
+
controlPlaneEventsFile: resolve2(logsDir, "control-plane.events.jsonl"),
|
|
319
|
+
sessionFile,
|
|
320
|
+
artifactsDir
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// packages/server/src/server-helpers/snapshot-service.ts
|
|
325
|
+
import { listAgentRuntimes } from "@rig/runtime/control-plane/runtime/isolation";
|
|
326
|
+
|
|
327
|
+
// packages/server/src/server-helpers/snapshot-orchestrator.ts
|
|
328
|
+
import {
|
|
329
|
+
listAuthorityRuns as listAuthorityRuns3,
|
|
330
|
+
readAuthorityRun as readAuthorityRun2
|
|
331
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
332
|
+
|
|
333
|
+
// packages/server/src/server-helpers/queue-state.ts
|
|
334
|
+
import { resolve as resolve3 } from "path";
|
|
335
|
+
import { readJsonFile, writeJsonFile } from "@rig/runtime/control-plane/authority-files";
|
|
336
|
+
function resolveQueueStatePath(projectRoot) {
|
|
337
|
+
return resolve3(resolveServerAuthorityPaths(projectRoot).stateDir, "task-queue.json");
|
|
338
|
+
}
|
|
339
|
+
function readQueueState(projectRoot) {
|
|
340
|
+
const queue = readJsonFile(resolveQueueStatePath(projectRoot), null);
|
|
341
|
+
if (!Array.isArray(queue))
|
|
342
|
+
return [];
|
|
343
|
+
return queue.filter((entry) => entry && typeof entry === "object").map((entry, index) => ({
|
|
344
|
+
taskId: normalizeString(entry.taskId),
|
|
345
|
+
score: typeof entry.score === "number" ? Math.max(0, Math.trunc(entry.score)) : 0,
|
|
346
|
+
unblockCount: typeof entry.unblockCount === "number" ? Math.max(0, Math.trunc(entry.unblockCount)) : 0,
|
|
347
|
+
position: typeof entry.position === "number" ? Math.max(0, Math.trunc(entry.position)) : index
|
|
348
|
+
})).filter((entry) => Boolean(entry.taskId)).sort((left, right) => right.score - left.score || left.position - right.position).map((entry, index) => ({ ...entry, position: index }));
|
|
349
|
+
}
|
|
350
|
+
function writeQueueState(projectRoot, queue) {
|
|
351
|
+
writeJsonFile(resolveQueueStatePath(projectRoot), queue);
|
|
352
|
+
}
|
|
353
|
+
function dequeueTaskState(projectRoot, taskId) {
|
|
354
|
+
const next = readQueueState(projectRoot).filter((entry) => entry.taskId !== taskId).map((entry, index) => ({ ...entry, position: index }));
|
|
355
|
+
writeQueueState(projectRoot, next);
|
|
356
|
+
return next;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// packages/server/src/server-helpers/remote-snapshots.ts
|
|
360
|
+
import { listAuthorityRemoteEndpoints } from "@rig/runtime/control-plane/authority-files";
|
|
361
|
+
|
|
362
|
+
// packages/server/src/server-helpers/conversation-snapshot.ts
|
|
363
|
+
import {
|
|
364
|
+
listAuthorityRuns as listAuthorityRuns2,
|
|
365
|
+
readJsonlFile as readJsonlFile2
|
|
366
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
367
|
+
var snapshotCache = new Map;
|
|
368
|
+
|
|
369
|
+
// packages/server/src/server-helpers/plugin-host-cache.ts
|
|
370
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
371
|
+
import { resolve as resolve4 } from "path";
|
|
372
|
+
var contextCache = new Map;
|
|
373
|
+
var taskListCache = new Map;
|
|
374
|
+
function getPluginHostConfigMtime(projectRoot) {
|
|
375
|
+
for (const name of ["rig.config.ts", "rig.config.json"]) {
|
|
376
|
+
const path = resolve4(projectRoot, name);
|
|
377
|
+
if (existsSync2(path)) {
|
|
378
|
+
try {
|
|
379
|
+
return statSync2(path).mtimeMs;
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
async function getCachedPluginHostContext(projectRoot) {
|
|
388
|
+
const mtimeMs = getPluginHostConfigMtime(projectRoot);
|
|
389
|
+
if (mtimeMs === null) {
|
|
390
|
+
contextCache.delete(projectRoot);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const cached = contextCache.get(projectRoot);
|
|
394
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.ctx) {
|
|
395
|
+
return cached.ctx;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const { buildPluginHostContext } = await import("@rig/runtime/control-plane/plugin-host-context");
|
|
399
|
+
const ctx = await buildPluginHostContext(projectRoot);
|
|
400
|
+
if (!ctx) {
|
|
401
|
+
contextCache.delete(projectRoot);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
contextCache.set(projectRoot, { mtimeMs, ctx });
|
|
405
|
+
return ctx;
|
|
406
|
+
} catch (err) {
|
|
407
|
+
contextCache.delete(projectRoot);
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// packages/server/src/server-helpers/task-projection.ts
|
|
413
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
414
|
+
import { resolve as resolve5 } from "path";
|
|
415
|
+
function projectionPath(projectRoot) {
|
|
416
|
+
return resolve5(projectRoot, ".rig", "state", "task-projection.json");
|
|
417
|
+
}
|
|
418
|
+
function stateDir(projectRoot) {
|
|
419
|
+
return resolve5(projectRoot, ".rig", "state");
|
|
420
|
+
}
|
|
421
|
+
function cloneTask(task) {
|
|
422
|
+
return { ...task };
|
|
423
|
+
}
|
|
424
|
+
function writeTaskProjection(projectRoot, input) {
|
|
425
|
+
const activeByTask = new Map((input.activeRuns ?? []).map((run) => [run.taskId, run]));
|
|
426
|
+
const tasks = input.tasks.map((task) => {
|
|
427
|
+
const projected = cloneTask(task);
|
|
428
|
+
const active = activeByTask.get(task.id);
|
|
429
|
+
if (active) {
|
|
430
|
+
projected.status = "in_progress";
|
|
431
|
+
projected.activeRun = {
|
|
432
|
+
runId: active.runId,
|
|
433
|
+
...active.status ? { status: active.status } : {},
|
|
434
|
+
...active.stage ? { stage: active.stage } : {}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return projected;
|
|
438
|
+
});
|
|
439
|
+
const snapshot = {
|
|
440
|
+
version: 1,
|
|
441
|
+
source: input.source,
|
|
442
|
+
reason: input.reason,
|
|
443
|
+
refreshedAt: input.refreshedAt ?? new Date().toISOString(),
|
|
444
|
+
tasks
|
|
445
|
+
};
|
|
446
|
+
mkdirSync2(stateDir(projectRoot), { recursive: true });
|
|
447
|
+
writeFileSync2(projectionPath(projectRoot), JSON.stringify(snapshot, null, 2), "utf8");
|
|
448
|
+
return snapshot;
|
|
449
|
+
}
|
|
450
|
+
function readTaskProjection(projectRoot) {
|
|
451
|
+
const file = projectionPath(projectRoot);
|
|
452
|
+
if (!existsSync3(file))
|
|
453
|
+
return null;
|
|
454
|
+
try {
|
|
455
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
456
|
+
return parsed && parsed.version === 1 && Array.isArray(parsed.tasks) ? parsed : null;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function refreshTaskProjection(projectRoot, input) {
|
|
462
|
+
const tasks = typeof input.tasks === "function" ? await input.tasks() : input.tasks;
|
|
463
|
+
const activeRuns = typeof input.activeRuns === "function" ? await input.activeRuns() : input.activeRuns;
|
|
464
|
+
return writeTaskProjection(projectRoot, {
|
|
465
|
+
source: input.source,
|
|
466
|
+
reason: input.reason ?? "refresh",
|
|
467
|
+
tasks,
|
|
468
|
+
activeRuns
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
473
|
+
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
474
|
+
|
|
475
|
+
// packages/server/src/server-helpers/broadcasters.ts
|
|
476
|
+
import { RIG_WS_CHANNELS } from "@rig/contracts";
|
|
477
|
+
|
|
478
|
+
// packages/server/src/server-helpers/run-writers.ts
|
|
479
|
+
import { resolve as resolve6 } from "path";
|
|
480
|
+
import {
|
|
481
|
+
appendJsonlRecord,
|
|
482
|
+
readAuthorityRun as readAuthorityRun3,
|
|
483
|
+
resolveAuthorityRunDir as resolveAuthorityRunDir2,
|
|
484
|
+
writeJsonFile as writeJsonFile2
|
|
485
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
486
|
+
function appendRunTimelineEntry(projectRoot, runId, value) {
|
|
487
|
+
if (!readAuthorityRun3(projectRoot, runId)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
appendJsonlRecord(runTimelinePath(projectRoot, runId), value);
|
|
491
|
+
patchRunRecord(projectRoot, runId, {});
|
|
492
|
+
}
|
|
493
|
+
function patchRunRecord(projectRoot, runId, patch) {
|
|
494
|
+
const current = readAuthorityRun3(projectRoot, runId);
|
|
495
|
+
if (!current) {
|
|
496
|
+
throw new Error(`Run not found: ${runId}`);
|
|
497
|
+
}
|
|
498
|
+
const next = {
|
|
499
|
+
...current,
|
|
500
|
+
...patch,
|
|
501
|
+
updatedAt: normalizeString(patch.updatedAt) ?? new Date().toISOString()
|
|
502
|
+
};
|
|
503
|
+
writeJsonFile2(resolve6(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
|
|
504
|
+
return next;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
508
|
+
var DEFAULT_TERMINAL_SHELL = process.env.SHELL || "/bin/zsh";
|
|
509
|
+
|
|
510
|
+
// packages/server/src/server-helpers/run-mutations.ts
|
|
511
|
+
import { loadConfig } from "@rig/core/load-config";
|
|
512
|
+
import {
|
|
513
|
+
listAuthorityRuns as listAuthorityRuns4,
|
|
514
|
+
readAuthorityRun as readAuthorityRun4,
|
|
515
|
+
resolveAuthorityRunDir as resolveAuthorityRunDir3,
|
|
516
|
+
writeJsonFile as writeJsonFile3
|
|
517
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
518
|
+
import { readPublishedRigServerStateSync } from "@rig/runtime/local-server";
|
|
519
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot3 } from "@rig/runtime/control-plane/native/utils";
|
|
520
|
+
import {
|
|
521
|
+
buildTaskRunLifecycleComment,
|
|
522
|
+
updateConfiguredTaskSourceTask
|
|
523
|
+
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
524
|
+
|
|
525
|
+
// packages/server/src/scheduler.ts
|
|
526
|
+
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
527
|
+
var TERMINAL_RUN_STATUSES = new Set(["done", "completed", "error", "failed", "stopped", "cancelled"]);
|
|
528
|
+
var RUNNABLE_TASK_STATUSES = new Set(["draft", "open", "ready", "queued"]);
|
|
529
|
+
var REMOTE_READY_STATUSES = new Set(["ready", "idle", "connected"]);
|
|
530
|
+
|
|
531
|
+
// packages/server/src/server-helpers/validation-failure.ts
|
|
532
|
+
import {
|
|
533
|
+
readJsonFile as readJsonFile2,
|
|
534
|
+
resolveTaskArtifactDirs
|
|
535
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
536
|
+
|
|
537
|
+
// packages/server/src/server-helpers/github-auth-store.ts
|
|
538
|
+
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
539
|
+
import { resolve as resolve7 } from "path";
|
|
540
|
+
function cleanString(value) {
|
|
541
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
542
|
+
}
|
|
543
|
+
function cleanScopes(value) {
|
|
544
|
+
if (!Array.isArray(value))
|
|
545
|
+
return [];
|
|
546
|
+
return value.flatMap((entry) => {
|
|
547
|
+
const clean = cleanString(entry);
|
|
548
|
+
return clean ? [clean] : [];
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function readStoredAuth(stateFile) {
|
|
552
|
+
if (!existsSync4(stateFile))
|
|
553
|
+
return {};
|
|
554
|
+
try {
|
|
555
|
+
const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
|
|
556
|
+
return {
|
|
557
|
+
...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
|
|
558
|
+
login: cleanString(parsed.login),
|
|
559
|
+
userId: cleanString(parsed.userId),
|
|
560
|
+
scopes: cleanScopes(parsed.scopes),
|
|
561
|
+
selectedRepo: cleanString(parsed.selectedRepo),
|
|
562
|
+
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
563
|
+
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
564
|
+
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
565
|
+
};
|
|
566
|
+
} catch {
|
|
567
|
+
return {};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function parsePendingDevice(value) {
|
|
571
|
+
if (!value || typeof value !== "object")
|
|
572
|
+
return null;
|
|
573
|
+
const record = value;
|
|
574
|
+
const pollId = cleanString(record.pollId);
|
|
575
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
576
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
577
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
578
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
579
|
+
return null;
|
|
580
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
581
|
+
}
|
|
582
|
+
function writeStoredAuth(stateFile, payload) {
|
|
583
|
+
mkdirSync3(resolve7(stateFile, ".."), { recursive: true });
|
|
584
|
+
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
585
|
+
`, { encoding: "utf8", mode: 384 });
|
|
586
|
+
try {
|
|
587
|
+
chmodSync(stateFile, 384);
|
|
588
|
+
} catch {}
|
|
589
|
+
}
|
|
590
|
+
function resolveGitHubAuthStateFile(projectRoot) {
|
|
591
|
+
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
592
|
+
}
|
|
593
|
+
function createGitHubAuthStore(projectRoot) {
|
|
594
|
+
const stateFile = resolveGitHubAuthStateFile(projectRoot);
|
|
595
|
+
return {
|
|
596
|
+
stateFile,
|
|
597
|
+
status(options) {
|
|
598
|
+
const stored = readStoredAuth(stateFile);
|
|
599
|
+
const token = cleanString(stored.token);
|
|
600
|
+
return {
|
|
601
|
+
signedIn: Boolean(token),
|
|
602
|
+
login: cleanString(stored.login),
|
|
603
|
+
userId: cleanString(stored.userId),
|
|
604
|
+
scopes: cleanScopes(stored.scopes),
|
|
605
|
+
selectedRepo: cleanString(stored.selectedRepo),
|
|
606
|
+
oauthConfigured: options?.oauthConfigured === true,
|
|
607
|
+
tokenSource: token ? stored.tokenSource ?? "manual-token" : null
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
readToken() {
|
|
611
|
+
return cleanString(readStoredAuth(stateFile).token);
|
|
612
|
+
},
|
|
613
|
+
saveToken(input) {
|
|
614
|
+
const previous = readStoredAuth(stateFile);
|
|
615
|
+
writeStoredAuth(stateFile, {
|
|
616
|
+
...previous,
|
|
617
|
+
token: input.token,
|
|
618
|
+
tokenSource: input.tokenSource,
|
|
619
|
+
login: input.login ?? null,
|
|
620
|
+
userId: input.userId ?? null,
|
|
621
|
+
scopes: input.scopes ?? [],
|
|
622
|
+
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
623
|
+
pendingDevice: null,
|
|
624
|
+
updatedAt: new Date().toISOString()
|
|
625
|
+
});
|
|
626
|
+
},
|
|
627
|
+
savePendingDevice(input) {
|
|
628
|
+
const previous = readStoredAuth(stateFile);
|
|
629
|
+
writeStoredAuth(stateFile, {
|
|
630
|
+
...previous,
|
|
631
|
+
pendingDevice: input,
|
|
632
|
+
updatedAt: new Date().toISOString()
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
saveSelectedRepo(selectedRepo) {
|
|
636
|
+
const previous = readStoredAuth(stateFile);
|
|
637
|
+
writeStoredAuth(stateFile, {
|
|
638
|
+
...previous,
|
|
639
|
+
selectedRepo: selectedRepo ?? null,
|
|
640
|
+
updatedAt: new Date().toISOString()
|
|
641
|
+
});
|
|
642
|
+
},
|
|
643
|
+
readPendingDevice(pollId) {
|
|
644
|
+
const pending = readStoredAuth(stateFile).pendingDevice ?? null;
|
|
645
|
+
if (!pending || pending.pollId !== pollId)
|
|
646
|
+
return null;
|
|
647
|
+
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
648
|
+
return null;
|
|
649
|
+
return pending;
|
|
650
|
+
},
|
|
651
|
+
clearPendingDevice() {
|
|
652
|
+
const previous = readStoredAuth(stateFile);
|
|
653
|
+
writeStoredAuth(stateFile, {
|
|
654
|
+
...previous,
|
|
655
|
+
pendingDevice: null,
|
|
656
|
+
updatedAt: new Date().toISOString()
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// packages/server/src/server-helpers/github-projects.ts
|
|
663
|
+
function asRecord(value) {
|
|
664
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
665
|
+
}
|
|
666
|
+
function asString(value) {
|
|
667
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
668
|
+
}
|
|
669
|
+
async function defaultGraphQLFetch(query, variables, token) {
|
|
670
|
+
const response = await fetch("https://api.github.com/graphql", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: {
|
|
673
|
+
"content-type": "application/json",
|
|
674
|
+
authorization: `Bearer ${token}`,
|
|
675
|
+
accept: "application/vnd.github+json"
|
|
676
|
+
},
|
|
677
|
+
body: JSON.stringify({ query, variables })
|
|
678
|
+
});
|
|
679
|
+
const json = await response.json().catch(() => ({}));
|
|
680
|
+
if (!response.ok || json.errors) {
|
|
681
|
+
throw new Error(`GitHub Projects GraphQL request failed: ${JSON.stringify(json.errors ?? { status: response.status })}`);
|
|
682
|
+
}
|
|
683
|
+
return json.data;
|
|
684
|
+
}
|
|
685
|
+
async function resolveProjectStatusField(input) {
|
|
686
|
+
const query = `
|
|
687
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
688
|
+
node(id: $projectId) {
|
|
689
|
+
... on ProjectV2 {
|
|
690
|
+
fields(first: 50) {
|
|
691
|
+
nodes {
|
|
692
|
+
... on ProjectV2FieldCommon { id name }
|
|
693
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
`;
|
|
700
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
701
|
+
const data = await fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
702
|
+
const fields = asRecord(asRecord(asRecord(data)?.node)?.fields)?.nodes;
|
|
703
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
704
|
+
const record = asRecord(node);
|
|
705
|
+
if (asString(record?.name)?.toLowerCase() !== "status")
|
|
706
|
+
continue;
|
|
707
|
+
const id = asString(record?.id);
|
|
708
|
+
if (!id)
|
|
709
|
+
continue;
|
|
710
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
711
|
+
const optionRecord = asRecord(option);
|
|
712
|
+
const optionId = asString(optionRecord?.id);
|
|
713
|
+
const name = asString(optionRecord?.name);
|
|
714
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
715
|
+
}) : [];
|
|
716
|
+
return { id, name: "Status", options };
|
|
717
|
+
}
|
|
718
|
+
throw new Error(`GitHub Project ${input.projectId} does not expose a Status single-select field.`);
|
|
719
|
+
}
|
|
720
|
+
async function ensureIssueProjectItem(input) {
|
|
721
|
+
const query = `
|
|
722
|
+
query RigFindProjectIssueItem($projectId: ID!, $issueNodeId: ID!) {
|
|
723
|
+
node(id: $projectId) {
|
|
724
|
+
... on ProjectV2 {
|
|
725
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
`;
|
|
730
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
731
|
+
const data = await fetchGraphQL(query, { projectId: input.projectId, issueNodeId: input.issueNodeId }, input.token);
|
|
732
|
+
const nodes = asRecord(asRecord(asRecord(data)?.node)?.items)?.nodes;
|
|
733
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
734
|
+
const record = asRecord(node);
|
|
735
|
+
const content = asRecord(record?.content);
|
|
736
|
+
if (asString(content?.id) === input.issueNodeId) {
|
|
737
|
+
const id2 = asString(record?.id);
|
|
738
|
+
if (id2)
|
|
739
|
+
return { id: id2, created: false };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const mutation = `
|
|
743
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
744
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
745
|
+
}
|
|
746
|
+
`;
|
|
747
|
+
const created = await fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
748
|
+
const addResult = asRecord(asRecord(created)?.addProjectV2ItemById);
|
|
749
|
+
const id = asString(asRecord(addResult?.item)?.id);
|
|
750
|
+
if (!id)
|
|
751
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
752
|
+
return { id, created: true };
|
|
753
|
+
}
|
|
754
|
+
async function updateIssueProjectStatus(input) {
|
|
755
|
+
const mutation = `
|
|
756
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
757
|
+
updateProjectV2ItemFieldValue(input: {
|
|
758
|
+
projectId: $projectId,
|
|
759
|
+
itemId: $itemId,
|
|
760
|
+
fieldId: $fieldId,
|
|
761
|
+
value: { singleSelectOptionId: $optionId }
|
|
762
|
+
}) { projectV2Item { id } }
|
|
763
|
+
}
|
|
764
|
+
`;
|
|
765
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
766
|
+
await fetchGraphQL(mutation, {
|
|
767
|
+
projectId: input.projectId,
|
|
768
|
+
itemId: input.itemId,
|
|
769
|
+
fieldId: input.fieldId,
|
|
770
|
+
optionId: input.optionId
|
|
771
|
+
}, input.token);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// packages/server/src/server-helpers/github-project-status-sync.ts
|
|
775
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
776
|
+
running: "In Progress",
|
|
777
|
+
prOpen: "In Review",
|
|
778
|
+
ciFixing: "In Review",
|
|
779
|
+
done: "Done",
|
|
780
|
+
needsAttention: "Needs Attention"
|
|
781
|
+
};
|
|
782
|
+
function lifecycleStatusForTaskStatus(status) {
|
|
783
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
784
|
+
if (!normalized)
|
|
785
|
+
return null;
|
|
786
|
+
if (normalized === "closed" || normalized === "done")
|
|
787
|
+
return "done";
|
|
788
|
+
if (normalized === "under_review" || normalized === "review" || normalized === "pr_open")
|
|
789
|
+
return "prOpen";
|
|
790
|
+
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
791
|
+
return "ciFixing";
|
|
792
|
+
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
793
|
+
return "needsAttention";
|
|
794
|
+
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
795
|
+
return "running";
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
function cleanString2(value) {
|
|
799
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
800
|
+
}
|
|
801
|
+
function projectConfigFrom(config) {
|
|
802
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
803
|
+
return null;
|
|
804
|
+
const root = config;
|
|
805
|
+
return root.github?.projects ?? null;
|
|
806
|
+
}
|
|
807
|
+
async function syncGitHubProjectStatusForTaskUpdate(input) {
|
|
808
|
+
const projects = projectConfigFrom(input.config);
|
|
809
|
+
if (!projects?.enabled)
|
|
810
|
+
return { synced: false, reason: "project-sync-disabled" };
|
|
811
|
+
const projectId = cleanString2(projects.projectId);
|
|
812
|
+
if (!projectId)
|
|
813
|
+
return { synced: false, reason: "missing-project-id" };
|
|
814
|
+
const token = cleanString2(input.token);
|
|
815
|
+
if (!token)
|
|
816
|
+
return { synced: false, reason: "missing-token" };
|
|
817
|
+
const lifecycleStatus = lifecycleStatusForTaskStatus(input.status);
|
|
818
|
+
if (!lifecycleStatus)
|
|
819
|
+
return { synced: false, reason: "missing-status" };
|
|
820
|
+
const issueNodeId = cleanString2(input.issueNodeId);
|
|
821
|
+
if (!issueNodeId)
|
|
822
|
+
return { synced: false, reason: "missing-issue-node-id" };
|
|
823
|
+
const projectStatus = cleanString2(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
824
|
+
const field = await resolveProjectStatusField({ projectId, token, fetchGraphQL: input.fetchGraphQL });
|
|
825
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
826
|
+
if (!option)
|
|
827
|
+
return { synced: false, reason: "missing-project-status-option" };
|
|
828
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token, fetchGraphQL: input.fetchGraphQL });
|
|
829
|
+
await updateIssueProjectStatus({
|
|
830
|
+
projectId,
|
|
831
|
+
itemId: item.id,
|
|
832
|
+
fieldId: cleanString2(projects.statusFieldId) ?? field.id,
|
|
833
|
+
optionId: option.id,
|
|
834
|
+
token,
|
|
835
|
+
fetchGraphQL: input.fetchGraphQL
|
|
836
|
+
});
|
|
837
|
+
return { synced: true, lifecycleStatus, projectStatus, itemId: item.id };
|
|
838
|
+
}
|
|
839
|
+
function extractGitHubIssueNodeId(task) {
|
|
840
|
+
if (!task || typeof task !== "object" || Array.isArray(task))
|
|
841
|
+
return null;
|
|
842
|
+
const record = task;
|
|
843
|
+
const direct = cleanString2(record.issueNodeId) ?? cleanString2(record.nodeId) ?? cleanString2(record.node_id);
|
|
844
|
+
if (direct)
|
|
845
|
+
return direct;
|
|
846
|
+
const raw = record.raw;
|
|
847
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
848
|
+
return null;
|
|
849
|
+
const rawRecord = raw;
|
|
850
|
+
return cleanString2(rawRecord.id) ?? cleanString2(rawRecord.nodeId) ?? cleanString2(rawRecord.node_id);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// packages/server/src/server-helpers/run-mutations.ts
|
|
854
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
855
|
+
"completed",
|
|
856
|
+
"complete",
|
|
857
|
+
"done",
|
|
858
|
+
"merged",
|
|
859
|
+
"closed",
|
|
860
|
+
"failed",
|
|
861
|
+
"cancelled",
|
|
862
|
+
"canceled",
|
|
863
|
+
"needs_attention",
|
|
864
|
+
"needs-attention",
|
|
865
|
+
"stopped"
|
|
866
|
+
]);
|
|
867
|
+
var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
|
|
868
|
+
|
|
869
|
+
// packages/server/src/server-helpers/ws-router.ts
|
|
870
|
+
import {
|
|
871
|
+
ORCHESTRATION_WS_CHANNELS,
|
|
872
|
+
ORCHESTRATION_WS_METHODS,
|
|
873
|
+
RIG_WS_METHODS,
|
|
874
|
+
WS_METHODS
|
|
875
|
+
} from "@rig/contracts";
|
|
876
|
+
import {
|
|
877
|
+
listManagedRemoteEndpoints,
|
|
878
|
+
removeManagedRemoteEndpoint,
|
|
879
|
+
resolveRemoteEndpoint,
|
|
880
|
+
upsertManagedRemoteEndpoint,
|
|
881
|
+
RemoteWsClient
|
|
882
|
+
} from "@rig/runtime/control-plane/remote";
|
|
883
|
+
import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
|
|
884
|
+
import { readAuthorityRun as readAuthorityRun5 } from "@rig/runtime/control-plane/authority-files";
|
|
885
|
+
|
|
886
|
+
// packages/server/src/server-helpers/inspector-jobs.ts
|
|
887
|
+
import { readJsonFile as readJsonFile3 } from "@rig/runtime/control-plane/authority-files";
|
|
888
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
|
|
889
|
+
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
|
|
890
|
+
|
|
891
|
+
// packages/server/src/inspector/discovery.ts
|
|
892
|
+
import {
|
|
893
|
+
runStatus
|
|
894
|
+
} from "@rig/runtime/control-plane/native/run-ops";
|
|
895
|
+
import {
|
|
896
|
+
listAuthorityRuns as listAuthorityRuns5,
|
|
897
|
+
readAuthorityRun as readAuthorityRun6
|
|
898
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
899
|
+
|
|
900
|
+
// packages/server/src/inspector/service.ts
|
|
901
|
+
var ACTIVE_RUN_STATUSES = new Set(["preparing", "running", "validating", "reviewing"]);
|
|
902
|
+
|
|
903
|
+
// packages/server/src/inspector/upstream-sync.ts
|
|
904
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
|
|
905
|
+
var UPSTREAM_VALIDATION_DESCRIPTIONS = {
|
|
906
|
+
"integration:hg-auth-backport": "Preserves the upstream auth hardening cluster: nonce-backed node-client login, signature-aware JWT verification, shared-token onboarding semantics, and regression coverage.",
|
|
907
|
+
"integration:hg-core-security-backport": "Preserves the vendored core-app and backend security fixes for OTP validation, NoSQL/operator hardening, AES/encryption behavior, and rate-limit or redirect-state protections.",
|
|
908
|
+
"integration:hg-boundary-hardening": "Preserves boundary-safe CORS behavior and credential lookup error hygiene across the vendored backend and the extracted credentials-service analogue.",
|
|
909
|
+
"integration:hg-uudl-runtime": "Preserves the upstream uudl runtime/package fixes: production dependency placement, path-resolution/runtime bootstrap behavior, AWS/MSK environment detection, KMS coverage, and a compiling UUDL tree.",
|
|
910
|
+
"boundary:hg-auth-triage": "Requires a durable keep/adapt/reject triage record for the post-import auth policy and mobile-wallet SIWE commits so the watcher does not keep rediscovering the same ambiguity.",
|
|
911
|
+
"boundary:hg-upstream-triage": "Requires a durable keep/adapt/reject triage record for service-relevant upstream commits that do not match the current portable catalog, including commit hashes, touched paths, and follow-up routing decisions."
|
|
912
|
+
};
|
|
913
|
+
var CLUSTERS = {
|
|
914
|
+
"auth-hardening": {
|
|
915
|
+
classification: "portable-now",
|
|
916
|
+
title: "[HG-001] Preserve upstream auth and shared-token hardening across extracted auth work",
|
|
917
|
+
description: "Backport clearly portable auth changes introduced upstream after imported revision 58b56e15: 3196d5c (node-client login nonce), bb5b74f (JWT signature validation), and 92011ead (shared token verification for wallet signup onboarding). Apply them where they belong in this repo: hp-auth-service, hp-gateway if verification semantics surface there, and the upstream monorepo auth owner flows for the remaining wallet-login/shared-token work. This task must preserve the security behavior, not just copy code. It should explicitly reconcile with bd-k0y-10, bd-k0y-11, bd-k0y-12, and bd-k0y-13.",
|
|
918
|
+
acceptanceCriteria: "Map upstream commits 3196d5c, bb5b74f, and 92011ead to exact local targets; preserve nonce-backed node-client login, JWT signature validation, and shared-token verification behavior; add automated tests that fail without the hardening; document any deliberate non-ports.",
|
|
919
|
+
role: "extractor",
|
|
920
|
+
scope: [
|
|
921
|
+
"repos/spliter-monorepo/microservices/hp-auth-service/**",
|
|
922
|
+
"repos/spliter-monorepo/microservices/hp-gateway/**",
|
|
923
|
+
"repos/spliter-monorepo/humoongate/moongate/core-app/**"
|
|
924
|
+
],
|
|
925
|
+
validation: ["integration:hg-auth-backport", "boundary:changed-files"],
|
|
926
|
+
validationDescriptions: {
|
|
927
|
+
"integration:hg-auth-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-auth-backport"]
|
|
928
|
+
},
|
|
929
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:auth"]
|
|
930
|
+
},
|
|
931
|
+
"core-security": {
|
|
932
|
+
classification: "portable-now",
|
|
933
|
+
title: "[HG-002] Backport core-app and backend security fixes from post-import upstream",
|
|
934
|
+
description: "Backport the clearly portable security fixes from efdae709 (OTP validation bypass), f63830d (NoSQL operator injection hardening), d7f6919 (AES encryption), and af86ac16 (three critical/high vulnerabilities). Preserve the behavior in the vendored auth owner and the touched backend query surfaces instead of treating these as optional cleanups. The high-value touched files from af86ac16 must be reviewed and ported or consciously waived with rationale.",
|
|
935
|
+
acceptanceCriteria: "Review and port the exact security behaviors from efdae709, f63830d, d7f6919, and af86ac16; cover the listed core-app and backend touched files; add regression tests for OTP bypass, NoSQL operator injection, AES/encryption behavior, and rate-limit or redirect-state hardening; document any consciously waived upstream changes.",
|
|
936
|
+
role: "mechanic",
|
|
937
|
+
scope: [
|
|
938
|
+
"repos/spliter-monorepo/humoongate/moongate/core-app/**",
|
|
939
|
+
"repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**"
|
|
940
|
+
],
|
|
941
|
+
validation: ["integration:hg-core-security-backport", "boundary:changed-files"],
|
|
942
|
+
validationDescriptions: {
|
|
943
|
+
"integration:hg-core-security-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-core-security-backport"]
|
|
944
|
+
},
|
|
945
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:security"]
|
|
946
|
+
},
|
|
947
|
+
"boundary-hardening": {
|
|
948
|
+
classification: "portable-now",
|
|
949
|
+
title: "[HG-003] Backport request-boundary hardening for CORS and credential lookup flows",
|
|
950
|
+
description: "Backport the clearly portable boundary hardening from 083bbe4 (CORS config) and 36d52fba (credential lookup error hygiene). Preserve request-boundary behavior in the vendored backend and any analogous extracted credential-facing service seams so attackers cannot learn internals from credential lookup failures or exploit inconsistent CORS policy.",
|
|
951
|
+
acceptanceCriteria: "Port the exact request-boundary behavior from 083bbe4 and 36d52fba; preserve safe CORS policy and credential lookup error hygiene; add tests showing callers do not receive overly detailed credential lookup failures; document any extracted-service analogue that must mirror the behavior.",
|
|
952
|
+
role: "mechanic",
|
|
953
|
+
scope: [
|
|
954
|
+
"repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**",
|
|
955
|
+
"repos/spliter-monorepo/microservices/hp-credentials-service/**"
|
|
956
|
+
],
|
|
957
|
+
validation: ["integration:hg-boundary-hardening", "boundary:changed-files"],
|
|
958
|
+
validationDescriptions: {
|
|
959
|
+
"integration:hg-boundary-hardening": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-boundary-hardening"]
|
|
960
|
+
},
|
|
961
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:boundary"]
|
|
962
|
+
},
|
|
963
|
+
"uudl-runtime": {
|
|
964
|
+
classification: "portable-now",
|
|
965
|
+
title: "[HG-004] Backport uudl runtime and packaging fixes from upstream",
|
|
966
|
+
description: "Backport the clearly portable uudl runtime fixes from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03. Preserve the runtime behavior rather than cherry-picking filenames: production dependency placement, tsconfig-paths resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS wiring all need to be reflected in whichever UUDL runtime this repo still owns.",
|
|
967
|
+
acceptanceCriteria: "Port the runtime behaviors from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03; preserve production dependency placement, tsconfig-paths runtime resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS configuration; add runtime-focused tests or smoke coverage for each preserved behavior.",
|
|
968
|
+
role: "extractor",
|
|
969
|
+
scope: ["repos/spliter-monorepo/humoongate/humanity/hp-uudl/**"],
|
|
970
|
+
validation: ["integration:hg-uudl-runtime", "boundary:changed-files"],
|
|
971
|
+
validationDescriptions: {
|
|
972
|
+
"integration:hg-uudl-runtime": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-uudl-runtime"]
|
|
973
|
+
},
|
|
974
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:uudl"]
|
|
975
|
+
},
|
|
976
|
+
"auth-triage": {
|
|
977
|
+
classification: "needs-human-triage",
|
|
978
|
+
title: "[HG-005] Triage post-import auth policy deltas and mobile-wallet SIWE changes",
|
|
979
|
+
description: "Review the upstream changes that are relevant but not safely auto-portable: 1262b75 (JWT_EXPIRES_IN 1d -> 7d), 15036424 (unlink-wallet endpoint and Wallet.default fix), and d4a47015 (unlink-wallet error handling). Produce an explicit keep/adapt/reject decision for each commit with rationale tied to the extracted auth plan, core-app ownership, and current threat model, and record the result in artifacts/bd-2ztk.5/decision-log.md so future agents do not silently re-litigate these decisions.",
|
|
980
|
+
acceptanceCriteria: "For 1262b75, 15036424, and d4a47015, produce explicit keep/adapt/reject decisions with rationale tied to auth ownership and threat model; record the decision in artifacts/bd-2ztk.5/decision-log.md; identify any follow-on implementation tasks if the answer is keep or adapt.",
|
|
981
|
+
role: "architect",
|
|
982
|
+
scope: ["artifacts/bd-2ztk.5/**", "docs/research/**"],
|
|
983
|
+
validation: ["boundary:hg-auth-triage"],
|
|
984
|
+
validationDescriptions: {
|
|
985
|
+
"boundary:hg-auth-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-auth-triage"]
|
|
986
|
+
},
|
|
987
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
|
|
988
|
+
},
|
|
989
|
+
"generic-triage": {
|
|
990
|
+
classification: "needs-human-triage",
|
|
991
|
+
title: "[HG-007] Triage uncatalogued service-relevant upstream commits",
|
|
992
|
+
description: "Review future upstream commits that touch service-relevant paths but do not match the current portable-now catalog. For each commit or batch, produce keep/adapt/reject decisions with rationale, cite the touched paths and why they matter here, and either route them into an existing backport task or open a new follow-up task when they are worth preserving.",
|
|
993
|
+
acceptanceCriteria: "For each uncatalogued service-relevant upstream commit or batch, record keep/adapt/reject decisions with rationale in artifacts/bd-2ztk.7/decision-log.md; cite the commit hashes and touched paths; and either attach the work to an existing backport task or open a new follow-up task when preservation is warranted.",
|
|
994
|
+
role: "architect",
|
|
995
|
+
scope: ["artifacts/bd-2ztk.7/**", "docs/research/**"],
|
|
996
|
+
validation: ["boundary:hg-upstream-triage"],
|
|
997
|
+
validationDescriptions: {
|
|
998
|
+
"boundary:hg-upstream-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-upstream-triage"]
|
|
999
|
+
},
|
|
1000
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// packages/server/src/server-helpers/inspector-agent-lifecycle.ts
|
|
1005
|
+
function inspectorAgentLifecycleSnapshot(input) {
|
|
1006
|
+
const agentSnapshot = input.agent?.snapshot() ?? null;
|
|
1007
|
+
return {
|
|
1008
|
+
service: {
|
|
1009
|
+
available: input.inspector !== null,
|
|
1010
|
+
running: input.inspector?.isRunning() ?? false
|
|
1011
|
+
},
|
|
1012
|
+
agent: {
|
|
1013
|
+
available: input.agent !== null,
|
|
1014
|
+
status: input.retry?.disabled ? "disabled" : agentSnapshot?.status ?? "unavailable",
|
|
1015
|
+
retry: input.retry
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// packages/server/src/server.ts
|
|
1021
|
+
var RIG_WORKSPACE_ID = "rig-local-workspace";
|
|
1022
|
+
var serverPathEnvQueue = Promise.resolve();
|
|
1023
|
+
if (false) {}
|
|
1024
|
+
|
|
1025
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
1026
|
+
import {
|
|
1027
|
+
listAuthorityRuns as listAuthorityRuns7,
|
|
1028
|
+
readAuthorityRun as readAuthorityRun8,
|
|
1029
|
+
resolveAuthorityPaths,
|
|
1030
|
+
writeJsonFile as writeJsonFile4
|
|
1031
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
1032
|
+
import {
|
|
1033
|
+
mutateWorkspaceServiceFabric as mutateWorkspaceServiceFabric2,
|
|
1034
|
+
readTaskArtifactPreview as readTaskArtifactPreview2,
|
|
1035
|
+
readWorkspaceRemoteFleet as readWorkspaceRemoteFleet2,
|
|
1036
|
+
readWorkspaceTopology as readWorkspaceTopology2
|
|
1037
|
+
} from "@rig/runtime/control-plane/native/workspace-ops";
|
|
1038
|
+
import {
|
|
1039
|
+
doctorManagedRemoteEndpoints,
|
|
1040
|
+
listManagedRemoteEndpoints as listManagedRemoteEndpoints2,
|
|
1041
|
+
removeManagedRemoteEndpoint as removeManagedRemoteEndpoint2,
|
|
1042
|
+
resolveRemoteEndpoint as resolveRemoteEndpoint2,
|
|
1043
|
+
updateManagedRemoteEndpointInAuthority,
|
|
1044
|
+
upsertManagedRemoteEndpoint as upsertManagedRemoteEndpoint2,
|
|
1045
|
+
RemoteWsClient as RemoteWsClient2
|
|
1046
|
+
} from "@rig/runtime/control-plane/remote";
|
|
1047
|
+
|
|
1048
|
+
// packages/server/src/server-helpers/run-steering.ts
|
|
1049
|
+
import { dirname as dirname3, resolve as resolve8 } from "path";
|
|
1050
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "fs";
|
|
1051
|
+
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun7, resolveAuthorityRunDir as resolveAuthorityRunDir4 } from "@rig/runtime/control-plane/authority-files";
|
|
1052
|
+
var steeringSequence = 0;
|
|
1053
|
+
function runSteeringPath(projectRoot, runId) {
|
|
1054
|
+
return resolve8(resolveAuthorityRunDir4(projectRoot, runId), "steering.jsonl");
|
|
1055
|
+
}
|
|
1056
|
+
function readJsonl(path) {
|
|
1057
|
+
if (!existsSync5(path))
|
|
1058
|
+
return [];
|
|
1059
|
+
return readFileSync3(path, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
1060
|
+
try {
|
|
1061
|
+
const value = JSON.parse(line);
|
|
1062
|
+
return value && typeof value === "object" && !Array.isArray(value) ? [value] : [];
|
|
1063
|
+
} catch {
|
|
1064
|
+
return [];
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
function normalizeQueuedMessage(record) {
|
|
1069
|
+
const id = typeof record.id === "string" ? record.id : null;
|
|
1070
|
+
const runId = typeof record.runId === "string" ? record.runId : null;
|
|
1071
|
+
const message = typeof record.message === "string" ? record.message : null;
|
|
1072
|
+
if (!id || !runId || !message)
|
|
1073
|
+
return null;
|
|
1074
|
+
return {
|
|
1075
|
+
id,
|
|
1076
|
+
runId,
|
|
1077
|
+
message,
|
|
1078
|
+
actor: typeof record.actor === "string" ? record.actor : "operator",
|
|
1079
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : new Date().toISOString(),
|
|
1080
|
+
delivered: record.delivered === true
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function readQueuedRunSteeringMessages(projectRoot, runId) {
|
|
1084
|
+
const latestById = new Map;
|
|
1085
|
+
for (const record of readJsonl(runSteeringPath(projectRoot, runId))) {
|
|
1086
|
+
const message = normalizeQueuedMessage(record);
|
|
1087
|
+
if (message)
|
|
1088
|
+
latestById.set(message.id, message);
|
|
1089
|
+
}
|
|
1090
|
+
return Array.from(latestById.values());
|
|
1091
|
+
}
|
|
1092
|
+
function markQueuedRunSteeringMessagesDelivered(projectRoot, runId, ids) {
|
|
1093
|
+
const requested = new Set(ids.map((id) => id.trim()).filter(Boolean));
|
|
1094
|
+
if (requested.size === 0)
|
|
1095
|
+
return [];
|
|
1096
|
+
const existing = readQueuedRunSteeringMessages(projectRoot, runId);
|
|
1097
|
+
const deliveredAt = new Date().toISOString();
|
|
1098
|
+
const path = runSteeringPath(projectRoot, runId);
|
|
1099
|
+
const delivered = [];
|
|
1100
|
+
for (const entry of existing) {
|
|
1101
|
+
if (!requested.has(entry.id) || entry.delivered)
|
|
1102
|
+
continue;
|
|
1103
|
+
appendJsonlRecord2(path, { ...entry, delivered: true, deliveredAt });
|
|
1104
|
+
delivered.push(entry.id);
|
|
1105
|
+
}
|
|
1106
|
+
if (delivered.length > 0) {
|
|
1107
|
+
appendRunTimelineEntry(projectRoot, runId, {
|
|
1108
|
+
id: `steering-ack:${runId}:${Date.now()}`,
|
|
1109
|
+
type: "rig_bridge_event",
|
|
1110
|
+
text: `Delivered ${delivered.length} steering message${delivered.length === 1 ? "" : "s"}`,
|
|
1111
|
+
createdAt: deliveredAt,
|
|
1112
|
+
state: "completed",
|
|
1113
|
+
payload: { kind: "steering-delivered", messageIds: delivered }
|
|
1114
|
+
});
|
|
1115
|
+
patchRunRecord(projectRoot, runId, { latestSteeringDeliveredAt: deliveredAt });
|
|
1116
|
+
}
|
|
1117
|
+
return delivered;
|
|
1118
|
+
}
|
|
1119
|
+
function queueRunSteeringMessage(projectRoot, runId, input) {
|
|
1120
|
+
const run = readAuthorityRun7(projectRoot, runId);
|
|
1121
|
+
if (!run)
|
|
1122
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1123
|
+
const text = input.message.trim();
|
|
1124
|
+
if (!text)
|
|
1125
|
+
throw new Error("message is required");
|
|
1126
|
+
const createdAt = new Date().toISOString();
|
|
1127
|
+
const entry = {
|
|
1128
|
+
id: `steer:${runId}:${Date.now()}:${++steeringSequence}`,
|
|
1129
|
+
runId,
|
|
1130
|
+
message: text,
|
|
1131
|
+
actor: input.actor?.trim() || "operator",
|
|
1132
|
+
createdAt,
|
|
1133
|
+
delivered: false
|
|
1134
|
+
};
|
|
1135
|
+
const path = runSteeringPath(projectRoot, runId);
|
|
1136
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
1137
|
+
appendJsonlRecord2(path, entry);
|
|
1138
|
+
appendRunTimelineEntry(projectRoot, runId, {
|
|
1139
|
+
id: entry.id,
|
|
1140
|
+
type: "operator_message",
|
|
1141
|
+
text,
|
|
1142
|
+
createdAt,
|
|
1143
|
+
state: "queued"
|
|
1144
|
+
});
|
|
1145
|
+
patchRunRecord(projectRoot, runId, { latestSteeringAt: createdAt });
|
|
1146
|
+
return entry;
|
|
1147
|
+
}
|
|
1148
|
+
function appendRunBridgeEvent(projectRoot, runId, event) {
|
|
1149
|
+
const kind = typeof event.kind === "string" ? event.kind : "event";
|
|
1150
|
+
const createdAt = typeof event.createdAt === "string" ? event.createdAt : new Date().toISOString();
|
|
1151
|
+
if (kind === "status") {
|
|
1152
|
+
patchRunRecord(projectRoot, runId, { statusDetail: event.message ?? event.status ?? null, updatedAt: createdAt });
|
|
1153
|
+
}
|
|
1154
|
+
if (kind === "artifact" || kind === "task-metadata" || kind === "status") {
|
|
1155
|
+
appendRunTimelineEntry(projectRoot, runId, {
|
|
1156
|
+
id: `pi-rig:${kind}:${Date.now()}`,
|
|
1157
|
+
type: "rig_bridge_event",
|
|
1158
|
+
text: typeof event.message === "string" ? event.message : JSON.stringify(event),
|
|
1159
|
+
createdAt,
|
|
1160
|
+
state: "completed",
|
|
1161
|
+
payload: event
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
1167
|
+
import { buildRigInitConfigSource } from "@rig/core";
|
|
1168
|
+
import {
|
|
1169
|
+
buildTaskRunLifecycleComment as buildTaskRunLifecycleComment2,
|
|
1170
|
+
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
1171
|
+
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1172
|
+
|
|
1173
|
+
// packages/server/src/server-helpers/project-registry.ts
|
|
1174
|
+
import { createHash } from "crypto";
|
|
1175
|
+
import { spawnSync } from "child_process";
|
|
1176
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
|
|
1177
|
+
import { dirname as dirname4, resolve as resolve9 } from "path";
|
|
1178
|
+
function normalizeRepoSlug(value) {
|
|
1179
|
+
const trimmed = value.trim();
|
|
1180
|
+
return /^[^/\s]+\/[^/\s]+$/.test(trimmed) ? trimmed : null;
|
|
1181
|
+
}
|
|
1182
|
+
function registryPath(projectRoot) {
|
|
1183
|
+
return resolve9(projectRoot, ".rig", "state", "projects.json");
|
|
1184
|
+
}
|
|
1185
|
+
function readRegistry(projectRoot) {
|
|
1186
|
+
const path = registryPath(projectRoot);
|
|
1187
|
+
if (!existsSync6(path))
|
|
1188
|
+
return {};
|
|
1189
|
+
try {
|
|
1190
|
+
const payload = JSON.parse(readFileSync4(path, "utf8"));
|
|
1191
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1192
|
+
return {};
|
|
1193
|
+
const projects = payload.projects;
|
|
1194
|
+
return projects && typeof projects === "object" && !Array.isArray(projects) ? projects : {};
|
|
1195
|
+
} catch {
|
|
1196
|
+
return {};
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function writeRegistry(projectRoot, projects) {
|
|
1200
|
+
const path = registryPath(projectRoot);
|
|
1201
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
1202
|
+
writeFileSync5(path, `${JSON.stringify({ projects }, null, 2)}
|
|
1203
|
+
`, "utf8");
|
|
1204
|
+
}
|
|
1205
|
+
function resolveConfigPath(projectRoot) {
|
|
1206
|
+
for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
|
|
1207
|
+
const path = resolve9(projectRoot, name);
|
|
1208
|
+
if (existsSync6(path))
|
|
1209
|
+
return path;
|
|
1210
|
+
}
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
function hashFile(path) {
|
|
1214
|
+
if (!path)
|
|
1215
|
+
return null;
|
|
1216
|
+
try {
|
|
1217
|
+
return createHash("sha256").update(readFileSync4(path)).digest("hex");
|
|
1218
|
+
} catch {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function buildConfigStatus(projectRoot) {
|
|
1223
|
+
return resolveConfigPath(projectRoot) ? "valid" : "missing";
|
|
1224
|
+
}
|
|
1225
|
+
function readDefaultBranch(projectRoot) {
|
|
1226
|
+
const origin = spawnSync("git", ["-C", projectRoot, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { encoding: "utf8", timeout: 5000 });
|
|
1227
|
+
if (origin.status === 0 && origin.stdout.trim())
|
|
1228
|
+
return origin.stdout.trim().replace(/^origin\//, "");
|
|
1229
|
+
const head = spawnSync("git", ["-C", projectRoot, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8", timeout: 5000 });
|
|
1230
|
+
return head.status === 0 && head.stdout.trim() && head.stdout.trim() !== "HEAD" ? head.stdout.trim() : null;
|
|
1231
|
+
}
|
|
1232
|
+
function buildRunSummary(projectRoot) {
|
|
1233
|
+
const runsDir = resolve9(projectRoot, ".rig", "runs");
|
|
1234
|
+
try {
|
|
1235
|
+
const runs = readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
1236
|
+
try {
|
|
1237
|
+
const run = JSON.parse(readFileSync4(resolve9(runsDir, entry.name, "run.json"), "utf8"));
|
|
1238
|
+
return [{ runId: typeof run.runId === "string" ? run.runId : entry.name, status: typeof run.status === "string" ? run.status : "unknown", updatedAt: typeof run.updatedAt === "string" ? run.updatedAt : "" }];
|
|
1239
|
+
} catch {
|
|
1240
|
+
return [];
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
const active = runs.filter((run) => !["completed", "failed", "cancelled", "canceled", "stopped", "closed", "merged", "needs_attention"].includes(run.status.toLowerCase())).length;
|
|
1244
|
+
const latest = runs.toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0]?.runId ?? null;
|
|
1245
|
+
return { total: runs.length, active, latestRunId: latest };
|
|
1246
|
+
} catch {
|
|
1247
|
+
return { total: 0, active: 0, latestRunId: null };
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function getProjectRecord(projectRoot, repoSlug) {
|
|
1251
|
+
const normalized = normalizeRepoSlug(repoSlug);
|
|
1252
|
+
if (!normalized)
|
|
1253
|
+
return null;
|
|
1254
|
+
return readRegistry(projectRoot)[normalized] ?? null;
|
|
1255
|
+
}
|
|
1256
|
+
function upsertProjectRecord(projectRoot, input) {
|
|
1257
|
+
const repoSlug = normalizeRepoSlug(input.repoSlug);
|
|
1258
|
+
if (!repoSlug)
|
|
1259
|
+
throw new Error(`Invalid repo slug: ${input.repoSlug}`);
|
|
1260
|
+
const now = new Date().toISOString();
|
|
1261
|
+
const projects = readRegistry(projectRoot);
|
|
1262
|
+
const existing = projects[repoSlug];
|
|
1263
|
+
const checkout = input.checkout ? { ...input.checkout, createdAt: input.checkout.createdAt ?? now } : null;
|
|
1264
|
+
const checkouts = checkout ? [...(existing?.checkouts ?? []).filter((entry) => !(entry.kind === checkout.kind && entry.path === checkout.path)), checkout] : existing?.checkouts ?? [];
|
|
1265
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
1266
|
+
const configStatus = buildConfigStatus(projectRoot);
|
|
1267
|
+
const authStatus = input.githubAuthStatus ?? existing?.github.authStatus ?? "unauthenticated";
|
|
1268
|
+
const record = {
|
|
1269
|
+
repoSlug,
|
|
1270
|
+
defaultBranch: readDefaultBranch(projectRoot) ?? existing?.defaultBranch ?? null,
|
|
1271
|
+
checkouts,
|
|
1272
|
+
configStatus,
|
|
1273
|
+
config: { status: configStatus, hash: hashFile(configPath), path: configPath },
|
|
1274
|
+
github: { authStatus },
|
|
1275
|
+
taskSource: existing?.taskSource ?? { health: "unknown", lastCheckedAt: null },
|
|
1276
|
+
runs: buildRunSummary(projectRoot),
|
|
1277
|
+
workers: existing?.workers ?? { localAvailable: true, remoteAvailable: false, remoteCount: 0 },
|
|
1278
|
+
operatorPermissions: existing?.operatorPermissions ?? { canRead: true, canRun: authStatus === "authenticated", canControl: authStatus === "authenticated" },
|
|
1279
|
+
createdAt: existing?.createdAt ?? now,
|
|
1280
|
+
updatedAt: now
|
|
1281
|
+
};
|
|
1282
|
+
projects[repoSlug] = record;
|
|
1283
|
+
writeRegistry(projectRoot, projects);
|
|
1284
|
+
return record;
|
|
1285
|
+
}
|
|
1286
|
+
function linkProjectCheckout(projectRoot, repoSlug, checkout) {
|
|
1287
|
+
return upsertProjectRecord(projectRoot, { repoSlug, checkout });
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// packages/server/src/server-helpers/remote-checkout.ts
|
|
1291
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1292
|
+
import { dirname as dirname5, isAbsolute, relative, resolve as resolve10 } from "path";
|
|
1293
|
+
function safeSlugSegments(repoSlug) {
|
|
1294
|
+
const segments = repoSlug.split("/").map((part) => part.trim()).filter(Boolean);
|
|
1295
|
+
if (segments.length !== 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("\\"))) {
|
|
1296
|
+
throw new Error("repoSlug must be owner/repo");
|
|
1297
|
+
}
|
|
1298
|
+
return segments;
|
|
1299
|
+
}
|
|
1300
|
+
function safeCheckoutKey(value) {
|
|
1301
|
+
const raw = value?.trim();
|
|
1302
|
+
if (!raw)
|
|
1303
|
+
return null;
|
|
1304
|
+
const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
|
|
1305
|
+
return safe || null;
|
|
1306
|
+
}
|
|
1307
|
+
function repoSlugPath(baseDir, repoSlug, checkoutKey) {
|
|
1308
|
+
const key = safeCheckoutKey(checkoutKey);
|
|
1309
|
+
return resolve10(baseDir, ...key ? [key] : [], ...safeSlugSegments(repoSlug));
|
|
1310
|
+
}
|
|
1311
|
+
function sanitizeSnapshotId(value, fallback) {
|
|
1312
|
+
const raw = (value ?? fallback).trim();
|
|
1313
|
+
const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
|
|
1314
|
+
return safe || fallback;
|
|
1315
|
+
}
|
|
1316
|
+
function assertWithinRoot(root, relativePath) {
|
|
1317
|
+
if (!relativePath || isAbsolute(relativePath) || relativePath.includes("\x00")) {
|
|
1318
|
+
throw new Error(`Invalid snapshot file path: ${relativePath}`);
|
|
1319
|
+
}
|
|
1320
|
+
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
1321
|
+
const segments = normalizedRelative.split("/");
|
|
1322
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1323
|
+
throw new Error(`Unsafe snapshot file path: ${relativePath}`);
|
|
1324
|
+
}
|
|
1325
|
+
const target = resolve10(root, ...segments);
|
|
1326
|
+
const rel = relative(root, target);
|
|
1327
|
+
if (rel === ".." || rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel)) {
|
|
1328
|
+
throw new Error(`Snapshot file path escapes checkout root: ${relativePath}`);
|
|
1329
|
+
}
|
|
1330
|
+
return target;
|
|
1331
|
+
}
|
|
1332
|
+
function decodeSnapshotArchive(archive) {
|
|
1333
|
+
if (!archive || typeof archive !== "object" || archive.version !== 1 || !Array.isArray(archive.files)) {
|
|
1334
|
+
throw new Error("Unsupported snapshot archive payload");
|
|
1335
|
+
}
|
|
1336
|
+
return archive;
|
|
1337
|
+
}
|
|
1338
|
+
function parseSnapshotArchiveContentBase64(contentBase64) {
|
|
1339
|
+
const json = Buffer.from(contentBase64, "base64").toString("utf8");
|
|
1340
|
+
return decodeSnapshotArchive(JSON.parse(json));
|
|
1341
|
+
}
|
|
1342
|
+
function extractUploadedSnapshotArchive(input) {
|
|
1343
|
+
const archive = decodeSnapshotArchive(input.archive);
|
|
1344
|
+
const snapshotId = sanitizeSnapshotId(input.snapshotId, `snapshot-${(input.now?.() ?? new Date).toISOString().replace(/[:.]/g, "-")}`);
|
|
1345
|
+
const checkoutPath = resolve10(repoSlugPath(input.baseDir, input.repoSlug, input.checkoutKey), snapshotId);
|
|
1346
|
+
mkdirSync6(checkoutPath, { recursive: true });
|
|
1347
|
+
for (const file of archive.files) {
|
|
1348
|
+
if (!file || typeof file.path !== "string" || typeof file.contentBase64 !== "string") {
|
|
1349
|
+
throw new Error("Invalid snapshot archive file entry");
|
|
1350
|
+
}
|
|
1351
|
+
const target = assertWithinRoot(checkoutPath, file.path);
|
|
1352
|
+
mkdirSync6(dirname5(target), { recursive: true });
|
|
1353
|
+
writeFileSync6(target, Buffer.from(file.contentBase64, "base64"));
|
|
1354
|
+
}
|
|
1355
|
+
writeFileSync6(resolve10(checkoutPath, ".rig-uploaded-snapshot.json"), `${JSON.stringify({
|
|
1356
|
+
repoSlug: input.repoSlug,
|
|
1357
|
+
snapshotId,
|
|
1358
|
+
fileCount: archive.files.length,
|
|
1359
|
+
archiveCreatedAt: archive.createdAt ?? null,
|
|
1360
|
+
extractedAt: (input.now?.() ?? new Date).toISOString()
|
|
1361
|
+
}, null, 2)}
|
|
1362
|
+
`, "utf8");
|
|
1363
|
+
return { kind: "uploaded-snapshot", path: checkoutPath, fileCount: archive.files.length, snapshotId };
|
|
1364
|
+
}
|
|
1365
|
+
async function runChecked(command, args, cwd, env) {
|
|
1366
|
+
const result = await command(args, cwd || env ? { ...cwd ? { cwd } : {}, ...env ? { env } : {} } : undefined);
|
|
1367
|
+
if (result.exitCode !== 0) {
|
|
1368
|
+
throw new Error(`${args.join(" ")} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim());
|
|
1369
|
+
}
|
|
1370
|
+
return result;
|
|
1371
|
+
}
|
|
1372
|
+
function gitCredentialConfig(token) {
|
|
1373
|
+
const clean = token?.trim();
|
|
1374
|
+
if (!clean)
|
|
1375
|
+
return { args: [] };
|
|
1376
|
+
return {
|
|
1377
|
+
args: [
|
|
1378
|
+
"-c",
|
|
1379
|
+
"credential.helper=",
|
|
1380
|
+
"-c",
|
|
1381
|
+
'credential.helper=!f() { test "$1" = get && echo username=x-access-token && echo password="$RIG_GIT_CREDENTIAL_TOKEN"; }; f'
|
|
1382
|
+
],
|
|
1383
|
+
env: {
|
|
1384
|
+
RIG_GIT_CREDENTIAL_TOKEN: clean,
|
|
1385
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
async function prepareRemoteCheckout(input) {
|
|
1390
|
+
const exists = input.exists ?? existsSync7;
|
|
1391
|
+
const strategy = input.strategy;
|
|
1392
|
+
if (strategy.kind === "uploaded-snapshot") {
|
|
1393
|
+
return extractUploadedSnapshotArchive({
|
|
1394
|
+
repoSlug: strategy.repoSlug,
|
|
1395
|
+
baseDir: strategy.baseDir,
|
|
1396
|
+
archive: strategy.archive,
|
|
1397
|
+
snapshotId: strategy.snapshotId,
|
|
1398
|
+
checkoutKey: strategy.checkoutKey
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
if (strategy.kind === "existing-path") {
|
|
1402
|
+
const checkoutPath2 = resolve10(strategy.path);
|
|
1403
|
+
if (!exists(checkoutPath2)) {
|
|
1404
|
+
throw new Error(`Existing remote checkout path does not exist: ${checkoutPath2}`);
|
|
1405
|
+
}
|
|
1406
|
+
await runChecked(input.command, ["git", "-C", checkoutPath2, "rev-parse", "--is-inside-work-tree"]);
|
|
1407
|
+
return { kind: "existing-path", path: checkoutPath2 };
|
|
1408
|
+
}
|
|
1409
|
+
const checkoutPath = repoSlugPath(strategy.baseDir, strategy.repoSlug, strategy.checkoutKey);
|
|
1410
|
+
const credentials = gitCredentialConfig(strategy.credentialToken);
|
|
1411
|
+
if (!exists(checkoutPath)) {
|
|
1412
|
+
await runChecked(input.command, ["git", ...credentials.args, "clone", strategy.repoUrl, checkoutPath], undefined, credentials.env);
|
|
1413
|
+
}
|
|
1414
|
+
if (strategy.kind === "current-ref") {
|
|
1415
|
+
const ref = strategy.ref.trim();
|
|
1416
|
+
if (!ref)
|
|
1417
|
+
throw new Error("current-ref checkout requires a ref");
|
|
1418
|
+
await runChecked(input.command, ["git", "-C", checkoutPath, ...credentials.args, "fetch", "origin", ref], undefined, credentials.env);
|
|
1419
|
+
await runChecked(input.command, ["git", "-C", checkoutPath, "checkout", ref]);
|
|
1420
|
+
return { kind: "current-ref", path: checkoutPath, ref };
|
|
1421
|
+
}
|
|
1422
|
+
return { kind: "managed-clone", path: checkoutPath };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
1426
|
+
function buildServerControlStatus() {
|
|
1427
|
+
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
1428
|
+
const switchable = Boolean(switchCommand);
|
|
1429
|
+
return {
|
|
1430
|
+
mode: switchable ? "switchable" : "restart-required",
|
|
1431
|
+
canSwitchProjectRoot: switchable,
|
|
1432
|
+
requiresRestart: !switchable,
|
|
1433
|
+
switchEndpoint: "/api/server/project-root",
|
|
1434
|
+
reason: switchable ? "This Rig server can switch project roots through its configured supervisor restart hook; restarts are handled automatically." : "This Rig server is bound to one project root at startup. Browser/remote project paths are server-host paths; switching requires a trusted supervisor restart."
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function buildProjectConfigStatus(root) {
|
|
1438
|
+
const hasConfigTs = existsSync8(resolve11(root, "rig.config.ts"));
|
|
1439
|
+
const hasConfigJson = existsSync8(resolve11(root, "rig.config.json"));
|
|
1440
|
+
const hasLegacyTaskConfig = existsSync8(resolve11(root, ".rig", "task-config.json"));
|
|
1441
|
+
let kind = "missing";
|
|
1442
|
+
if (hasConfigTs)
|
|
1443
|
+
kind = "rig-config-ts";
|
|
1444
|
+
else if (hasConfigJson)
|
|
1445
|
+
kind = "rig-config-json";
|
|
1446
|
+
else if (hasLegacyTaskConfig)
|
|
1447
|
+
kind = "legacy-task-config";
|
|
1448
|
+
return {
|
|
1449
|
+
projectRoot: root,
|
|
1450
|
+
kind,
|
|
1451
|
+
hasConfigTs,
|
|
1452
|
+
hasConfigJson,
|
|
1453
|
+
hasLegacyTaskConfig,
|
|
1454
|
+
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function normalizeCommit(value) {
|
|
1458
|
+
const raw = normalizeString(value);
|
|
1459
|
+
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
1460
|
+
}
|
|
1461
|
+
function asPlainRecord(value) {
|
|
1462
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1463
|
+
}
|
|
1464
|
+
var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.0";
|
|
1465
|
+
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
1466
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
|
|
1467
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
|
|
1468
|
+
};
|
|
1469
|
+
function repoParts(repoSlug) {
|
|
1470
|
+
const [owner, repo] = repoSlug.split("/");
|
|
1471
|
+
if (!owner || !repo)
|
|
1472
|
+
return { owner: "owner", repo: basename(repoSlug) || "repo", slug: repoSlug || "owner/repo" };
|
|
1473
|
+
return { owner, repo, slug: `${owner}/${repo}` };
|
|
1474
|
+
}
|
|
1475
|
+
function repairDir(checkoutPath) {
|
|
1476
|
+
const dir = resolve11(checkoutPath, ".rig", "state", "repairs", new Date().toISOString().replace(/[:.]/g, "-"));
|
|
1477
|
+
mkdirSync7(dir, { recursive: true });
|
|
1478
|
+
return dir;
|
|
1479
|
+
}
|
|
1480
|
+
function backupCheckoutFile(checkoutPath, relativePath) {
|
|
1481
|
+
const source = resolve11(checkoutPath, relativePath);
|
|
1482
|
+
const backupPath = resolve11(repairDir(checkoutPath), relativePath.replace(/[\\/]/g, "__"));
|
|
1483
|
+
mkdirSync7(dirname6(backupPath), { recursive: true });
|
|
1484
|
+
copyFileSync(source, backupPath);
|
|
1485
|
+
return backupPath;
|
|
1486
|
+
}
|
|
1487
|
+
function parsePackageJsonLosslessly(checkoutPath) {
|
|
1488
|
+
const packagePath = resolve11(checkoutPath, "package.json");
|
|
1489
|
+
if (!existsSync8(packagePath)) {
|
|
1490
|
+
return { existed: false, packageJson: { name: basename(checkoutPath) || "rig-project", private: true } };
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const parsed = JSON.parse(readFileSync5(packagePath, "utf8"));
|
|
1494
|
+
return {
|
|
1495
|
+
existed: true,
|
|
1496
|
+
packageJson: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { name: basename(checkoutPath) || "rig-project", private: true }
|
|
1497
|
+
};
|
|
1498
|
+
} catch {
|
|
1499
|
+
return {
|
|
1500
|
+
existed: true,
|
|
1501
|
+
backupPath: backupCheckoutFile(checkoutPath, "package.json"),
|
|
1502
|
+
packageJson: { name: basename(checkoutPath) || "rig-project", private: true }
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function ensureRemoteCheckoutRigPackageDeps(checkoutPath) {
|
|
1507
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync8(resolve11(checkoutPath, name)));
|
|
1508
|
+
const packagePath = resolve11(checkoutPath, "package.json");
|
|
1509
|
+
if (!hasConfig && !existsSync8(packagePath)) {
|
|
1510
|
+
return { skipped: true, reason: "package.json and rig.config missing" };
|
|
1511
|
+
}
|
|
1512
|
+
const parsed = parsePackageJsonLosslessly(checkoutPath);
|
|
1513
|
+
const existingDevDependencies = parsed.packageJson.devDependencies;
|
|
1514
|
+
const devDependencies = existingDevDependencies && typeof existingDevDependencies === "object" && !Array.isArray(existingDevDependencies) ? { ...existingDevDependencies } : {};
|
|
1515
|
+
const added = [];
|
|
1516
|
+
const updated = [];
|
|
1517
|
+
for (const [name, spec] of Object.entries(RIG_CONFIG_DEV_DEPENDENCIES)) {
|
|
1518
|
+
if (devDependencies[name] === spec)
|
|
1519
|
+
continue;
|
|
1520
|
+
if (Object.prototype.hasOwnProperty.call(devDependencies, name))
|
|
1521
|
+
updated.push(name);
|
|
1522
|
+
else
|
|
1523
|
+
added.push(name);
|
|
1524
|
+
devDependencies[name] = spec;
|
|
1525
|
+
}
|
|
1526
|
+
const changed = !parsed.existed || Boolean(parsed.backupPath) || added.length > 0 || updated.length > 0 || existingDevDependencies !== devDependencies && (!existingDevDependencies || typeof existingDevDependencies !== "object" || Array.isArray(existingDevDependencies));
|
|
1527
|
+
if (changed) {
|
|
1528
|
+
writeFileSync7(packagePath, `${JSON.stringify({ ...parsed.packageJson, devDependencies }, null, 2)}
|
|
1529
|
+
`, "utf8");
|
|
1530
|
+
}
|
|
1531
|
+
return {
|
|
1532
|
+
path: packagePath,
|
|
1533
|
+
existed: parsed.existed,
|
|
1534
|
+
changed,
|
|
1535
|
+
...parsed.backupPath ? { backupPath: parsed.backupPath } : {},
|
|
1536
|
+
added,
|
|
1537
|
+
updated,
|
|
1538
|
+
required: RIG_CONFIG_DEV_DEPENDENCIES
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
function generatedRigConfigSource(repoSlug) {
|
|
1542
|
+
const parts = repoParts(repoSlug);
|
|
1543
|
+
return buildRigInitConfigSource({
|
|
1544
|
+
projectName: parts.slug,
|
|
1545
|
+
projectRepo: parts.slug,
|
|
1546
|
+
taskSource: { kind: "github-issues", owner: parts.owner, repo: parts.repo },
|
|
1547
|
+
useStandardPlugin: true
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
function configLooksStructurallyUsable(source) {
|
|
1551
|
+
return /taskSource\s*:/.test(source) && /workspace\s*:/.test(source) && /project\s*:/.test(source);
|
|
1552
|
+
}
|
|
1553
|
+
function ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, reason = "missing or incomplete rig config") {
|
|
1554
|
+
const configPath = resolve11(checkoutPath, "rig.config.ts");
|
|
1555
|
+
const existingConfigName = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync8(resolve11(checkoutPath, name)));
|
|
1556
|
+
if (existingConfigName) {
|
|
1557
|
+
const existingPath = resolve11(checkoutPath, existingConfigName);
|
|
1558
|
+
const source = readFileSync5(existingPath, "utf8");
|
|
1559
|
+
if (existingConfigName !== "rig.config.json" && configLooksStructurallyUsable(source)) {
|
|
1560
|
+
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
1561
|
+
}
|
|
1562
|
+
if (existingConfigName === "rig.config.json") {
|
|
1563
|
+
try {
|
|
1564
|
+
const parsed = JSON.parse(source);
|
|
1565
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "taskSource" in parsed && "workspace" in parsed && "project" in parsed) {
|
|
1566
|
+
return { path: existingPath, changed: false, reason: "config structurally complete" };
|
|
1567
|
+
}
|
|
1568
|
+
} catch {}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const backupPath = existingConfigName ? backupCheckoutFile(checkoutPath, existingConfigName) : undefined;
|
|
1572
|
+
writeFileSync7(configPath, generatedRigConfigSource(repoSlug), "utf8");
|
|
1573
|
+
return {
|
|
1574
|
+
path: configPath,
|
|
1575
|
+
changed: true,
|
|
1576
|
+
reason,
|
|
1577
|
+
...backupPath ? { backupPath } : {}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function validateRemoteCheckoutRigConfig(checkoutPath) {
|
|
1581
|
+
const configFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].find((name) => existsSync8(resolve11(checkoutPath, name)));
|
|
1582
|
+
if (!configFile)
|
|
1583
|
+
return { ok: false, error: "missing rig config" };
|
|
1584
|
+
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
1585
|
+
return { skipped: true, reason: "test install skipped", configFile };
|
|
1586
|
+
}
|
|
1587
|
+
const script = [
|
|
1588
|
+
`const mod = await import(\`./${configFile}\`);`,
|
|
1589
|
+
`const config = mod.default ?? mod.config ?? mod;`,
|
|
1590
|
+
`if (!config || typeof config !== "object") throw new Error("config did not export an object");`,
|
|
1591
|
+
`if (!config.project) throw new Error("missing project");`,
|
|
1592
|
+
`if (!config.taskSource) throw new Error("missing taskSource");`,
|
|
1593
|
+
`if (!config.workspace) throw new Error("missing workspace");`
|
|
1594
|
+
].join(`
|
|
1595
|
+
`);
|
|
1596
|
+
const result = spawnSync2("bun", ["-e", script], { cwd: checkoutPath, env: process.env, encoding: "utf8" });
|
|
1597
|
+
if (result.error || result.status !== 0) {
|
|
1598
|
+
return { ok: false, configFile, error: result.error instanceof Error ? result.error.message : result.stderr || result.stdout || `exit ${result.status ?? "unknown"}` };
|
|
1599
|
+
}
|
|
1600
|
+
return { ok: true, configFile };
|
|
1601
|
+
}
|
|
1602
|
+
function installRemoteCheckoutPackages(checkoutPath) {
|
|
1603
|
+
if (!existsSync8(resolve11(checkoutPath, "package.json"))) {
|
|
1604
|
+
return { skipped: true, reason: "package.json missing" };
|
|
1605
|
+
}
|
|
1606
|
+
if (process.env.RIG_TEST_SKIP_REMOTE_CHECKOUT_INSTALL === "1") {
|
|
1607
|
+
return { skipped: true, command: "bun install" };
|
|
1608
|
+
}
|
|
1609
|
+
const result = spawnSync2("bun", ["install"], { cwd: checkoutPath, env: process.env, encoding: "utf8" });
|
|
1610
|
+
if (result.error || result.status !== 0) {
|
|
1611
|
+
throw new Error(`bun install failed in ${checkoutPath}: ${result.error instanceof Error ? result.error.message : result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`}`);
|
|
1612
|
+
}
|
|
1613
|
+
return { ok: true, command: "bun install", stdout: result.stdout?.trim() || undefined };
|
|
1614
|
+
}
|
|
1615
|
+
function repairRemoteCheckoutForRig(checkoutPath, repoSlug) {
|
|
1616
|
+
const hasConfig = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync8(resolve11(checkoutPath, name)));
|
|
1617
|
+
const hasPackage = existsSync8(resolve11(checkoutPath, "package.json"));
|
|
1618
|
+
if (!hasConfig && !hasPackage) {
|
|
1619
|
+
return {
|
|
1620
|
+
packageJson: { skipped: true, reason: "package.json and rig.config missing" },
|
|
1621
|
+
config: { skipped: true, reason: "package.json and rig.config missing" },
|
|
1622
|
+
configValidation: { skipped: true, reason: "package.json and rig.config missing" }
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
let config = ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug);
|
|
1626
|
+
const packageJson = ensureRemoteCheckoutRigPackageDeps(checkoutPath);
|
|
1627
|
+
const validation = validateRemoteCheckoutRigConfig(checkoutPath);
|
|
1628
|
+
if (validation.ok === false) {
|
|
1629
|
+
config = ensureRemoteCheckoutRigConfig(checkoutPath, repoSlug, `config validation failed: ${String(validation.error ?? "unknown")}`);
|
|
1630
|
+
}
|
|
1631
|
+
return { packageJson, config, configValidation: validation };
|
|
1632
|
+
}
|
|
1633
|
+
async function runRemoteCheckoutCommand(args, options) {
|
|
1634
|
+
const result = spawnSync2(args[0], args.slice(1), {
|
|
1635
|
+
cwd: options?.cwd,
|
|
1636
|
+
env: { ...process.env, ...options?.env ?? {} },
|
|
1637
|
+
encoding: "utf8"
|
|
1638
|
+
});
|
|
1639
|
+
return {
|
|
1640
|
+
exitCode: typeof result.status === "number" ? result.status : result.error ? 1 : 0,
|
|
1641
|
+
stdout: result.stdout || undefined,
|
|
1642
|
+
stderr: result.stderr || (result.error instanceof Error ? result.error.message : undefined)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function buildRemoteRunLogPayload(value, identifiers) {
|
|
1646
|
+
const payload = asPlainRecord(value);
|
|
1647
|
+
if (payload) {
|
|
1648
|
+
return { ...payload, ...identifiers };
|
|
1649
|
+
}
|
|
1650
|
+
return value === undefined ? identifiers : { ...identifiers, rawPayload: value };
|
|
1651
|
+
}
|
|
1652
|
+
function runSourceTaskIdentity(run) {
|
|
1653
|
+
const sourceTask = run.sourceTask;
|
|
1654
|
+
return sourceTask && typeof sourceTask === "object" && !Array.isArray(sourceTask) ? sourceTask : null;
|
|
1655
|
+
}
|
|
1656
|
+
async function updateRemoteRunTaskSourceLifecycle(projectRoot, run, status, summary, errorText) {
|
|
1657
|
+
if (!run.taskId)
|
|
1658
|
+
return;
|
|
1659
|
+
try {
|
|
1660
|
+
await updateConfiguredTaskSourceTask2(projectRoot, {
|
|
1661
|
+
taskId: run.taskId,
|
|
1662
|
+
sourceTask: runSourceTaskIdentity(run),
|
|
1663
|
+
update: {
|
|
1664
|
+
status,
|
|
1665
|
+
comment: buildTaskRunLifecycleComment2({
|
|
1666
|
+
runId: run.runId,
|
|
1667
|
+
status,
|
|
1668
|
+
summary,
|
|
1669
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
1670
|
+
logsDir: normalizeString(run.logRoot),
|
|
1671
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
1672
|
+
errorText
|
|
1673
|
+
})
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
const message = `Failed to update task source for ${run.taskId} to ${status}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1678
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
1679
|
+
errorText: [normalizeString(run.errorText), message].filter(Boolean).join(`
|
|
1680
|
+
`) || message
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function buildRemoteRunLogEntry(body, identifiers) {
|
|
1685
|
+
const reserved = new Set([
|
|
1686
|
+
"workspaceId",
|
|
1687
|
+
"runId",
|
|
1688
|
+
"hostId",
|
|
1689
|
+
"leaseId",
|
|
1690
|
+
"id",
|
|
1691
|
+
"title",
|
|
1692
|
+
"detail",
|
|
1693
|
+
"tone",
|
|
1694
|
+
"status",
|
|
1695
|
+
"payload",
|
|
1696
|
+
"createdAt"
|
|
1697
|
+
]);
|
|
1698
|
+
const extras = Object.fromEntries(Object.entries(body).filter(([key]) => !reserved.has(key)));
|
|
1699
|
+
return {
|
|
1700
|
+
...extras,
|
|
1701
|
+
id: `log:${identifiers.runId}:${Date.now()}`,
|
|
1702
|
+
title: normalizeString(body.title) ?? "Remote log",
|
|
1703
|
+
detail: typeof body.detail === "string" ? body.detail : null,
|
|
1704
|
+
tone: normalizeString(body.tone) ?? "info",
|
|
1705
|
+
status: normalizeString(body.status),
|
|
1706
|
+
payload: buildRemoteRunLogPayload(body.payload, {
|
|
1707
|
+
hostId: identifiers.hostId,
|
|
1708
|
+
leaseId: identifiers.leaseId
|
|
1709
|
+
}),
|
|
1710
|
+
createdAt: normalizeString(body.createdAt) ?? new Date().toISOString()
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function readGitHeadCommit(projectRoot) {
|
|
1714
|
+
try {
|
|
1715
|
+
let gitDir = resolve11(projectRoot, ".git");
|
|
1716
|
+
try {
|
|
1717
|
+
const dotGit = readFileSync5(gitDir, "utf8").trim();
|
|
1718
|
+
const gitDirPrefix = "gitdir:";
|
|
1719
|
+
if (dotGit.startsWith(gitDirPrefix)) {
|
|
1720
|
+
gitDir = resolve11(projectRoot, dotGit.slice(gitDirPrefix.length).trim());
|
|
1721
|
+
}
|
|
1722
|
+
} catch {}
|
|
1723
|
+
const head = readFileSync5(resolve11(gitDir, "HEAD"), "utf8").trim();
|
|
1724
|
+
const refPrefix = "ref:";
|
|
1725
|
+
if (!head.startsWith(refPrefix)) {
|
|
1726
|
+
return normalizeCommit(head);
|
|
1727
|
+
}
|
|
1728
|
+
const ref = head.slice(refPrefix.length).trim();
|
|
1729
|
+
const refPath = resolve11(gitDir, ref);
|
|
1730
|
+
if (existsSync8(refPath)) {
|
|
1731
|
+
return normalizeCommit(readFileSync5(refPath, "utf8").trim());
|
|
1732
|
+
}
|
|
1733
|
+
const commonDir = normalizeString(readFileSync5(resolve11(gitDir, "commondir"), "utf8"));
|
|
1734
|
+
return commonDir ? normalizeCommit(readFileSync5(resolve11(gitDir, commonDir, ref), "utf8").trim()) : null;
|
|
1735
|
+
} catch {
|
|
1736
|
+
return null;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function isAuthorizedInspectorStreamRequest(req, authToken) {
|
|
1740
|
+
if (!authToken)
|
|
1741
|
+
return true;
|
|
1742
|
+
const header = req.headers.get("authorization");
|
|
1743
|
+
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
1744
|
+
if (match?.[1] === authToken)
|
|
1745
|
+
return true;
|
|
1746
|
+
try {
|
|
1747
|
+
return new URL(req.url).searchParams.get("token") === authToken;
|
|
1748
|
+
} catch {
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function buildDeploymentStatus(projectRoot) {
|
|
1753
|
+
const envCommit = normalizeCommit(process.env.RIG_COMMIT_SHA ?? process.env.GITHUB_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.COMMIT_SHA);
|
|
1754
|
+
const gitCommit = envCommit ?? readGitHeadCommit(projectRoot);
|
|
1755
|
+
return {
|
|
1756
|
+
currentCommit: gitCommit,
|
|
1757
|
+
commitSource: envCommit ? "env" : gitCommit ? "git" : null,
|
|
1758
|
+
routes: [
|
|
1759
|
+
"/api/server/status",
|
|
1760
|
+
"/api/workspace/tasks",
|
|
1761
|
+
"/api/snapshot",
|
|
1762
|
+
"/api/github/auth/status",
|
|
1763
|
+
"/api/project/github"
|
|
1764
|
+
]
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function configuredRepoFromTaskSource(taskSource) {
|
|
1768
|
+
const owner = normalizeString(taskSource?.owner);
|
|
1769
|
+
const repo = normalizeString(taskSource?.repo);
|
|
1770
|
+
return owner && repo ? `${owner}/${repo}` : null;
|
|
1771
|
+
}
|
|
1772
|
+
async function buildTaskSourceStatus(state, config) {
|
|
1773
|
+
const diagnostics = state.snapshotService.getTaskSourceErrors();
|
|
1774
|
+
const selectedRepo = createGitHubAuthStore(state.projectRoot).status({
|
|
1775
|
+
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
1776
|
+
}).selectedRepo;
|
|
1777
|
+
try {
|
|
1778
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
1779
|
+
const taskSource = ctx?.config.taskSource && typeof ctx.config.taskSource === "object" ? ctx.config.taskSource : null;
|
|
1780
|
+
const kind = normalizeString(taskSource?.kind) ?? (config.kind === "legacy-task-config" ? "legacy-task-config" : null);
|
|
1781
|
+
const registered = kind ? ctx?.taskSourceRegistry.list().some((source) => source.kind === kind) ?? false : false;
|
|
1782
|
+
const configuredRepo = configuredRepoFromTaskSource(taskSource);
|
|
1783
|
+
const staleSelectedRepo = kind === "github-issues" && Boolean(selectedRepo && configuredRepo && selectedRepo !== configuredRepo);
|
|
1784
|
+
const staleDiagnostics = diagnostics.length > 0;
|
|
1785
|
+
return {
|
|
1786
|
+
kind,
|
|
1787
|
+
registered,
|
|
1788
|
+
configuredRepo,
|
|
1789
|
+
selectedRepo,
|
|
1790
|
+
stale: staleSelectedRepo || staleDiagnostics,
|
|
1791
|
+
staleReason: staleSelectedRepo ? `Selected GitHub repo ${selectedRepo} does not match configured task source ${configuredRepo}.` : staleDiagnostics ? "Task source has read diagnostics." : null,
|
|
1792
|
+
diagnostics
|
|
1793
|
+
};
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
return {
|
|
1796
|
+
kind: config.kind === "legacy-task-config" ? "legacy-task-config" : null,
|
|
1797
|
+
registered: false,
|
|
1798
|
+
configuredRepo: null,
|
|
1799
|
+
selectedRepo,
|
|
1800
|
+
stale: true,
|
|
1801
|
+
staleReason: error instanceof Error ? error.message : String(error),
|
|
1802
|
+
diagnostics
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function cleanHeaderScopes(value) {
|
|
1807
|
+
if (!value)
|
|
1808
|
+
return [];
|
|
1809
|
+
return value.split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
1810
|
+
}
|
|
1811
|
+
function bearerTokenFromRequest(req) {
|
|
1812
|
+
const header = req.headers.get("authorization");
|
|
1813
|
+
if (!header)
|
|
1814
|
+
return null;
|
|
1815
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
1816
|
+
return match ? match[1]?.trim() || null : null;
|
|
1817
|
+
}
|
|
1818
|
+
function isLoopbackRequest(req) {
|
|
1819
|
+
try {
|
|
1820
|
+
const hostname = new URL(req.url).hostname.toLowerCase();
|
|
1821
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1822
|
+
} catch {
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function isPublicRigAuthBootstrapRoute(pathname) {
|
|
1827
|
+
return pathname === "/" || pathname === "/health" || pathname === "/api/health" || pathname === "/api/server/status" || pathname === "/api/github/auth/status" || pathname === "/api/github/auth/token" || pathname === "/api/github/auth/device/start" || pathname === "/api/github/auth/device/poll";
|
|
1828
|
+
}
|
|
1829
|
+
function normalizePrMode(value) {
|
|
1830
|
+
const mode = normalizeString(value);
|
|
1831
|
+
return mode === "auto" || mode === "ask" || mode === "off" ? mode : undefined;
|
|
1832
|
+
}
|
|
1833
|
+
function authorizeRigHttpRequest(input) {
|
|
1834
|
+
if (input.legacyAuthorized) {
|
|
1835
|
+
return { authorized: true, actor: "rig-local-server", reason: "server-token" };
|
|
1836
|
+
}
|
|
1837
|
+
const bearer = bearerTokenFromRequest(input.req);
|
|
1838
|
+
const store = createGitHubAuthStore(input.projectRoot);
|
|
1839
|
+
const storedToken = store.readToken();
|
|
1840
|
+
if (bearer && storedToken && bearer === storedToken) {
|
|
1841
|
+
const status = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
1842
|
+
return { authorized: true, actor: status.login ?? "github-operator", reason: "github-token" };
|
|
1843
|
+
}
|
|
1844
|
+
if (isPublicRigAuthBootstrapRoute(input.pathname)) {
|
|
1845
|
+
return { authorized: true, actor: null, reason: "public-bootstrap" };
|
|
1846
|
+
}
|
|
1847
|
+
if (!input.serverAuthToken && !storedToken && isLoopbackRequest(input.req)) {
|
|
1848
|
+
return { authorized: true, actor: null, reason: "loopback-dev-no-auth" };
|
|
1849
|
+
}
|
|
1850
|
+
return { authorized: false, actor: null, reason: storedToken ? "github-token-required" : "auth-required" };
|
|
1851
|
+
}
|
|
1852
|
+
async function fetchGitHubUserInfo(token) {
|
|
1853
|
+
const response = await fetch("https://api.github.com/user", {
|
|
1854
|
+
headers: {
|
|
1855
|
+
accept: "application/vnd.github+json",
|
|
1856
|
+
authorization: `Bearer ${token}`,
|
|
1857
|
+
"user-agent": "rig-server"
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
if (!response.ok) {
|
|
1861
|
+
throw new Error(`GitHub user lookup failed (${response.status})`);
|
|
1862
|
+
}
|
|
1863
|
+
const payload = await response.json().catch(() => ({}));
|
|
1864
|
+
return {
|
|
1865
|
+
login: typeof payload.login === "string" ? payload.login : null,
|
|
1866
|
+
userId: typeof payload.id === "number" || typeof payload.id === "string" ? String(payload.id) : null,
|
|
1867
|
+
scopes: cleanHeaderScopes(response.headers.get("x-oauth-scopes"))
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
async function postGitHubForm(endpoint, body) {
|
|
1871
|
+
const response = await fetch(endpoint, {
|
|
1872
|
+
method: "POST",
|
|
1873
|
+
headers: {
|
|
1874
|
+
accept: "application/json",
|
|
1875
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1876
|
+
"user-agent": "rig-server"
|
|
1877
|
+
},
|
|
1878
|
+
body: new URLSearchParams(body)
|
|
1879
|
+
});
|
|
1880
|
+
const payload = await response.json().catch(() => ({}));
|
|
1881
|
+
return { status: response.status, payload };
|
|
1882
|
+
}
|
|
1883
|
+
function resolveRequestedProjectRoot(currentRoot, rawRoot) {
|
|
1884
|
+
const requestedRoot = normalizeString(rawRoot);
|
|
1885
|
+
if (!requestedRoot)
|
|
1886
|
+
return currentRoot;
|
|
1887
|
+
if (!isAbsolute2(requestedRoot)) {
|
|
1888
|
+
throw new Error("projectRoot must be an absolute path on the Rig server host");
|
|
1889
|
+
}
|
|
1890
|
+
const normalizedRoot = resolve11(requestedRoot);
|
|
1891
|
+
if (!existsSync8(normalizedRoot)) {
|
|
1892
|
+
throw new Error("projectRoot does not exist on the Rig server host");
|
|
1893
|
+
}
|
|
1894
|
+
return normalizedRoot;
|
|
1895
|
+
}
|
|
1896
|
+
function runProjectRootSwitchIfNeeded(currentRoot, targetRoot) {
|
|
1897
|
+
if (targetRoot === currentRoot)
|
|
1898
|
+
return { switched: false, message: null };
|
|
1899
|
+
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
1900
|
+
if (!switchCommand) {
|
|
1901
|
+
throw new Error("Project root switch command is not configured on this Rig server.");
|
|
1902
|
+
}
|
|
1903
|
+
const result = spawnSync2(switchCommand, [targetRoot], {
|
|
1904
|
+
cwd: currentRoot,
|
|
1905
|
+
env: process.env,
|
|
1906
|
+
encoding: "utf8"
|
|
1907
|
+
});
|
|
1908
|
+
if (result.error || result.status !== 0) {
|
|
1909
|
+
throw new Error(result.error instanceof Error ? result.error.message : result.stderr || result.stdout || `Project-root switch command failed (${result.status ?? "unknown"}).`);
|
|
1910
|
+
}
|
|
1911
|
+
return { switched: true, message: "Project-root switch accepted. Rig server restart has been scheduled." };
|
|
1912
|
+
}
|
|
1913
|
+
function buildGitHubProjectConfigSource(input) {
|
|
1914
|
+
return [
|
|
1915
|
+
`import { defineConfig } from "@rig/core";`,
|
|
1916
|
+
`import standard, { createStateGitHubCredentialProvider } from "@rig/standard-plugin";`,
|
|
1917
|
+
``,
|
|
1918
|
+
`export default defineConfig({`,
|
|
1919
|
+
` project: { name: ${JSON.stringify(input.projectName ?? input.repo)} },`,
|
|
1920
|
+
` plugins: [standard({`,
|
|
1921
|
+
` githubCredentialProvider: createStateGitHubCredentialProvider(),`,
|
|
1922
|
+
` githubWorkspaceId: ${JSON.stringify(`${input.owner}/${input.repo}`)},`,
|
|
1923
|
+
` githubUserId: process.env.RIG_GITHUB_USER_ID ?? ${JSON.stringify(input.githubUserId ?? "server-selected-user")},`,
|
|
1924
|
+
` })],`,
|
|
1925
|
+
` taskSource: {`,
|
|
1926
|
+
` kind: "github-issues",`,
|
|
1927
|
+
` owner: ${JSON.stringify(input.owner)},`,
|
|
1928
|
+
` repo: ${JSON.stringify(input.repo)},`,
|
|
1929
|
+
` state: "open",`,
|
|
1930
|
+
...input.assignee ? [` options: { assignee: ${JSON.stringify(input.assignee)} },`] : [],
|
|
1931
|
+
` },`,
|
|
1932
|
+
` workspace: { mainRepo: ".", isolation: "worktree" },`,
|
|
1933
|
+
`});`,
|
|
1934
|
+
``
|
|
1935
|
+
].join(`
|
|
1936
|
+
`);
|
|
1937
|
+
}
|
|
1938
|
+
async function probeGitHubRepository(input) {
|
|
1939
|
+
const headers = {
|
|
1940
|
+
accept: "application/vnd.github+json",
|
|
1941
|
+
"user-agent": "rig-server"
|
|
1942
|
+
};
|
|
1943
|
+
if (input.token) {
|
|
1944
|
+
headers.authorization = `Bearer ${input.token}`;
|
|
1945
|
+
}
|
|
1946
|
+
try {
|
|
1947
|
+
const response = await fetch(`https://api.github.com/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}`, { headers });
|
|
1948
|
+
const payload = await response.json().catch(() => ({}));
|
|
1949
|
+
if (response.ok) {
|
|
1950
|
+
return {
|
|
1951
|
+
ok: true,
|
|
1952
|
+
owner: input.owner,
|
|
1953
|
+
repo: input.repo,
|
|
1954
|
+
status: response.status,
|
|
1955
|
+
authenticated: Boolean(input.token),
|
|
1956
|
+
authenticationRequired: payload.private === true && !input.token,
|
|
1957
|
+
fullName: typeof payload.full_name === "string" ? payload.full_name : `${input.owner}/${input.repo}`,
|
|
1958
|
+
private: typeof payload.private === "boolean" ? payload.private : null,
|
|
1959
|
+
message: input.token ? "Repository access verified with signed-in GitHub credentials." : "Public repository access verified without credentials.",
|
|
1960
|
+
scopes: input.scopes
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const authenticationRequired = !input.token && (response.status === 401 || response.status === 403 || response.status === 404);
|
|
1964
|
+
return {
|
|
1965
|
+
ok: false,
|
|
1966
|
+
owner: input.owner,
|
|
1967
|
+
repo: input.repo,
|
|
1968
|
+
status: response.status,
|
|
1969
|
+
authenticated: Boolean(input.token),
|
|
1970
|
+
authenticationRequired,
|
|
1971
|
+
fullName: null,
|
|
1972
|
+
private: null,
|
|
1973
|
+
message: authenticationRequired ? "Repository is private, missing, or inaccessible without GitHub sign-in. Sign in before saving this config." : typeof payload.message === "string" ? payload.message : `GitHub repository probe failed (${response.status}).`,
|
|
1974
|
+
scopes: input.scopes
|
|
1975
|
+
};
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
return {
|
|
1978
|
+
ok: false,
|
|
1979
|
+
owner: input.owner,
|
|
1980
|
+
repo: input.repo,
|
|
1981
|
+
status: 0,
|
|
1982
|
+
authenticated: Boolean(input.token),
|
|
1983
|
+
authenticationRequired: !input.token,
|
|
1984
|
+
fullName: null,
|
|
1985
|
+
private: null,
|
|
1986
|
+
message: error instanceof Error ? error.message : "GitHub repository probe failed.",
|
|
1987
|
+
scopes: input.scopes
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
function backupConfigPath(configPath) {
|
|
1992
|
+
return `${configPath}.bak.${new Date().toISOString().replace(/[.:]/g, "-")}`;
|
|
1993
|
+
}
|
|
1994
|
+
function stringValue(value) {
|
|
1995
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1996
|
+
}
|
|
1997
|
+
function recordValue(value) {
|
|
1998
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1999
|
+
}
|
|
2000
|
+
function taskAssigneeLogins(task) {
|
|
2001
|
+
const record = recordValue(task);
|
|
2002
|
+
if (!record)
|
|
2003
|
+
return [];
|
|
2004
|
+
const direct = Array.isArray(record.assignees) ? record.assignees : [];
|
|
2005
|
+
const raw = recordValue(record.raw);
|
|
2006
|
+
const rawAssignees = raw && Array.isArray(raw.assignees) ? raw.assignees : [];
|
|
2007
|
+
return [...direct, ...rawAssignees].flatMap((entry) => {
|
|
2008
|
+
if (typeof entry === "string")
|
|
2009
|
+
return [entry];
|
|
2010
|
+
const login = stringValue(recordValue(entry)?.login);
|
|
2011
|
+
return login ? [login] : [];
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
function taskMatchesState(task, state) {
|
|
2015
|
+
const record = recordValue(task);
|
|
2016
|
+
if (!record)
|
|
2017
|
+
return false;
|
|
2018
|
+
const normalized = state.trim().toLowerCase();
|
|
2019
|
+
const rawState = stringValue(recordValue(record.raw)?.state)?.toLowerCase();
|
|
2020
|
+
if (rawState)
|
|
2021
|
+
return rawState === normalized;
|
|
2022
|
+
const status = stringValue(record.status)?.toLowerCase();
|
|
2023
|
+
if (normalized === "open")
|
|
2024
|
+
return status !== "closed";
|
|
2025
|
+
if (normalized === "closed")
|
|
2026
|
+
return status === "closed";
|
|
2027
|
+
return status === normalized;
|
|
2028
|
+
}
|
|
2029
|
+
function resolveAssigneeFilter(projectRoot, value) {
|
|
2030
|
+
const assignee = stringValue(value)?.trim();
|
|
2031
|
+
if (!assignee)
|
|
2032
|
+
return null;
|
|
2033
|
+
const normalized = assignee.toLowerCase();
|
|
2034
|
+
if (normalized !== "@me" && normalized !== "me")
|
|
2035
|
+
return assignee;
|
|
2036
|
+
const authLogin = createGitHubAuthStore(projectRoot).status({
|
|
2037
|
+
oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
2038
|
+
}).login;
|
|
2039
|
+
return stringValue(authLogin) ?? stringValue(process.env.RIG_GITHUB_USER_LOGIN) ?? stringValue(process.env.RIG_GITHUB_LOGIN) ?? stringValue(process.env.GITHUB_ACTOR) ?? assignee;
|
|
2040
|
+
}
|
|
2041
|
+
function activeTaskRunProjections(projectRoot) {
|
|
2042
|
+
return listAuthorityRuns7(projectRoot).filter((run) => !["completed", "failed", "cancelled", "stopped"].includes(String(run.status ?? "").toLowerCase())).flatMap((run) => run.taskId ? [{
|
|
2043
|
+
taskId: run.taskId,
|
|
2044
|
+
runId: run.runId,
|
|
2045
|
+
status: normalizeString(run.status) ?? undefined,
|
|
2046
|
+
stage: normalizeString(run.stage) ?? undefined
|
|
2047
|
+
}] : []);
|
|
2048
|
+
}
|
|
2049
|
+
function taskIdOf(task) {
|
|
2050
|
+
return stringValue(recordValue(task)?.id);
|
|
2051
|
+
}
|
|
2052
|
+
function taskDependencies(task) {
|
|
2053
|
+
const record = recordValue(task);
|
|
2054
|
+
const deps = record?.dependencies ?? record?.deps;
|
|
2055
|
+
return Array.isArray(deps) ? deps.flatMap((entry) => typeof entry === "string" && entry.trim() ? [entry.trim()] : []) : [];
|
|
2056
|
+
}
|
|
2057
|
+
function selectNextWorkspaceTask(projectRoot, tasks) {
|
|
2058
|
+
const activeTaskIds = new Set(activeTaskRunProjections(projectRoot).map((entry) => entry.taskId));
|
|
2059
|
+
const byId = new Map(tasks.flatMap((task) => {
|
|
2060
|
+
const id = taskIdOf(task);
|
|
2061
|
+
return id ? [[id, task]] : [];
|
|
2062
|
+
}));
|
|
2063
|
+
const runnable = tasks.filter((task) => {
|
|
2064
|
+
const id = taskIdOf(task);
|
|
2065
|
+
if (!id || activeTaskIds.has(id))
|
|
2066
|
+
return false;
|
|
2067
|
+
const status = stringValue(recordValue(task)?.status)?.toLowerCase() ?? "open";
|
|
2068
|
+
if (["blocked", "failed", "closed", "completed", "cancelled", "canceled"].includes(status))
|
|
2069
|
+
return false;
|
|
2070
|
+
return taskDependencies(task).every((dep) => {
|
|
2071
|
+
const dependency = byId.get(dep);
|
|
2072
|
+
const depStatus = stringValue(recordValue(dependency)?.status)?.toLowerCase();
|
|
2073
|
+
return depStatus === "closed" || depStatus === "completed";
|
|
2074
|
+
});
|
|
2075
|
+
});
|
|
2076
|
+
if (runnable.length === 0)
|
|
2077
|
+
return null;
|
|
2078
|
+
const queue = readQueueState(projectRoot);
|
|
2079
|
+
const queueRank = new Map(queue.map((entry, index) => [entry.taskId, { score: entry.score, position: index }]));
|
|
2080
|
+
return runnable.toSorted((left, right) => {
|
|
2081
|
+
const leftId = taskIdOf(left) ?? "";
|
|
2082
|
+
const rightId = taskIdOf(right) ?? "";
|
|
2083
|
+
const leftQueue = queueRank.get(leftId);
|
|
2084
|
+
const rightQueue = queueRank.get(rightId);
|
|
2085
|
+
if (leftQueue || rightQueue) {
|
|
2086
|
+
return (rightQueue?.score ?? -1) - (leftQueue?.score ?? -1) || (leftQueue?.position ?? 999999) - (rightQueue?.position ?? 999999);
|
|
2087
|
+
}
|
|
2088
|
+
const leftStatus = stringValue(recordValue(left)?.status)?.toLowerCase() === "ready" ? 1 : 0;
|
|
2089
|
+
const rightStatus = stringValue(recordValue(right)?.status)?.toLowerCase() === "ready" ? 1 : 0;
|
|
2090
|
+
if (leftStatus !== rightStatus)
|
|
2091
|
+
return rightStatus - leftStatus;
|
|
2092
|
+
const leftPriority = typeof recordValue(left)?.priority === "number" ? recordValue(left).priority : 0;
|
|
2093
|
+
const rightPriority = typeof recordValue(right)?.priority === "number" ? recordValue(right).priority : 0;
|
|
2094
|
+
return rightPriority - leftPriority || leftId.localeCompare(rightId);
|
|
2095
|
+
})[0] ?? null;
|
|
2096
|
+
}
|
|
2097
|
+
function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
2098
|
+
if (!Array.isArray(tasks))
|
|
2099
|
+
return [];
|
|
2100
|
+
let filtered = tasks;
|
|
2101
|
+
const assignee = resolveAssigneeFilter(projectRoot, searchParams.get("assignee"));
|
|
2102
|
+
if (assignee) {
|
|
2103
|
+
filtered = filtered.filter((task) => taskAssigneeLogins(task).some((login) => login.toLowerCase() === assignee.toLowerCase()));
|
|
2104
|
+
}
|
|
2105
|
+
const state = stringValue(searchParams.get("state"));
|
|
2106
|
+
if (state) {
|
|
2107
|
+
filtered = filtered.filter((task) => taskMatchesState(task, state));
|
|
2108
|
+
}
|
|
2109
|
+
const status = stringValue(searchParams.get("status"));
|
|
2110
|
+
if (status) {
|
|
2111
|
+
filtered = filtered.filter((task) => stringValue(recordValue(task)?.status)?.toLowerCase() === status.toLowerCase());
|
|
2112
|
+
}
|
|
2113
|
+
const limit = Number.parseInt(searchParams.get("limit") || "", 10);
|
|
2114
|
+
if (Number.isFinite(limit) && limit > 0) {
|
|
2115
|
+
filtered = filtered.slice(0, Math.min(limit, 500));
|
|
2116
|
+
}
|
|
2117
|
+
return filtered;
|
|
2118
|
+
}
|
|
2119
|
+
function redactRemoteEndpoint(endpoint) {
|
|
2120
|
+
const { token, ...rest } = endpoint;
|
|
2121
|
+
return {
|
|
2122
|
+
...rest,
|
|
2123
|
+
token: "",
|
|
2124
|
+
tokenConfigured: token.trim().length > 0
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function isSecretKey(key) {
|
|
2128
|
+
const normalized = key.toLowerCase();
|
|
2129
|
+
return normalized === "token" || normalized === "authtoken" || normalized === "authorization" || normalized === "connectiontoken" || normalized === "access_token" || normalized === "secret";
|
|
2130
|
+
}
|
|
2131
|
+
function redactSecretFields(value) {
|
|
2132
|
+
if (Array.isArray(value)) {
|
|
2133
|
+
return value.map((entry) => redactSecretFields(entry));
|
|
2134
|
+
}
|
|
2135
|
+
const record = recordValue(value);
|
|
2136
|
+
if (!record) {
|
|
2137
|
+
return value;
|
|
2138
|
+
}
|
|
2139
|
+
const redacted = {};
|
|
2140
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
2141
|
+
redacted[key] = isSecretKey(key) && typeof entry === "string" ? entry.trim().length > 0 ? "[redacted]" : "" : redactSecretFields(entry);
|
|
2142
|
+
}
|
|
2143
|
+
return redacted;
|
|
2144
|
+
}
|
|
2145
|
+
function validateRemoteLease(deps, state, input) {
|
|
2146
|
+
const run = readAuthorityRun8(state.projectRoot, input.runId);
|
|
2147
|
+
if (!run) {
|
|
2148
|
+
return { ok: false, response: deps.jsonResponse({ ok: false, error: "Remote run not found" }, 404) };
|
|
2149
|
+
}
|
|
2150
|
+
if (run.mode !== "remote") {
|
|
2151
|
+
return { ok: false, response: deps.jsonResponse({ ok: false, error: "Run is not remote-owned" }, 409) };
|
|
2152
|
+
}
|
|
2153
|
+
if (run.hostId !== input.hostId || run.endpointId !== input.leaseId) {
|
|
2154
|
+
return {
|
|
2155
|
+
ok: false,
|
|
2156
|
+
response: deps.jsonResponse({ ok: false, error: "Remote lease does not own this run" }, 409)
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
return { ok: true, run };
|
|
2160
|
+
}
|
|
2161
|
+
function createRigServerFetch(state, deps) {
|
|
2162
|
+
return async function fetchHandler(req, server) {
|
|
2163
|
+
return deps.withServerPathEnv(state.projectRoot, async () => {
|
|
2164
|
+
const browserOrigin = deps.resolveAllowedBrowserOrigin(req);
|
|
2165
|
+
const finalizeResponse = (response) => deps.withCorsHeaders(response, req, browserOrigin);
|
|
2166
|
+
const upgradeResponse = handleWebSocketUpgrade({
|
|
2167
|
+
req,
|
|
2168
|
+
server,
|
|
2169
|
+
authToken: state.authToken
|
|
2170
|
+
});
|
|
2171
|
+
if (upgradeResponse) {
|
|
2172
|
+
return upgradeResponse;
|
|
2173
|
+
}
|
|
2174
|
+
return finalizeResponse(await (async () => {
|
|
2175
|
+
const url = new URL(req.url);
|
|
2176
|
+
if (req.method === "OPTIONS" && browserOrigin && req.headers.has("access-control-request-method")) {
|
|
2177
|
+
return new Response(null, { status: 204 });
|
|
2178
|
+
}
|
|
2179
|
+
if (url.pathname === "/health" || url.pathname === "/api/health") {
|
|
2180
|
+
const queryToken = url.searchParams.get("token");
|
|
2181
|
+
const headerToken = (() => {
|
|
2182
|
+
const header = req.headers.get("authorization");
|
|
2183
|
+
if (!header)
|
|
2184
|
+
return null;
|
|
2185
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
2186
|
+
return match ? match[1] : null;
|
|
2187
|
+
})();
|
|
2188
|
+
const callerToken = queryToken ?? headerToken;
|
|
2189
|
+
if (callerToken !== null || state.authToken !== null) {
|
|
2190
|
+
if (callerToken !== state.authToken) {
|
|
2191
|
+
return deps.jsonResponse({ ok: false, error: "Token mismatch" }, 401);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return deps.jsonResponse({
|
|
2195
|
+
ok: true,
|
|
2196
|
+
projectRoot: state.projectRoot,
|
|
2197
|
+
subscribers: state.subscribers.size,
|
|
2198
|
+
sockets: state.sockets.size,
|
|
2199
|
+
eventsFile: state.eventsFile,
|
|
2200
|
+
notifications: state.targets.length
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
const isLinearWebhook = url.pathname === "/api/linear/webhook" && req.method === "POST";
|
|
2204
|
+
const isInspectorStream = url.pathname === "/api/inspector/stream" && req.method === "GET";
|
|
2205
|
+
const legacyAuthorizedHttpRequest = isInspectorStream && isAuthorizedInspectorStreamRequest(req, state.authToken) ? true : deps.isAuthorizedHttpRequest(req, state.authToken);
|
|
2206
|
+
const requestAuth = authorizeRigHttpRequest({
|
|
2207
|
+
req,
|
|
2208
|
+
pathname: url.pathname,
|
|
2209
|
+
projectRoot: state.projectRoot,
|
|
2210
|
+
serverAuthToken: state.authToken,
|
|
2211
|
+
legacyAuthorized: legacyAuthorizedHttpRequest
|
|
2212
|
+
});
|
|
2213
|
+
const authorizedLinearWebhook = isLinearWebhook && deps.isAuthorizedLinearWebhookRequest(req);
|
|
2214
|
+
if (!requestAuth.authorized && !authorizedLinearWebhook) {
|
|
2215
|
+
return deps.jsonResponse({ ok: false, error: "Unauthorized HTTP request", reason: requestAuth.reason }, 401);
|
|
2216
|
+
}
|
|
2217
|
+
if (url.pathname === "/") {
|
|
2218
|
+
const inspectorSnapshot = state.inspector?.snapshot() ?? null;
|
|
2219
|
+
const inspectorAgentSnapshot = state.inspectorAgent?.snapshot() ?? null;
|
|
2220
|
+
return deps.htmlResponse(`<!doctype html>
|
|
2221
|
+
<html lang="en">
|
|
2222
|
+
<head>
|
|
2223
|
+
<meta charset="utf-8" />
|
|
2224
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2225
|
+
<title>Rig Server</title>
|
|
2226
|
+
<style>
|
|
2227
|
+
:root { color-scheme: dark; }
|
|
2228
|
+
body {
|
|
2229
|
+
margin: 0;
|
|
2230
|
+
background: #0b1020;
|
|
2231
|
+
color: #e6ecff;
|
|
2232
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
2233
|
+
}
|
|
2234
|
+
main {
|
|
2235
|
+
max-width: 960px;
|
|
2236
|
+
margin: 0 auto;
|
|
2237
|
+
padding: 32px 24px 48px;
|
|
2238
|
+
}
|
|
2239
|
+
h1, h2 { margin: 0 0 12px; }
|
|
2240
|
+
p { color: #bfd0ff; line-height: 1.55; }
|
|
2241
|
+
.card {
|
|
2242
|
+
margin-top: 18px;
|
|
2243
|
+
padding: 16px 18px;
|
|
2244
|
+
border-radius: 14px;
|
|
2245
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
2246
|
+
background: rgba(255,255,255,0.04);
|
|
2247
|
+
}
|
|
2248
|
+
ul { padding-left: 18px; margin: 0; }
|
|
2249
|
+
li { margin: 8px 0; }
|
|
2250
|
+
a { color: #8dc2ff; }
|
|
2251
|
+
code { color: #ffffff; }
|
|
2252
|
+
.muted { color: #8ea2d9; }
|
|
2253
|
+
</style>
|
|
2254
|
+
</head>
|
|
2255
|
+
<body>
|
|
2256
|
+
<main>
|
|
2257
|
+
<h1>Rig Server</h1>
|
|
2258
|
+
<p>Control-plane API and live global inspector overlay for this workspace.</p>
|
|
2259
|
+
<section class="card">
|
|
2260
|
+
<h2>Status</h2>
|
|
2261
|
+
<ul>
|
|
2262
|
+
<li><strong>Project root:</strong> <code>${deps.escapeHtml(state.projectRoot)}</code></li>
|
|
2263
|
+
<li><strong>Inspector service:</strong> <code>${state.inspector?.isRunning() ? "running" : "stopped"}</code></li>
|
|
2264
|
+
<li><strong>Inspector agent:</strong> <code>${deps.escapeHtml(inspectorAgentSnapshot?.status ?? "unavailable")}</code></li>
|
|
2265
|
+
<li><strong>Active inspector runs:</strong> <code>${String(inspectorSnapshot?.activeRuns?.length ?? 0)}</code></li>
|
|
2266
|
+
</ul>
|
|
2267
|
+
</section>
|
|
2268
|
+
<section class="card">
|
|
2269
|
+
<h2>Useful endpoints</h2>
|
|
2270
|
+
<ul>
|
|
2271
|
+
<li><a href="/health">/health</a></li>
|
|
2272
|
+
<li><a href="/api/snapshot">/api/snapshot</a></li>
|
|
2273
|
+
<li><a href="/api/inspector/snapshot">/api/inspector/snapshot</a></li>
|
|
2274
|
+
<li><a href="/events/recent">/events/recent</a></li>
|
|
2275
|
+
</ul>
|
|
2276
|
+
</section>
|
|
2277
|
+
<p class="muted">Use the JSON APIs for automation and deep inspection. This root page exists to make the server legible in a browser.</p>
|
|
2278
|
+
</main>
|
|
2279
|
+
</body>
|
|
2280
|
+
</html>`);
|
|
2281
|
+
}
|
|
2282
|
+
if (url.pathname === "/events/recent") {
|
|
2283
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "50", 10);
|
|
2284
|
+
return deps.jsonResponse(deps.readRecentEvents(state.eventsFile, Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 50));
|
|
2285
|
+
}
|
|
2286
|
+
if (url.pathname === "/events/stream") {
|
|
2287
|
+
const stream = new ReadableStream({
|
|
2288
|
+
start(controller) {
|
|
2289
|
+
state.subscribers.add(controller);
|
|
2290
|
+
controller.enqueue(`event: ready
|
|
2291
|
+
data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
2292
|
+
|
|
2293
|
+
`);
|
|
2294
|
+
},
|
|
2295
|
+
cancel() {
|
|
2296
|
+
for (const controller of state.subscribers) {
|
|
2297
|
+
if (controller.desiredSize === null) {
|
|
2298
|
+
state.subscribers.delete(controller);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
});
|
|
2303
|
+
return new Response(stream, {
|
|
2304
|
+
headers: {
|
|
2305
|
+
"Content-Type": "text/event-stream",
|
|
2306
|
+
"Cache-Control": "no-cache",
|
|
2307
|
+
Connection: "keep-alive"
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
if (url.pathname === "/notify/test" && req.method === "POST") {
|
|
2312
|
+
const event = {
|
|
2313
|
+
id: `http-test-${Date.now()}`,
|
|
2314
|
+
runId: "manual",
|
|
2315
|
+
timestamp: new Date().toISOString(),
|
|
2316
|
+
type: state.eventType,
|
|
2317
|
+
payload: { trigger: "http" }
|
|
2318
|
+
};
|
|
2319
|
+
const sent = await deps.dispatchEventToTargets(event, state.targets);
|
|
2320
|
+
return deps.jsonResponse({ ok: true, sent, eventType: state.eventType });
|
|
2321
|
+
}
|
|
2322
|
+
if (url.pathname === "/api/workspace/summary") {
|
|
2323
|
+
return deps.jsonResponse(await deps.readWorkspaceSummaryRecord(state.projectRoot));
|
|
2324
|
+
}
|
|
2325
|
+
if (url.pathname === "/api/workspace/topology") {
|
|
2326
|
+
return deps.jsonResponse(readWorkspaceTopology2(state.projectRoot));
|
|
2327
|
+
}
|
|
2328
|
+
if (url.pathname === "/api/workspace/remote-hosts") {
|
|
2329
|
+
return deps.jsonResponse(readWorkspaceRemoteFleet2(state.projectRoot));
|
|
2330
|
+
}
|
|
2331
|
+
if (url.pathname === "/api/workspace/service-fabric/status") {
|
|
2332
|
+
return deps.jsonResponse(await mutateWorkspaceServiceFabric2(state.projectRoot, "verify"));
|
|
2333
|
+
}
|
|
2334
|
+
if (req.method === "POST" && (url.pathname === "/api/workspace/service-fabric/up" || url.pathname === "/api/workspace/service-fabric/verify" || url.pathname === "/api/workspace/service-fabric/down")) {
|
|
2335
|
+
const body = await deps.readJsonBody(req);
|
|
2336
|
+
const services = Array.isArray(body.services) ? body.services.flatMap((entry) => typeof entry === "string" ? [entry] : []) : [];
|
|
2337
|
+
const action = url.pathname.split("/").at(-1);
|
|
2338
|
+
if (action !== "up" && action !== "verify" && action !== "down") {
|
|
2339
|
+
return deps.notFound();
|
|
2340
|
+
}
|
|
2341
|
+
try {
|
|
2342
|
+
return deps.jsonResponse(await mutateWorkspaceServiceFabric2(state.projectRoot, action, services));
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2345
|
+
return deps.jsonResponse({ ok: false, error: message }, action === "verify" ? 400 : 501);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (url.pathname === "/api/workspace/task-projection") {
|
|
2349
|
+
return deps.jsonResponse({ ok: true, projection: readTaskProjection(state.projectRoot) });
|
|
2350
|
+
}
|
|
2351
|
+
if (url.pathname === "/api/workspace/task-projection/refresh" && req.method === "POST") {
|
|
2352
|
+
const projection = state.taskProjectionReconciler ? await state.taskProjectionReconciler.tick("http-refresh") : await refreshTaskProjection(state.projectRoot, {
|
|
2353
|
+
source: "snapshot-service",
|
|
2354
|
+
reason: "http-refresh",
|
|
2355
|
+
tasks: () => deps.snapshotService.getWorkspaceTasks(),
|
|
2356
|
+
activeRuns: () => Promise.resolve(activeTaskRunProjections(state.projectRoot))
|
|
2357
|
+
});
|
|
2358
|
+
deps.broadcastSnapshotInvalidation(state, "task-projection-refresh");
|
|
2359
|
+
return deps.jsonResponse({ ok: true, projection });
|
|
2360
|
+
}
|
|
2361
|
+
const taskDetailsMatch = url.pathname.match(/^\/api\/workspace\/tasks\/([^/]+)$/);
|
|
2362
|
+
if (taskDetailsMatch && taskDetailsMatch[1] !== "next") {
|
|
2363
|
+
const taskId = decodeURIComponent(taskDetailsMatch[1] ?? "");
|
|
2364
|
+
const diagnostics = deps.snapshotService.getTaskSourceErrors();
|
|
2365
|
+
const projection = readTaskProjection(state.projectRoot) ?? await refreshTaskProjection(state.projectRoot, {
|
|
2366
|
+
source: "snapshot-service",
|
|
2367
|
+
reason: "workspace-task-details-read",
|
|
2368
|
+
tasks: () => deps.snapshotService.getWorkspaceTasks(),
|
|
2369
|
+
activeRuns: () => Promise.resolve(activeTaskRunProjections(state.projectRoot))
|
|
2370
|
+
});
|
|
2371
|
+
const task = projection.tasks.find((entry) => entry.id === taskId || String(entry.raw?.number ?? "") === taskId) ?? null;
|
|
2372
|
+
return task ? deps.jsonResponse({ ok: true, task, diagnostics }) : deps.notFound();
|
|
2373
|
+
}
|
|
2374
|
+
if (url.pathname === "/api/workspace/tasks" || url.pathname === "/api/workspace/tasks/next") {
|
|
2375
|
+
const diagnostics = deps.snapshotService.getTaskSourceErrors();
|
|
2376
|
+
const refreshFromSnapshotService = (reason) => refreshTaskProjection(state.projectRoot, {
|
|
2377
|
+
source: "snapshot-service",
|
|
2378
|
+
reason,
|
|
2379
|
+
tasks: () => deps.snapshotService.getWorkspaceTasks(),
|
|
2380
|
+
activeRuns: () => Promise.resolve(activeTaskRunProjections(state.projectRoot))
|
|
2381
|
+
});
|
|
2382
|
+
const shouldRefresh = url.searchParams.get("refresh") === "1";
|
|
2383
|
+
const projection = shouldRefresh ? await refreshFromSnapshotService("workspace-tasks-refresh") : readTaskProjection(state.projectRoot) ?? await refreshFromSnapshotService("workspace-tasks-read");
|
|
2384
|
+
const allTasks = projection.tasks;
|
|
2385
|
+
if (allTasks.length === 0 && diagnostics.length > 0) {
|
|
2386
|
+
return deps.jsonResponse({
|
|
2387
|
+
ok: false,
|
|
2388
|
+
error: "Task source returned no tasks because the configured source failed.",
|
|
2389
|
+
diagnostics
|
|
2390
|
+
}, 502);
|
|
2391
|
+
}
|
|
2392
|
+
const tasks = filterWorkspaceTasks(state.projectRoot, allTasks, url.searchParams);
|
|
2393
|
+
if (url.pathname === "/api/workspace/tasks/next") {
|
|
2394
|
+
return deps.jsonResponse({ task: selectNextWorkspaceTask(state.projectRoot, tasks), count: tasks.length, diagnostics, projection: { refreshedAt: projection.refreshedAt, reason: projection.reason } });
|
|
2395
|
+
}
|
|
2396
|
+
return deps.jsonResponse(tasks);
|
|
2397
|
+
}
|
|
2398
|
+
if (url.pathname === "/api/tasks/update" && req.method === "POST") {
|
|
2399
|
+
const body = await deps.readJsonBody(req);
|
|
2400
|
+
const id = normalizeString(body.id);
|
|
2401
|
+
if (!id) {
|
|
2402
|
+
return deps.badRequest("id is required");
|
|
2403
|
+
}
|
|
2404
|
+
const update = {
|
|
2405
|
+
...normalizeString(body.status) ? { status: normalizeString(body.status) } : {},
|
|
2406
|
+
...normalizeString(body.comment) ? { comment: normalizeString(body.comment) } : {},
|
|
2407
|
+
...normalizeString(body.title) ? { title: normalizeString(body.title) } : {},
|
|
2408
|
+
...typeof body.body === "string" ? { body: body.body } : {}
|
|
2409
|
+
};
|
|
2410
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
2411
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
2412
|
+
if (!source) {
|
|
2413
|
+
return deps.badRequest("No task source is configured");
|
|
2414
|
+
}
|
|
2415
|
+
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
2416
|
+
return;
|
|
2417
|
+
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
2418
|
+
if (source.updateTask) {
|
|
2419
|
+
await source.updateTask(id, update);
|
|
2420
|
+
} else if (update.status && source.updateStatus) {
|
|
2421
|
+
await source.updateStatus(id, update.status);
|
|
2422
|
+
} else {
|
|
2423
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
2424
|
+
}
|
|
2425
|
+
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
2426
|
+
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
2427
|
+
taskId: id,
|
|
2428
|
+
status: update.status,
|
|
2429
|
+
issueNodeId,
|
|
2430
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
2431
|
+
config: ctx?.config
|
|
2432
|
+
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
2433
|
+
deps.snapshotService.invalidate("github-issue-updated");
|
|
2434
|
+
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
2435
|
+
return;
|
|
2436
|
+
});
|
|
2437
|
+
deps.broadcastSnapshotInvalidation(state, "github-issue-updated");
|
|
2438
|
+
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
2439
|
+
}
|
|
2440
|
+
if (url.pathname === "/api/workspace/task-labels") {
|
|
2441
|
+
return deps.jsonResponse({
|
|
2442
|
+
ok: true,
|
|
2443
|
+
ready: true,
|
|
2444
|
+
labelsReady: true,
|
|
2445
|
+
labels: [
|
|
2446
|
+
"ready",
|
|
2447
|
+
"blocked",
|
|
2448
|
+
"in-progress",
|
|
2449
|
+
"under-review",
|
|
2450
|
+
"failed",
|
|
2451
|
+
"cancelled",
|
|
2452
|
+
"rig:running",
|
|
2453
|
+
"rig:pr-open",
|
|
2454
|
+
"rig:ci-fixing",
|
|
2455
|
+
"rig:done",
|
|
2456
|
+
"rig:needs-attention"
|
|
2457
|
+
],
|
|
2458
|
+
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
if (url.pathname === "/api/server/status") {
|
|
2462
|
+
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2463
|
+
const taskSource = await buildTaskSourceStatus(state, config);
|
|
2464
|
+
return deps.jsonResponse({
|
|
2465
|
+
ok: true,
|
|
2466
|
+
projectRoot: state.projectRoot,
|
|
2467
|
+
sourceKind: taskSource.kind,
|
|
2468
|
+
config,
|
|
2469
|
+
control: buildServerControlStatus(),
|
|
2470
|
+
deployment: buildDeploymentStatus(state.projectRoot),
|
|
2471
|
+
taskSource
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
if (url.pathname === "/api/projects" && req.method === "POST") {
|
|
2475
|
+
const body = await deps.readJsonBody(req);
|
|
2476
|
+
const repoSlug = normalizeString(body.repoSlug);
|
|
2477
|
+
if (!repoSlug || !normalizeRepoSlug(repoSlug)) {
|
|
2478
|
+
return deps.jsonResponse({ ok: false, error: "repoSlug must be owner/repo" }, 400);
|
|
2479
|
+
}
|
|
2480
|
+
const rawCheckout = asPlainRecord(body.checkout);
|
|
2481
|
+
const checkoutKind = normalizeString(rawCheckout?.kind);
|
|
2482
|
+
const checkout = checkoutKind === "local" || checkoutKind === "managed-clone" || checkoutKind === "current-ref" || checkoutKind === "uploaded-snapshot" || checkoutKind === "existing-path" ? {
|
|
2483
|
+
kind: checkoutKind,
|
|
2484
|
+
path: normalizeString(rawCheckout?.path) ?? state.projectRoot,
|
|
2485
|
+
ref: normalizeString(rawCheckout?.ref) ?? undefined
|
|
2486
|
+
} : undefined;
|
|
2487
|
+
const record = upsertProjectRecord(state.projectRoot, { repoSlug, checkout });
|
|
2488
|
+
return deps.jsonResponse({ ok: true, project: record });
|
|
2489
|
+
}
|
|
2490
|
+
const snapshotUploadMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/upload-snapshot$/);
|
|
2491
|
+
if (snapshotUploadMatch && req.method === "POST") {
|
|
2492
|
+
const repoSlug = decodeURIComponent(snapshotUploadMatch[1] ?? "");
|
|
2493
|
+
if (!normalizeRepoSlug(repoSlug)) {
|
|
2494
|
+
return deps.jsonResponse({ ok: false, error: "repoSlug must be owner/repo" }, 400);
|
|
2495
|
+
}
|
|
2496
|
+
const body = await deps.readJsonBody(req);
|
|
2497
|
+
const archiveContentBase64 = normalizeString(body.archiveContentBase64);
|
|
2498
|
+
if (!archiveContentBase64) {
|
|
2499
|
+
return deps.badRequest("archiveContentBase64 is required");
|
|
2500
|
+
}
|
|
2501
|
+
const baseDir = normalizeString(body.baseDir) ?? normalizeString(process.env.RIG_REMOTE_SNAPSHOT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve11(normalizeString(process.env.RIG_STATE_DIR), "remote-snapshots") : resolve11(state.projectRoot, ".rig", "remote-snapshots"));
|
|
2502
|
+
const checkoutKey = normalizeString(body.checkoutKey) ?? "default";
|
|
2503
|
+
try {
|
|
2504
|
+
const archive = parseSnapshotArchiveContentBase64(archiveContentBase64);
|
|
2505
|
+
const checkout = extractUploadedSnapshotArchive({
|
|
2506
|
+
repoSlug,
|
|
2507
|
+
baseDir,
|
|
2508
|
+
archive,
|
|
2509
|
+
snapshotId: normalizeString(body.snapshotId) ?? undefined,
|
|
2510
|
+
checkoutKey
|
|
2511
|
+
});
|
|
2512
|
+
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2513
|
+
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2514
|
+
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2515
|
+
const project = linkProjectCheckout(state.projectRoot, repoSlug, {
|
|
2516
|
+
kind: "uploaded-snapshot",
|
|
2517
|
+
path: checkout.path,
|
|
2518
|
+
ref: checkout.snapshotId
|
|
2519
|
+
});
|
|
2520
|
+
deps.snapshotService.invalidate("uploaded-snapshot-checkout");
|
|
2521
|
+
deps.broadcastSnapshotInvalidation(state, "uploaded-snapshot-checkout");
|
|
2522
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
return deps.jsonResponse({
|
|
2525
|
+
ok: false,
|
|
2526
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2527
|
+
}, 400);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
const prepareCheckoutMatch = url.pathname.match(/^\/api\/projects\/(.+?)\/prepare-checkout$/);
|
|
2531
|
+
if (prepareCheckoutMatch && req.method === "POST") {
|
|
2532
|
+
const repoSlug = decodeURIComponent(prepareCheckoutMatch[1] ?? "");
|
|
2533
|
+
if (!normalizeRepoSlug(repoSlug)) {
|
|
2534
|
+
return deps.jsonResponse({ ok: false, error: "repoSlug must be owner/repo" }, 400);
|
|
2535
|
+
}
|
|
2536
|
+
const body = await deps.readJsonBody(req);
|
|
2537
|
+
const checkoutInput = asPlainRecord(body.checkout) ?? asPlainRecord(body.strategy) ?? body;
|
|
2538
|
+
const kind = normalizeString(checkoutInput.kind) ?? "managed-clone";
|
|
2539
|
+
if (kind !== "managed-clone" && kind !== "current-ref" && kind !== "existing-path") {
|
|
2540
|
+
return deps.jsonResponse({ ok: false, error: "checkout kind must be managed-clone, current-ref, or existing-path" }, 400);
|
|
2541
|
+
}
|
|
2542
|
+
const baseDir = normalizeString(body.baseDir) ?? normalizeString(checkoutInput.baseDir) ?? normalizeString(process.env.RIG_REMOTE_CHECKOUT_BASE_DIR) ?? (normalizeString(process.env.RIG_STATE_DIR) ? resolve11(normalizeString(process.env.RIG_STATE_DIR), "remote-checkouts") : resolve11(state.projectRoot, ".rig", "remote-checkouts"));
|
|
2543
|
+
const checkoutKey = normalizeString(body.checkoutKey) ?? normalizeString(checkoutInput.checkoutKey) ?? normalizeString(checkoutInput.key) ?? "default";
|
|
2544
|
+
const repoUrl = normalizeString(body.repoUrl) ?? normalizeString(checkoutInput.repoUrl) ?? `https://github.com/${repoSlug}.git`;
|
|
2545
|
+
const credentialToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
2546
|
+
try {
|
|
2547
|
+
const checkout = await prepareRemoteCheckout({
|
|
2548
|
+
command: runRemoteCheckoutCommand,
|
|
2549
|
+
strategy: kind === "current-ref" ? { kind: "current-ref", repoSlug, repoUrl, baseDir, checkoutKey, ref: normalizeString(checkoutInput.ref) ?? "HEAD", credentialToken } : kind === "existing-path" ? { kind: "existing-path", path: normalizeString(checkoutInput.path) ?? state.projectRoot } : { kind: "managed-clone", repoSlug, repoUrl, baseDir, checkoutKey, credentialToken }
|
|
2550
|
+
});
|
|
2551
|
+
const checkoutRepair = repairRemoteCheckoutForRig(checkout.path, repoSlug);
|
|
2552
|
+
const packageInstall = installRemoteCheckoutPackages(checkout.path);
|
|
2553
|
+
const postInstallConfigValidation = validateRemoteCheckoutRigConfig(checkout.path);
|
|
2554
|
+
const project = linkProjectCheckout(state.projectRoot, repoSlug, {
|
|
2555
|
+
kind: checkout.kind,
|
|
2556
|
+
path: checkout.path,
|
|
2557
|
+
ref: checkout.ref ?? checkout.snapshotId ?? undefined
|
|
2558
|
+
});
|
|
2559
|
+
deps.snapshotService.invalidate("remote-checkout-prepared");
|
|
2560
|
+
deps.broadcastSnapshotInvalidation(state, "remote-checkout-prepared");
|
|
2561
|
+
return deps.jsonResponse({ ok: true, checkout, project, checkoutRepair, packageInstall, postInstallConfigValidation });
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const projectMatch = url.pathname.match(/^\/api\/projects\/(.+?)(?:\/link-checkout)?$/);
|
|
2567
|
+
if (projectMatch) {
|
|
2568
|
+
const repoSlug = decodeURIComponent(projectMatch[1] ?? "");
|
|
2569
|
+
if (!normalizeRepoSlug(repoSlug)) {
|
|
2570
|
+
return deps.jsonResponse({ ok: false, error: "repoSlug must be owner/repo" }, 400);
|
|
2571
|
+
}
|
|
2572
|
+
if (url.pathname.endsWith("/link-checkout") && req.method === "POST") {
|
|
2573
|
+
const body = await deps.readJsonBody(req);
|
|
2574
|
+
const kind = normalizeString(body.kind);
|
|
2575
|
+
if (kind !== "local" && kind !== "managed-clone" && kind !== "current-ref" && kind !== "uploaded-snapshot" && kind !== "existing-path") {
|
|
2576
|
+
return deps.jsonResponse({ ok: false, error: "checkout kind is required" }, 400);
|
|
2577
|
+
}
|
|
2578
|
+
const project = linkProjectCheckout(state.projectRoot, repoSlug, {
|
|
2579
|
+
kind,
|
|
2580
|
+
path: normalizeString(body.path) ?? state.projectRoot,
|
|
2581
|
+
ref: normalizeString(body.ref) ?? undefined
|
|
2582
|
+
});
|
|
2583
|
+
return deps.jsonResponse({ ok: true, project });
|
|
2584
|
+
}
|
|
2585
|
+
if (req.method === "GET") {
|
|
2586
|
+
const project = getProjectRecord(state.projectRoot, repoSlug);
|
|
2587
|
+
return project ? deps.jsonResponse({ ok: true, project }) : deps.notFound();
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
if (url.pathname === "/api/server/project-root" && req.method === "POST") {
|
|
2591
|
+
const body = await deps.readJsonBody(req);
|
|
2592
|
+
const requestedRoot = normalizeString(body.projectRoot);
|
|
2593
|
+
if (!requestedRoot) {
|
|
2594
|
+
return deps.badRequest("projectRoot is required");
|
|
2595
|
+
}
|
|
2596
|
+
if (!isAbsolute2(requestedRoot)) {
|
|
2597
|
+
return deps.badRequest("projectRoot must be an absolute path on the Rig server host");
|
|
2598
|
+
}
|
|
2599
|
+
const normalizedRoot = resolve11(requestedRoot);
|
|
2600
|
+
const exists = existsSync8(normalizedRoot);
|
|
2601
|
+
const control = buildServerControlStatus();
|
|
2602
|
+
const switchCommand = process.env.RIG_PROJECT_ROOT_SWITCH_COMMAND?.trim();
|
|
2603
|
+
if (!exists) {
|
|
2604
|
+
return deps.jsonResponse({
|
|
2605
|
+
ok: false,
|
|
2606
|
+
projectRoot: state.projectRoot,
|
|
2607
|
+
requestedProjectRoot: normalizedRoot,
|
|
2608
|
+
exists,
|
|
2609
|
+
control,
|
|
2610
|
+
requiresRestart: true,
|
|
2611
|
+
message: "Requested project root does not exist on the Rig server host."
|
|
2612
|
+
}, 404);
|
|
2613
|
+
}
|
|
2614
|
+
if (!existsSync8(resolve11(normalizedRoot, "rig.config.ts")) && !existsSync8(resolve11(normalizedRoot, "rig.config.json"))) {
|
|
2615
|
+
return deps.jsonResponse({
|
|
2616
|
+
ok: false,
|
|
2617
|
+
projectRoot: state.projectRoot,
|
|
2618
|
+
requestedProjectRoot: normalizedRoot,
|
|
2619
|
+
exists,
|
|
2620
|
+
control,
|
|
2621
|
+
requiresRestart: true,
|
|
2622
|
+
message: "Requested project root exists but has no rig.config.ts or rig.config.json."
|
|
2623
|
+
}, 400);
|
|
2624
|
+
}
|
|
2625
|
+
if (switchCommand) {
|
|
2626
|
+
const result = spawnSync2(switchCommand, [normalizedRoot], {
|
|
2627
|
+
cwd: state.projectRoot,
|
|
2628
|
+
env: process.env,
|
|
2629
|
+
encoding: "utf8"
|
|
2630
|
+
});
|
|
2631
|
+
if (result.error || result.status !== 0) {
|
|
2632
|
+
return deps.jsonResponse({
|
|
2633
|
+
ok: false,
|
|
2634
|
+
projectRoot: state.projectRoot,
|
|
2635
|
+
requestedProjectRoot: normalizedRoot,
|
|
2636
|
+
exists,
|
|
2637
|
+
control,
|
|
2638
|
+
requiresRestart: true,
|
|
2639
|
+
message: result.error instanceof Error ? result.error.message : result.stderr || result.stdout || `Project-root switch command failed (${result.status ?? "unknown"}).`
|
|
2640
|
+
}, 500);
|
|
2641
|
+
}
|
|
2642
|
+
return deps.jsonResponse({
|
|
2643
|
+
ok: true,
|
|
2644
|
+
projectRoot: state.projectRoot,
|
|
2645
|
+
requestedProjectRoot: normalizedRoot,
|
|
2646
|
+
exists,
|
|
2647
|
+
control,
|
|
2648
|
+
requiresRestart: false,
|
|
2649
|
+
message: "Project-root switch accepted. Rig server restart has been scheduled."
|
|
2650
|
+
}, 202);
|
|
2651
|
+
}
|
|
2652
|
+
return deps.jsonResponse({
|
|
2653
|
+
ok: false,
|
|
2654
|
+
projectRoot: state.projectRoot,
|
|
2655
|
+
requestedProjectRoot: normalizedRoot,
|
|
2656
|
+
exists,
|
|
2657
|
+
control,
|
|
2658
|
+
requiresRestart: true,
|
|
2659
|
+
message: "Project root is bound when the Rig server starts. Restart the server or supervisor with this server-host path to switch projects."
|
|
2660
|
+
}, 409);
|
|
2661
|
+
}
|
|
2662
|
+
if (url.pathname === "/api/github/auth/status") {
|
|
2663
|
+
const store = createGitHubAuthStore(state.projectRoot);
|
|
2664
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
2665
|
+
}
|
|
2666
|
+
if (url.pathname === "/api/github/repo/permissions") {
|
|
2667
|
+
const store = createGitHubAuthStore(state.projectRoot);
|
|
2668
|
+
const auth = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2669
|
+
if (!auth.signedIn) {
|
|
2670
|
+
return deps.jsonResponse({ ok: false, signedIn: false, canOpenPullRequest: false, reason: "not-authenticated" }, 401);
|
|
2671
|
+
}
|
|
2672
|
+
const scopeSet = new Set(auth.scopes.map((scope) => scope.toLowerCase()));
|
|
2673
|
+
const broadEnough = scopeSet.has("repo") || scopeSet.has("public_repo") || auth.scopes.length === 0;
|
|
2674
|
+
return deps.jsonResponse({
|
|
2675
|
+
ok: true,
|
|
2676
|
+
signedIn: true,
|
|
2677
|
+
login: auth.login,
|
|
2678
|
+
scopes: auth.scopes,
|
|
2679
|
+
canOpenPullRequest: broadEnough,
|
|
2680
|
+
pullRequests: broadEnough,
|
|
2681
|
+
push: broadEnough,
|
|
2682
|
+
reason: broadEnough ? "stored-token" : "token-scope-unverified"
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
if (url.pathname === "/api/github/auth/token" && req.method === "POST") {
|
|
2686
|
+
const body = await deps.readJsonBody(req);
|
|
2687
|
+
const token = normalizeString(body.token);
|
|
2688
|
+
const selectedRepo = normalizeString(body.selectedRepo);
|
|
2689
|
+
if (!token) {
|
|
2690
|
+
return deps.badRequest("token is required");
|
|
2691
|
+
}
|
|
2692
|
+
try {
|
|
2693
|
+
const user = await fetchGitHubUserInfo(token);
|
|
2694
|
+
const store = createGitHubAuthStore(state.projectRoot);
|
|
2695
|
+
store.saveToken({
|
|
2696
|
+
token,
|
|
2697
|
+
tokenSource: "manual-token",
|
|
2698
|
+
login: user.login,
|
|
2699
|
+
userId: user.userId,
|
|
2700
|
+
scopes: user.scopes,
|
|
2701
|
+
selectedRepo
|
|
2702
|
+
});
|
|
2703
|
+
return deps.jsonResponse({ ok: true, ...store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) });
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2706
|
+
return deps.jsonResponse({ ok: false, error: message }, 400);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (url.pathname === "/api/github/auth/device/start" && req.method === "POST") {
|
|
2710
|
+
const clientId = normalizeString(process.env.RIG_GITHUB_OAUTH_CLIENT_ID);
|
|
2711
|
+
if (!clientId) {
|
|
2712
|
+
return deps.jsonResponse({ ok: false, oauthConfigured: false, error: "RIG_GITHUB_OAUTH_CLIENT_ID is not configured." }, 400);
|
|
2713
|
+
}
|
|
2714
|
+
const body = await deps.readJsonBody(req);
|
|
2715
|
+
const scope = normalizeString(body.scope) ?? "repo read:user user:email";
|
|
2716
|
+
const result = await postGitHubForm("https://github.com/login/device/code", {
|
|
2717
|
+
client_id: clientId,
|
|
2718
|
+
scope
|
|
2719
|
+
});
|
|
2720
|
+
if (result.status < 200 || result.status >= 300 || typeof result.payload.device_code !== "string") {
|
|
2721
|
+
return deps.jsonResponse({ ok: false, error: typeof result.payload.error_description === "string" ? result.payload.error_description : "GitHub device flow start failed." }, 400);
|
|
2722
|
+
}
|
|
2723
|
+
const pollId = randomUUID();
|
|
2724
|
+
const expiresIn = typeof result.payload.expires_in === "number" ? result.payload.expires_in : 900;
|
|
2725
|
+
const interval = typeof result.payload.interval === "number" ? result.payload.interval : 5;
|
|
2726
|
+
createGitHubAuthStore(state.projectRoot).savePendingDevice({
|
|
2727
|
+
pollId,
|
|
2728
|
+
deviceCode: result.payload.device_code,
|
|
2729
|
+
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
2730
|
+
intervalSeconds: interval
|
|
2731
|
+
});
|
|
2732
|
+
return deps.jsonResponse({
|
|
2733
|
+
ok: true,
|
|
2734
|
+
pollId,
|
|
2735
|
+
userCode: typeof result.payload.user_code === "string" ? result.payload.user_code : "",
|
|
2736
|
+
verificationUri: typeof result.payload.verification_uri === "string" ? result.payload.verification_uri : "https://github.com/login/device",
|
|
2737
|
+
expiresIn,
|
|
2738
|
+
interval
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
if (url.pathname === "/api/github/auth/device/poll" && req.method === "POST") {
|
|
2742
|
+
const clientId = normalizeString(process.env.RIG_GITHUB_OAUTH_CLIENT_ID);
|
|
2743
|
+
if (!clientId) {
|
|
2744
|
+
return deps.jsonResponse({ ok: false, oauthConfigured: false, error: "RIG_GITHUB_OAUTH_CLIENT_ID is not configured." }, 400);
|
|
2745
|
+
}
|
|
2746
|
+
const body = await deps.readJsonBody(req);
|
|
2747
|
+
const pollId = normalizeString(body.pollId);
|
|
2748
|
+
if (!pollId) {
|
|
2749
|
+
return deps.badRequest("pollId is required");
|
|
2750
|
+
}
|
|
2751
|
+
const store = createGitHubAuthStore(state.projectRoot);
|
|
2752
|
+
const pending = store.readPendingDevice(pollId);
|
|
2753
|
+
if (!pending) {
|
|
2754
|
+
return deps.jsonResponse({ ok: false, status: "expired", error: "GitHub device flow expired or unknown." }, 410);
|
|
2755
|
+
}
|
|
2756
|
+
const result = await postGitHubForm("https://github.com/login/oauth/access_token", {
|
|
2757
|
+
client_id: clientId,
|
|
2758
|
+
device_code: pending.deviceCode,
|
|
2759
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
2760
|
+
});
|
|
2761
|
+
const error = typeof result.payload.error === "string" ? result.payload.error : null;
|
|
2762
|
+
if (error === "authorization_pending" || error === "slow_down") {
|
|
2763
|
+
return deps.jsonResponse({ ok: true, status: error, interval: error === "slow_down" ? pending.intervalSeconds + 5 : pending.intervalSeconds });
|
|
2764
|
+
}
|
|
2765
|
+
if (error || typeof result.payload.access_token !== "string") {
|
|
2766
|
+
return deps.jsonResponse({ ok: false, status: "error", error: typeof result.payload.error_description === "string" ? result.payload.error_description : "GitHub device authorization failed." }, 400);
|
|
2767
|
+
}
|
|
2768
|
+
const token = result.payload.access_token;
|
|
2769
|
+
const user = await fetchGitHubUserInfo(token);
|
|
2770
|
+
store.saveToken({ token, tokenSource: "oauth-device", login: user.login, userId: user.userId, scopes: user.scopes });
|
|
2771
|
+
return deps.jsonResponse({ ok: true, status: "signed-in", ...store.status({ oauthConfigured: true }) });
|
|
2772
|
+
}
|
|
2773
|
+
if (url.pathname === "/api/github/repo/probe" && req.method === "POST") {
|
|
2774
|
+
const body = await deps.readJsonBody(req);
|
|
2775
|
+
const owner = normalizeString(body.owner);
|
|
2776
|
+
const repo = normalizeString(body.repo);
|
|
2777
|
+
if (!owner || !repo) {
|
|
2778
|
+
return deps.badRequest("owner and repo are required");
|
|
2779
|
+
}
|
|
2780
|
+
const store = createGitHubAuthStore(state.projectRoot);
|
|
2781
|
+
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2782
|
+
const probe = await probeGitHubRepository({ owner, repo, token: store.readToken(), scopes: authStatus.scopes });
|
|
2783
|
+
return deps.jsonResponse({ ok: probe.ok, probe }, probe.ok ? 200 : 400);
|
|
2784
|
+
}
|
|
2785
|
+
if (url.pathname === "/api/project/github/preview" && req.method === "POST") {
|
|
2786
|
+
const body = await deps.readJsonBody(req);
|
|
2787
|
+
const owner = normalizeString(body.owner);
|
|
2788
|
+
const repo = normalizeString(body.repo);
|
|
2789
|
+
const rawProjectName = normalizeString(body.projectName);
|
|
2790
|
+
const assignee = normalizeString(body.assignee);
|
|
2791
|
+
if (!owner || !repo) {
|
|
2792
|
+
return deps.badRequest("owner and repo are required");
|
|
2793
|
+
}
|
|
2794
|
+
let targetRoot;
|
|
2795
|
+
try {
|
|
2796
|
+
targetRoot = resolveRequestedProjectRoot(state.projectRoot, body.projectRoot);
|
|
2797
|
+
} catch (error) {
|
|
2798
|
+
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
2799
|
+
}
|
|
2800
|
+
const authStatus = createGitHubAuthStore(state.projectRoot).status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2801
|
+
const configPath = resolve11(targetRoot, "rig.config.ts");
|
|
2802
|
+
const source = buildGitHubProjectConfigSource({
|
|
2803
|
+
projectName: rawProjectName,
|
|
2804
|
+
owner,
|
|
2805
|
+
repo,
|
|
2806
|
+
assignee,
|
|
2807
|
+
githubUserId: authStatus.userId ?? authStatus.login
|
|
2808
|
+
});
|
|
2809
|
+
return deps.jsonResponse({
|
|
2810
|
+
ok: true,
|
|
2811
|
+
projectRoot: targetRoot,
|
|
2812
|
+
configPath,
|
|
2813
|
+
exists: existsSync8(configPath),
|
|
2814
|
+
requiresOverwrite: existsSync8(configPath),
|
|
2815
|
+
source,
|
|
2816
|
+
owner,
|
|
2817
|
+
repo,
|
|
2818
|
+
assignee
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
if (url.pathname === "/api/project/github" && req.method === "POST") {
|
|
2822
|
+
const body = await deps.readJsonBody(req);
|
|
2823
|
+
const owner = normalizeString(body.owner);
|
|
2824
|
+
const repo = normalizeString(body.repo);
|
|
2825
|
+
const rawProjectName = normalizeString(body.projectName);
|
|
2826
|
+
const assignee = normalizeString(body.assignee);
|
|
2827
|
+
const overwrite = body.overwrite === true;
|
|
2828
|
+
if (!owner || !repo) {
|
|
2829
|
+
return deps.badRequest("owner and repo are required");
|
|
2830
|
+
}
|
|
2831
|
+
let targetRoot;
|
|
2832
|
+
try {
|
|
2833
|
+
targetRoot = resolveRequestedProjectRoot(state.projectRoot, body.projectRoot);
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
return deps.badRequest(error instanceof Error ? error.message : String(error));
|
|
2836
|
+
}
|
|
2837
|
+
const store = createGitHubAuthStore(state.projectRoot);
|
|
2838
|
+
const authStatus = store.status({ oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
2839
|
+
const token = store.readToken();
|
|
2840
|
+
if (!token) {
|
|
2841
|
+
return deps.jsonResponse({ ok: false, error: "Sign in with GitHub or save a PAT before configuring GitHub Issues." }, 401);
|
|
2842
|
+
}
|
|
2843
|
+
const source = buildGitHubProjectConfigSource({
|
|
2844
|
+
projectName: rawProjectName,
|
|
2845
|
+
owner,
|
|
2846
|
+
repo,
|
|
2847
|
+
assignee,
|
|
2848
|
+
githubUserId: authStatus.userId ?? authStatus.login
|
|
2849
|
+
});
|
|
2850
|
+
const configPath = resolve11(targetRoot, "rig.config.ts");
|
|
2851
|
+
if (existsSync8(configPath) && !overwrite) {
|
|
2852
|
+
return deps.jsonResponse({
|
|
2853
|
+
ok: false,
|
|
2854
|
+
error: "rig.config.ts already exists. Confirm overwrite to replace it; Rig will create a backup first.",
|
|
2855
|
+
existingConfig: true,
|
|
2856
|
+
requiresOverwrite: true,
|
|
2857
|
+
projectRoot: targetRoot,
|
|
2858
|
+
configPath,
|
|
2859
|
+
previewSource: source
|
|
2860
|
+
}, 409);
|
|
2861
|
+
}
|
|
2862
|
+
const repoProbe = await probeGitHubRepository({ owner, repo, token, scopes: authStatus.scopes });
|
|
2863
|
+
if (!repoProbe.ok) {
|
|
2864
|
+
return deps.jsonResponse({ ok: false, error: repoProbe.message, repoProbe }, 400);
|
|
2865
|
+
}
|
|
2866
|
+
let backupPath = null;
|
|
2867
|
+
if (existsSync8(configPath)) {
|
|
2868
|
+
backupPath = backupConfigPath(configPath);
|
|
2869
|
+
copyFileSync(configPath, backupPath);
|
|
2870
|
+
}
|
|
2871
|
+
writeFileSync7(configPath, source, "utf8");
|
|
2872
|
+
const selectedRepo = `${owner}/${repo}`;
|
|
2873
|
+
store.saveSelectedRepo(selectedRepo);
|
|
2874
|
+
const targetStore = createGitHubAuthStore(targetRoot);
|
|
2875
|
+
const targetToken = store.readToken();
|
|
2876
|
+
if (targetToken) {
|
|
2877
|
+
targetStore.saveToken({
|
|
2878
|
+
token: targetToken,
|
|
2879
|
+
tokenSource: authStatus.tokenSource === "oauth-device" ? "oauth-device" : "manual-token",
|
|
2880
|
+
login: authStatus.login,
|
|
2881
|
+
userId: authStatus.userId,
|
|
2882
|
+
scopes: authStatus.scopes,
|
|
2883
|
+
selectedRepo
|
|
2884
|
+
});
|
|
2885
|
+
} else {
|
|
2886
|
+
targetStore.saveSelectedRepo(selectedRepo);
|
|
2887
|
+
}
|
|
2888
|
+
const switchResult = runProjectRootSwitchIfNeeded(state.projectRoot, targetRoot);
|
|
2889
|
+
deps.snapshotService.invalidate("project-github-config-updated");
|
|
2890
|
+
deps.broadcastSnapshotInvalidation(state, "project-github-config-updated");
|
|
2891
|
+
return deps.jsonResponse({
|
|
2892
|
+
ok: true,
|
|
2893
|
+
projectRoot: targetRoot,
|
|
2894
|
+
configPath,
|
|
2895
|
+
backupPath,
|
|
2896
|
+
owner,
|
|
2897
|
+
repo,
|
|
2898
|
+
assignee,
|
|
2899
|
+
source,
|
|
2900
|
+
repoProbe,
|
|
2901
|
+
switched: switchResult.switched,
|
|
2902
|
+
message: switchResult.message ?? undefined
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
if (url.pathname === "/api/project/config-status") {
|
|
2906
|
+
return deps.jsonResponse(buildProjectConfigStatus(state.projectRoot));
|
|
2907
|
+
}
|
|
2908
|
+
if (url.pathname === "/api/snapshot") {
|
|
2909
|
+
const payload = await deps.snapshotService.getRigSnapshot();
|
|
2910
|
+
return deps.jsonResponse({
|
|
2911
|
+
workspace: payload.workspace,
|
|
2912
|
+
workspaces: payload.snapshot.workspaces,
|
|
2913
|
+
graphs: payload.graphs,
|
|
2914
|
+
tasks: payload.tasks,
|
|
2915
|
+
runs: payload.runs,
|
|
2916
|
+
approvals: payload.approvals,
|
|
2917
|
+
userInputs: payload.userInputs,
|
|
2918
|
+
artifacts: payload.artifacts,
|
|
2919
|
+
queue: payload.queue,
|
|
2920
|
+
taskSourceDiagnostics: deps.snapshotService.getTaskSourceErrors(),
|
|
2921
|
+
updatedAt: payload.snapshot.updatedAt
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
if (url.pathname === "/api/inspector/snapshot") {
|
|
2925
|
+
const snapshot = state.inspector ? state.inspector.snapshot() : {
|
|
2926
|
+
activeRuns: [],
|
|
2927
|
+
recentFindings: [],
|
|
2928
|
+
followups: [],
|
|
2929
|
+
analysisReports: [],
|
|
2930
|
+
availableTools: []
|
|
2931
|
+
};
|
|
2932
|
+
return deps.jsonResponse({
|
|
2933
|
+
...snapshot,
|
|
2934
|
+
agent: state.inspectorAgent?.snapshot() ?? null,
|
|
2935
|
+
lifecycle: inspectorAgentLifecycleSnapshot({
|
|
2936
|
+
inspector: state.inspector,
|
|
2937
|
+
agent: state.inspectorAgent,
|
|
2938
|
+
retry: state.inspectorAgentLifecycle?.snapshot() ?? null
|
|
2939
|
+
})
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
if (url.pathname === "/api/inspector/stream") {
|
|
2943
|
+
const once = deps.isTruthyQuery(url.searchParams.get("once"));
|
|
2944
|
+
const pollMs = deps.normalizePositiveIntQuery(url.searchParams.get("pollMs"), 1000);
|
|
2945
|
+
let timer = null;
|
|
2946
|
+
let closed = false;
|
|
2947
|
+
const stream = new ReadableStream({
|
|
2948
|
+
start(controller) {
|
|
2949
|
+
let sequence = 0;
|
|
2950
|
+
let lastFingerprint = null;
|
|
2951
|
+
const stop = () => {
|
|
2952
|
+
if (timer) {
|
|
2953
|
+
clearInterval(timer);
|
|
2954
|
+
timer = null;
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
const emitSnapshot = () => {
|
|
2958
|
+
if (closed) {
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
const payload = deps.buildInspectorStreamPayload(state, sequence + 1);
|
|
2962
|
+
const fingerprint = JSON.stringify({
|
|
2963
|
+
snapshot: payload.snapshot,
|
|
2964
|
+
agent: payload.agent,
|
|
2965
|
+
journal: payload.journal
|
|
2966
|
+
});
|
|
2967
|
+
if (!once && fingerprint === lastFingerprint) {
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
sequence += 1;
|
|
2971
|
+
lastFingerprint = fingerprint;
|
|
2972
|
+
controller.enqueue(deps.formatSseEvent("snapshot", { ...payload, sequence }));
|
|
2973
|
+
if (once) {
|
|
2974
|
+
closed = true;
|
|
2975
|
+
stop();
|
|
2976
|
+
controller.close();
|
|
2977
|
+
}
|
|
2978
|
+
};
|
|
2979
|
+
try {
|
|
2980
|
+
emitSnapshot();
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
controller.enqueue(deps.formatSseEvent("error", {
|
|
2983
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2984
|
+
}));
|
|
2985
|
+
controller.close();
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
if (!once) {
|
|
2989
|
+
timer = setInterval(() => {
|
|
2990
|
+
try {
|
|
2991
|
+
emitSnapshot();
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
if (!closed) {
|
|
2994
|
+
controller.enqueue(deps.formatSseEvent("error", {
|
|
2995
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2996
|
+
}));
|
|
2997
|
+
closed = true;
|
|
2998
|
+
stop();
|
|
2999
|
+
controller.close();
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}, pollMs);
|
|
3003
|
+
}
|
|
3004
|
+
},
|
|
3005
|
+
cancel() {
|
|
3006
|
+
closed = true;
|
|
3007
|
+
if (timer) {
|
|
3008
|
+
clearInterval(timer);
|
|
3009
|
+
timer = null;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
return new Response(stream, {
|
|
3014
|
+
headers: {
|
|
3015
|
+
"Content-Type": "text/event-stream",
|
|
3016
|
+
"Cache-Control": "no-cache",
|
|
3017
|
+
Connection: "keep-alive"
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
if (url.pathname === "/api/inspector/tools/invoke" && req.method === "POST") {
|
|
3022
|
+
const body = await deps.readJsonBody(req);
|
|
3023
|
+
const name = normalizeString(body.name);
|
|
3024
|
+
const input = body.input && typeof body.input === "object" ? body.input : {};
|
|
3025
|
+
if (!name) {
|
|
3026
|
+
return deps.badRequest("name is required");
|
|
3027
|
+
}
|
|
3028
|
+
if (!state.inspector) {
|
|
3029
|
+
return deps.jsonResponse({ status: "unavailable", summary: "Inspector is not available", details: null }, 503);
|
|
3030
|
+
}
|
|
3031
|
+
return deps.jsonResponse(await state.inspector.invokeTool(name, input));
|
|
3032
|
+
}
|
|
3033
|
+
if (url.pathname === "/api/runs") {
|
|
3034
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "100", 10);
|
|
3035
|
+
const runs = listAuthorityRuns7(state.projectRoot).slice(0, Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 100);
|
|
3036
|
+
return deps.jsonResponse(runs);
|
|
3037
|
+
}
|
|
3038
|
+
if (url.pathname === "/api/runs/task" && req.method === "POST") {
|
|
3039
|
+
const body = await deps.readJsonBody(req);
|
|
3040
|
+
const runId = normalizeString(body.runId) ?? crypto.randomUUID();
|
|
3041
|
+
const taskId = normalizeString(body.taskId);
|
|
3042
|
+
if (!taskId) {
|
|
3043
|
+
return deps.badRequest("taskId is required");
|
|
3044
|
+
}
|
|
3045
|
+
const createdAt = new Date().toISOString();
|
|
3046
|
+
const executionTarget = normalizeString(body.executionTarget) === "remote" ? "remote" : "local";
|
|
3047
|
+
try {
|
|
3048
|
+
await deps.createRunRecord(state.projectRoot, {
|
|
3049
|
+
runId,
|
|
3050
|
+
workspaceId: RIG_WORKSPACE_ID,
|
|
3051
|
+
taskId,
|
|
3052
|
+
title: normalizeString(body.title) ?? undefined,
|
|
3053
|
+
runtimeAdapter: normalizeRuntimeAdapter(body.runtimeAdapter),
|
|
3054
|
+
model: normalizeString(body.model) ?? undefined,
|
|
3055
|
+
runtimeMode: normalizeString(body.runtimeMode) ?? undefined,
|
|
3056
|
+
interactionMode: normalizeString(body.interactionMode) ?? undefined,
|
|
3057
|
+
executionTarget,
|
|
3058
|
+
remoteHostId: normalizeString(body.remoteHostId),
|
|
3059
|
+
initialPrompt: normalizeString(body.initialPrompt) ?? undefined,
|
|
3060
|
+
baselineMode: normalizeString(body.baselineMode) ?? undefined,
|
|
3061
|
+
prMode: normalizePrMode(body.prMode),
|
|
3062
|
+
initiatedBy: requestAuth.actor,
|
|
3063
|
+
createdAt
|
|
3064
|
+
});
|
|
3065
|
+
} catch (error) {
|
|
3066
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, /active Rig run/i.test(error instanceof Error ? error.message : String(error)) ? 409 : 400);
|
|
3067
|
+
}
|
|
3068
|
+
dequeueTaskState(state.projectRoot, taskId);
|
|
3069
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3070
|
+
if (executionTarget === "local") {
|
|
3071
|
+
queueMicrotask(() => {
|
|
3072
|
+
deps.startLocalRun(state, runId).catch((error) => {
|
|
3073
|
+
console.error("[server] failed to start local task run", error);
|
|
3074
|
+
});
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
queueMicrotask(() => {
|
|
3078
|
+
deps.reconcileScheduler(state, "api-task-run-created");
|
|
3079
|
+
});
|
|
3080
|
+
return deps.jsonResponse({ ok: true, runId, createdAt, accepted: true });
|
|
3081
|
+
}
|
|
3082
|
+
if (url.pathname === "/api/runs/adhoc" && req.method === "POST") {
|
|
3083
|
+
const body = await deps.readJsonBody(req);
|
|
3084
|
+
const runId = normalizeString(body.runId) ?? crypto.randomUUID();
|
|
3085
|
+
const title = normalizeString(body.title) ?? `Run ${runId}`;
|
|
3086
|
+
const initialPrompt = normalizeString(body.initialPrompt);
|
|
3087
|
+
if (!initialPrompt && !title) {
|
|
3088
|
+
return deps.badRequest("title or initialPrompt is required");
|
|
3089
|
+
}
|
|
3090
|
+
const createdAt = new Date().toISOString();
|
|
3091
|
+
const executionTarget = normalizeString(body.executionTarget) === "remote" ? "remote" : "local";
|
|
3092
|
+
await deps.createRunRecord(state.projectRoot, {
|
|
3093
|
+
runId,
|
|
3094
|
+
workspaceId: RIG_WORKSPACE_ID,
|
|
3095
|
+
title,
|
|
3096
|
+
runtimeAdapter: normalizeRuntimeAdapter(body.runtimeAdapter),
|
|
3097
|
+
model: normalizeString(body.model) ?? undefined,
|
|
3098
|
+
runtimeMode: normalizeString(body.runtimeMode) ?? undefined,
|
|
3099
|
+
interactionMode: normalizeString(body.interactionMode) ?? undefined,
|
|
3100
|
+
executionTarget,
|
|
3101
|
+
remoteHostId: normalizeString(body.remoteHostId),
|
|
3102
|
+
initialPrompt: initialPrompt ?? title,
|
|
3103
|
+
baselineMode: normalizeString(body.baselineMode) ?? undefined,
|
|
3104
|
+
prMode: normalizePrMode(body.prMode),
|
|
3105
|
+
initiatedBy: requestAuth.actor,
|
|
3106
|
+
createdAt
|
|
3107
|
+
});
|
|
3108
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3109
|
+
if (executionTarget === "local") {
|
|
3110
|
+
queueMicrotask(() => {
|
|
3111
|
+
deps.startLocalRun(state, runId).catch((error) => {
|
|
3112
|
+
console.error("[server] failed to start local adhoc run", error);
|
|
3113
|
+
});
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
return deps.jsonResponse({ ok: true, runId, createdAt, accepted: true });
|
|
3117
|
+
}
|
|
3118
|
+
if (url.pathname === "/api/runs/stop" && req.method === "POST") {
|
|
3119
|
+
const body = await deps.readJsonBody(req);
|
|
3120
|
+
const runId = normalizeString(body.runId);
|
|
3121
|
+
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3122
|
+
if (!runId) {
|
|
3123
|
+
return deps.badRequest("runId is required");
|
|
3124
|
+
}
|
|
3125
|
+
try {
|
|
3126
|
+
deps.stopRunRecord(state, { runId, createdAt }).catch((error) => {
|
|
3127
|
+
console.error("[server] failed to stop run", error);
|
|
3128
|
+
});
|
|
3129
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3130
|
+
queueMicrotask(() => {
|
|
3131
|
+
deps.reconcileScheduler(state, "api-run-stopped");
|
|
3132
|
+
});
|
|
3133
|
+
return deps.jsonResponse({ ok: true, runId, createdAt, accepted: true });
|
|
3134
|
+
} catch (error) {
|
|
3135
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
if (url.pathname === "/api/runs/resume" && req.method === "POST") {
|
|
3139
|
+
const body = await deps.readJsonBody(req);
|
|
3140
|
+
const runId = normalizeString(body.runId);
|
|
3141
|
+
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3142
|
+
const promptOverride = normalizeString(body.promptOverride);
|
|
3143
|
+
if (!runId) {
|
|
3144
|
+
return deps.badRequest("runId is required");
|
|
3145
|
+
}
|
|
3146
|
+
try {
|
|
3147
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
3148
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3149
|
+
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
3150
|
+
} catch (error) {
|
|
3151
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if (url.pathname === "/api/pi-rig/install" && req.method === "POST") {
|
|
3155
|
+
const configuredPackageSource = normalizeString(process.env.RIG_PI_RIG_PACKAGE_SOURCE);
|
|
3156
|
+
const packageSource = configuredPackageSource ?? [process.env.RIG_HOST_PROJECT_ROOT, process.cwd(), state.projectRoot].map((root) => normalizeString(root)).filter((root) => Boolean(root)).map((root) => resolve11(root, "packages", "pi-rig")).find((candidate) => existsSync8(resolve11(candidate, "package.json"))) ?? "npm:@rig/pi-rig";
|
|
3157
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
3158
|
+
return deps.jsonResponse({ ok: true, installed: true, piOk: true, piRigOk: true, extensionPath: "remote:~/.pi/agent/extensions/pi-rig", packageSource });
|
|
3159
|
+
}
|
|
3160
|
+
let version = spawnSync2("pi", ["--version"], { cwd: state.projectRoot, env: process.env, encoding: "utf8" });
|
|
3161
|
+
let piInstalledOrUpdated = false;
|
|
3162
|
+
if (version.error || version.status !== 0) {
|
|
3163
|
+
const installCommand = normalizeString(process.env.RIG_PI_INSTALL_COMMAND) ?? "bunx @earendil-works/pi@latest install";
|
|
3164
|
+
const parts = installCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
3165
|
+
if (parts.length > 0) {
|
|
3166
|
+
const piInstall = spawnSync2(parts[0], parts.slice(1), { cwd: state.projectRoot, env: process.env, encoding: "utf8" });
|
|
3167
|
+
piInstalledOrUpdated = true;
|
|
3168
|
+
if (!piInstall.error && piInstall.status === 0) {
|
|
3169
|
+
version = spawnSync2("pi", ["--version"], { cwd: state.projectRoot, env: process.env, encoding: "utf8" });
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
if (version.error || version.status !== 0) {
|
|
3173
|
+
return deps.jsonResponse({
|
|
3174
|
+
ok: false,
|
|
3175
|
+
installed: false,
|
|
3176
|
+
piOk: false,
|
|
3177
|
+
piRigOk: false,
|
|
3178
|
+
piInstalledOrUpdated,
|
|
3179
|
+
error: version.error instanceof Error ? version.error.message : version.stderr || version.stdout || "pi --version failed",
|
|
3180
|
+
extensionPath: "remote:~/.pi/agent/extensions/pi-rig"
|
|
3181
|
+
}, 200);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
const install = spawnSync2("pi", ["install", packageSource], { cwd: state.projectRoot, env: process.env, encoding: "utf8" });
|
|
3185
|
+
const installed = !install.error && install.status === 0;
|
|
3186
|
+
return deps.jsonResponse({
|
|
3187
|
+
ok: installed,
|
|
3188
|
+
installed,
|
|
3189
|
+
piOk: true,
|
|
3190
|
+
piRigOk: installed,
|
|
3191
|
+
piVersion: version.stdout.trim() || version.stderr.trim() || undefined,
|
|
3192
|
+
piInstalledOrUpdated,
|
|
3193
|
+
extensionPath: "remote:~/.pi/agent/extensions/pi-rig",
|
|
3194
|
+
packageSource,
|
|
3195
|
+
...installed ? {} : { error: install.error instanceof Error ? install.error.message : install.stderr || install.stdout || `pi install failed (${install.status ?? "unknown"})` }
|
|
3196
|
+
}, 200);
|
|
3197
|
+
}
|
|
3198
|
+
if (url.pathname === "/api/remote/endpoints" && req.method === "GET") {
|
|
3199
|
+
return deps.jsonResponse({
|
|
3200
|
+
endpoints: listManagedRemoteEndpoints2(undefined, state.projectRoot).map((endpoint) => redactRemoteEndpoint(endpoint))
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
if (url.pathname === "/api/remote/endpoints/doctor") {
|
|
3204
|
+
return deps.jsonResponse(doctorManagedRemoteEndpoints(state.projectRoot));
|
|
3205
|
+
}
|
|
3206
|
+
if (url.pathname === "/api/remote/endpoints" && req.method === "POST") {
|
|
3207
|
+
const body = await deps.readJsonBody(req);
|
|
3208
|
+
const alias = normalizeString(body.alias);
|
|
3209
|
+
const host = normalizeString(body.host);
|
|
3210
|
+
const token = normalizeString(body.token);
|
|
3211
|
+
const port = typeof body.port === "number" ? body.port : typeof body.port === "string" ? Number.parseInt(body.port, 10) : NaN;
|
|
3212
|
+
if (!alias || !host || !Number.isFinite(port) || port <= 0) {
|
|
3213
|
+
return deps.badRequest("alias, host, and port are required");
|
|
3214
|
+
}
|
|
3215
|
+
const endpoint = upsertManagedRemoteEndpoint2({ alias, host, port, token: token ?? undefined }, undefined, state.projectRoot);
|
|
3216
|
+
return deps.jsonResponse({ endpoint: redactRemoteEndpoint(endpoint) });
|
|
3217
|
+
}
|
|
3218
|
+
if (url.pathname === "/api/remote/endpoints/update" && req.method === "POST") {
|
|
3219
|
+
const body = await deps.readJsonBody(req);
|
|
3220
|
+
const updated = updateManagedRemoteEndpointInAuthority(state.projectRoot, {
|
|
3221
|
+
endpointId: normalizeString(body.endpointId) ?? undefined,
|
|
3222
|
+
alias: normalizeString(body.alias) ?? undefined,
|
|
3223
|
+
host: normalizeString(body.host) ?? undefined,
|
|
3224
|
+
port: typeof body.port === "number" ? body.port : typeof body.port === "string" ? Number.parseInt(body.port, 10) : undefined,
|
|
3225
|
+
token: normalizeString(body.token) ?? undefined
|
|
3226
|
+
});
|
|
3227
|
+
if (!updated) {
|
|
3228
|
+
return deps.jsonResponse({ ok: false, error: "Remote endpoint not found" }, 404);
|
|
3229
|
+
}
|
|
3230
|
+
return deps.jsonResponse({ endpoint: redactRemoteEndpoint(updated) });
|
|
3231
|
+
}
|
|
3232
|
+
if (url.pathname === "/api/remote/endpoints/remove" && req.method === "POST") {
|
|
3233
|
+
const body = await deps.readJsonBody(req);
|
|
3234
|
+
const alias = normalizeString(body.alias);
|
|
3235
|
+
if (!alias) {
|
|
3236
|
+
return deps.badRequest("alias is required");
|
|
3237
|
+
}
|
|
3238
|
+
const removed = removeManagedRemoteEndpoint2(alias, undefined, state.projectRoot);
|
|
3239
|
+
return removed ? deps.jsonResponse({ ok: true, alias }) : deps.jsonResponse({ ok: false, error: "Remote endpoint not found" }, 404);
|
|
3240
|
+
}
|
|
3241
|
+
if (url.pathname === "/api/remote/endpoints/test" && req.method === "POST") {
|
|
3242
|
+
const body = await deps.readJsonBody(req);
|
|
3243
|
+
try {
|
|
3244
|
+
const endpoint = resolveRemoteEndpoint2({
|
|
3245
|
+
projectRoot: state.projectRoot,
|
|
3246
|
+
remoteAlias: normalizeString(body.alias) ?? undefined,
|
|
3247
|
+
host: normalizeString(body.host) ?? undefined,
|
|
3248
|
+
port: typeof body.port === "number" ? String(body.port) : normalizeString(body.port) ?? undefined,
|
|
3249
|
+
token: normalizeString(body.token) ?? undefined
|
|
3250
|
+
});
|
|
3251
|
+
const client = new RemoteWsClient2(endpoint);
|
|
3252
|
+
await client.connect();
|
|
3253
|
+
const response = await client.ping();
|
|
3254
|
+
client.disconnect();
|
|
3255
|
+
return deps.jsonResponse({
|
|
3256
|
+
ok: true,
|
|
3257
|
+
endpoint: {
|
|
3258
|
+
alias: endpoint.alias ?? null,
|
|
3259
|
+
host: endpoint.host,
|
|
3260
|
+
port: endpoint.port,
|
|
3261
|
+
tokenConfigured: endpoint.token.trim().length > 0
|
|
3262
|
+
},
|
|
3263
|
+
response: redactSecretFields(response)
|
|
3264
|
+
});
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 502);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
if (url.pathname === "/api/remote/hosts/register" && req.method === "POST") {
|
|
3270
|
+
const body = await deps.readJsonBody(req);
|
|
3271
|
+
const workspaceId = normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID;
|
|
3272
|
+
const hostId = normalizeString(body.hostId);
|
|
3273
|
+
const name = normalizeString(body.name);
|
|
3274
|
+
const baseUrl = normalizeString(body.baseUrl);
|
|
3275
|
+
if (!hostId || !name || !baseUrl) {
|
|
3276
|
+
return deps.badRequest("hostId, name, and baseUrl are required");
|
|
3277
|
+
}
|
|
3278
|
+
const acceptedAt = new Date().toISOString();
|
|
3279
|
+
state.remoteHosts.set(hostId, {
|
|
3280
|
+
workspaceId,
|
|
3281
|
+
hostId,
|
|
3282
|
+
name,
|
|
3283
|
+
baseUrl,
|
|
3284
|
+
workspacePath: normalizeString(body.workspacePath),
|
|
3285
|
+
transport: normalizeString(body.transport) ?? "websocket",
|
|
3286
|
+
hostname: normalizeString(body.hostname),
|
|
3287
|
+
region: normalizeString(body.region),
|
|
3288
|
+
labels: normalizeStringArray(body.labels),
|
|
3289
|
+
capabilities: normalizeStringArray(body.capabilities),
|
|
3290
|
+
runtimeAdapters: normalizeStringArray(body.runtimeAdapters),
|
|
3291
|
+
status: normalizeString(body.status) ?? "ready",
|
|
3292
|
+
currentLeaseCount: typeof body.currentLeaseCount === "number" ? body.currentLeaseCount : Number(body.currentLeaseCount ?? 0) || 0,
|
|
3293
|
+
registeredAt: acceptedAt,
|
|
3294
|
+
lastHeartbeatAt: acceptedAt
|
|
3295
|
+
});
|
|
3296
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3297
|
+
await deps.reconcileScheduler(state, "remote-host-registered");
|
|
3298
|
+
return deps.jsonResponse({
|
|
3299
|
+
ok: true,
|
|
3300
|
+
workspaceId,
|
|
3301
|
+
hostId,
|
|
3302
|
+
acceptedAt,
|
|
3303
|
+
heartbeatIntervalMs: 15000
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
if (url.pathname === "/api/remote/hosts/heartbeat" && req.method === "POST") {
|
|
3307
|
+
const body = await deps.readJsonBody(req);
|
|
3308
|
+
const hostId = normalizeString(body.hostId);
|
|
3309
|
+
if (!hostId) {
|
|
3310
|
+
return deps.badRequest("hostId is required");
|
|
3311
|
+
}
|
|
3312
|
+
const existing = state.remoteHosts.get(hostId);
|
|
3313
|
+
if (!existing) {
|
|
3314
|
+
return deps.jsonResponse({ ok: false, error: "Remote host not registered" }, 404);
|
|
3315
|
+
}
|
|
3316
|
+
const acceptedAt = new Date().toISOString();
|
|
3317
|
+
state.remoteHosts.set(hostId, {
|
|
3318
|
+
...existing,
|
|
3319
|
+
status: normalizeString(body.status) ?? existing.status,
|
|
3320
|
+
currentLeaseCount: typeof body.currentLeaseCount === "number" ? body.currentLeaseCount : Number(body.currentLeaseCount ?? existing.currentLeaseCount) || existing.currentLeaseCount,
|
|
3321
|
+
lastHeartbeatAt: normalizeString(body.observedAt) ?? acceptedAt
|
|
3322
|
+
});
|
|
3323
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3324
|
+
await deps.reconcileScheduler(state, "remote-host-heartbeat");
|
|
3325
|
+
return deps.jsonResponse({
|
|
3326
|
+
ok: true,
|
|
3327
|
+
workspaceId: existing.workspaceId,
|
|
3328
|
+
hostId,
|
|
3329
|
+
acceptedAt,
|
|
3330
|
+
heartbeatIntervalMs: 15000
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
if (url.pathname === "/api/remote/runs/claim" && req.method === "POST") {
|
|
3334
|
+
const body = await deps.readJsonBody(req);
|
|
3335
|
+
const workspaceId = normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID;
|
|
3336
|
+
const hostId = normalizeString(body.hostId);
|
|
3337
|
+
if (!hostId) {
|
|
3338
|
+
return deps.badRequest("hostId is required");
|
|
3339
|
+
}
|
|
3340
|
+
const runtimeAdapters = normalizeStringArray(body.runtimeAdapters).map((entry) => normalizeRuntimeAdapter(entry));
|
|
3341
|
+
const claimed = await deps.claimRemoteRun(state, hostId, runtimeAdapters);
|
|
3342
|
+
return deps.jsonResponse({
|
|
3343
|
+
ok: true,
|
|
3344
|
+
workspaceId,
|
|
3345
|
+
hostId,
|
|
3346
|
+
acceptedAt: new Date().toISOString(),
|
|
3347
|
+
lease: claimed.lease,
|
|
3348
|
+
bundle: claimed.bundle
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
if (url.pathname === "/api/remote/runs/start" && req.method === "POST") {
|
|
3352
|
+
const body = await deps.readJsonBody(req);
|
|
3353
|
+
const runId = normalizeString(body.runId);
|
|
3354
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3355
|
+
const hostId = normalizeString(body.hostId);
|
|
3356
|
+
if (!runId || !leaseId || !hostId) {
|
|
3357
|
+
return deps.badRequest("runId, leaseId, and hostId are required");
|
|
3358
|
+
}
|
|
3359
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3360
|
+
if (!leaseValidation.ok) {
|
|
3361
|
+
return leaseValidation.response;
|
|
3362
|
+
}
|
|
3363
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
3364
|
+
status: "running",
|
|
3365
|
+
startedAt: new Date().toISOString(),
|
|
3366
|
+
hostId,
|
|
3367
|
+
endpointId: leaseId
|
|
3368
|
+
});
|
|
3369
|
+
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
3370
|
+
type: "run.started",
|
|
3371
|
+
runId,
|
|
3372
|
+
taskId: leaseValidation.run.taskId ?? null,
|
|
3373
|
+
timestamp: new Date().toISOString(),
|
|
3374
|
+
agent: leaseValidation.run.runtimeAdapter ?? "remote",
|
|
3375
|
+
summary: "Remote run accepted"
|
|
3376
|
+
});
|
|
3377
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3378
|
+
return deps.jsonResponse({
|
|
3379
|
+
ok: true,
|
|
3380
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3381
|
+
hostId,
|
|
3382
|
+
runId,
|
|
3383
|
+
leaseId,
|
|
3384
|
+
runtimeId: `remote-${runId}`,
|
|
3385
|
+
acceptedAt: new Date().toISOString()
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
if (url.pathname === "/api/remote/runs/log" && req.method === "POST") {
|
|
3389
|
+
const body = await deps.readJsonBody(req);
|
|
3390
|
+
const runId = normalizeString(body.runId);
|
|
3391
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3392
|
+
const hostId = normalizeString(body.hostId);
|
|
3393
|
+
if (!runId || !leaseId || !hostId) {
|
|
3394
|
+
return deps.badRequest("runId, leaseId, and hostId are required");
|
|
3395
|
+
}
|
|
3396
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3397
|
+
if (!leaseValidation.ok) {
|
|
3398
|
+
return leaseValidation.response;
|
|
3399
|
+
}
|
|
3400
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, buildRemoteRunLogEntry(body, { runId, hostId, leaseId }), "remote-run-log");
|
|
3401
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
3402
|
+
updatedAt: new Date().toISOString(),
|
|
3403
|
+
hostId,
|
|
3404
|
+
endpointId: leaseId
|
|
3405
|
+
});
|
|
3406
|
+
return deps.jsonResponse({
|
|
3407
|
+
ok: true,
|
|
3408
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3409
|
+
hostId,
|
|
3410
|
+
runId,
|
|
3411
|
+
leaseId,
|
|
3412
|
+
acceptedAt: new Date().toISOString()
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
if (url.pathname === "/api/remote/runs/message" && req.method === "POST") {
|
|
3416
|
+
const body = await deps.readJsonBody(req);
|
|
3417
|
+
const runId = normalizeString(body.runId);
|
|
3418
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3419
|
+
const hostId = normalizeString(body.hostId);
|
|
3420
|
+
const text = typeof body.text === "string" ? body.text : null;
|
|
3421
|
+
if (!runId || !leaseId || !hostId || text === null) {
|
|
3422
|
+
return deps.badRequest("runId, leaseId, hostId, and text are required");
|
|
3423
|
+
}
|
|
3424
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3425
|
+
if (!leaseValidation.ok) {
|
|
3426
|
+
return leaseValidation.response;
|
|
3427
|
+
}
|
|
3428
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
3429
|
+
id: normalizeString(body.messageId) ?? `message:${runId}:assistant`,
|
|
3430
|
+
type: "assistant_message",
|
|
3431
|
+
text,
|
|
3432
|
+
state: normalizeString(body.state) ?? "completed",
|
|
3433
|
+
createdAt: new Date().toISOString(),
|
|
3434
|
+
completedAt: normalizeString(body.state) === "streaming" ? null : new Date().toISOString()
|
|
3435
|
+
});
|
|
3436
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
3437
|
+
updatedAt: new Date().toISOString(),
|
|
3438
|
+
hostId,
|
|
3439
|
+
endpointId: leaseId,
|
|
3440
|
+
latestMessageId: normalizeString(body.messageId)
|
|
3441
|
+
});
|
|
3442
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3443
|
+
return deps.jsonResponse({
|
|
3444
|
+
ok: true,
|
|
3445
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3446
|
+
hostId,
|
|
3447
|
+
runId,
|
|
3448
|
+
leaseId,
|
|
3449
|
+
acceptedAt: new Date().toISOString()
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
if (url.pathname === "/api/remote/runs/artifact" && req.method === "POST") {
|
|
3453
|
+
const body = await deps.readJsonBody(req);
|
|
3454
|
+
const runId = normalizeString(body.runId);
|
|
3455
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3456
|
+
const hostId = normalizeString(body.hostId);
|
|
3457
|
+
const filename = normalizeString(body.filename);
|
|
3458
|
+
const contentBase64 = normalizeString(body.contentBase64);
|
|
3459
|
+
if (!runId || !leaseId || !hostId || !filename || !contentBase64) {
|
|
3460
|
+
return deps.badRequest("runId, leaseId, hostId, filename, and contentBase64 are required");
|
|
3461
|
+
}
|
|
3462
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3463
|
+
if (!leaseValidation.ok) {
|
|
3464
|
+
return leaseValidation.response;
|
|
3465
|
+
}
|
|
3466
|
+
let artifactPath;
|
|
3467
|
+
try {
|
|
3468
|
+
artifactPath = deps.normalizeRelativePath(remoteArtifactsRoot(state.projectRoot, runId), filename);
|
|
3469
|
+
} catch {
|
|
3470
|
+
return deps.badRequest("Invalid artifact path");
|
|
3471
|
+
}
|
|
3472
|
+
mkdirSync7(dirname6(artifactPath), { recursive: true });
|
|
3473
|
+
const bytes = Buffer.from(contentBase64, "base64");
|
|
3474
|
+
writeFileSync7(artifactPath, bytes);
|
|
3475
|
+
writeJsonFile4(`${artifactPath}.json`, {
|
|
3476
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3477
|
+
runId,
|
|
3478
|
+
hostId,
|
|
3479
|
+
leaseId,
|
|
3480
|
+
kind: normalizeString(body.kind) ?? "artifact",
|
|
3481
|
+
label: normalizeString(body.label) ?? filename,
|
|
3482
|
+
filename,
|
|
3483
|
+
contentType: normalizeString(body.contentType) ?? "application/octet-stream",
|
|
3484
|
+
uploadedAt: new Date().toISOString()
|
|
3485
|
+
});
|
|
3486
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3487
|
+
return deps.jsonResponse({
|
|
3488
|
+
ok: true,
|
|
3489
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3490
|
+
hostId,
|
|
3491
|
+
runId,
|
|
3492
|
+
leaseId,
|
|
3493
|
+
acceptedAt: new Date().toISOString(),
|
|
3494
|
+
artifactPath
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
if (url.pathname === "/api/remote/runs/complete" && req.method === "POST") {
|
|
3498
|
+
const body = await deps.readJsonBody(req);
|
|
3499
|
+
const runId = normalizeString(body.runId);
|
|
3500
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3501
|
+
const hostId = normalizeString(body.hostId);
|
|
3502
|
+
if (!runId || !leaseId || !hostId) {
|
|
3503
|
+
return deps.badRequest("runId, leaseId, and hostId are required");
|
|
3504
|
+
}
|
|
3505
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3506
|
+
if (!leaseValidation.ok) {
|
|
3507
|
+
return leaseValidation.response;
|
|
3508
|
+
}
|
|
3509
|
+
const run = leaseValidation.run;
|
|
3510
|
+
const completedAt = new Date().toISOString();
|
|
3511
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
3512
|
+
status: "completed",
|
|
3513
|
+
completedAt,
|
|
3514
|
+
hostId,
|
|
3515
|
+
endpointId: leaseId
|
|
3516
|
+
});
|
|
3517
|
+
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "completed", completedAt, hostId, endpointId: leaseId }, "closed", "Remote Rig task run completed and closed this task.");
|
|
3518
|
+
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
3519
|
+
type: "run.completed",
|
|
3520
|
+
runId,
|
|
3521
|
+
taskId: run.taskId ?? null,
|
|
3522
|
+
timestamp: new Date().toISOString(),
|
|
3523
|
+
agent: run.runtimeAdapter ?? "remote",
|
|
3524
|
+
summary: "Remote run completed"
|
|
3525
|
+
});
|
|
3526
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3527
|
+
await deps.reconcileScheduler(state, "remote-run-completed");
|
|
3528
|
+
return deps.jsonResponse({
|
|
3529
|
+
ok: true,
|
|
3530
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3531
|
+
hostId,
|
|
3532
|
+
runId,
|
|
3533
|
+
leaseId,
|
|
3534
|
+
acceptedAt: new Date().toISOString()
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
if (url.pathname === "/api/remote/runs/fail" && req.method === "POST") {
|
|
3538
|
+
const body = await deps.readJsonBody(req);
|
|
3539
|
+
const runId = normalizeString(body.runId);
|
|
3540
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3541
|
+
const hostId = normalizeString(body.hostId);
|
|
3542
|
+
if (!runId || !leaseId || !hostId) {
|
|
3543
|
+
return deps.badRequest("runId, leaseId, and hostId are required");
|
|
3544
|
+
}
|
|
3545
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3546
|
+
if (!leaseValidation.ok) {
|
|
3547
|
+
return leaseValidation.response;
|
|
3548
|
+
}
|
|
3549
|
+
const run = leaseValidation.run;
|
|
3550
|
+
const completedAt = new Date().toISOString();
|
|
3551
|
+
const errorText = normalizeString(body.errorText);
|
|
3552
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
3553
|
+
status: "failed",
|
|
3554
|
+
completedAt,
|
|
3555
|
+
hostId,
|
|
3556
|
+
endpointId: leaseId,
|
|
3557
|
+
errorText
|
|
3558
|
+
});
|
|
3559
|
+
await updateRemoteRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "failed", completedAt, hostId, endpointId: leaseId, errorText }, "failed", "Remote Rig task run failed.", errorText);
|
|
3560
|
+
await deps.enqueueRunLinearEvent(state.projectRoot, {
|
|
3561
|
+
type: "run.failed",
|
|
3562
|
+
runId,
|
|
3563
|
+
taskId: run.taskId ?? null,
|
|
3564
|
+
timestamp: new Date().toISOString(),
|
|
3565
|
+
agent: run.runtimeAdapter ?? "remote",
|
|
3566
|
+
summary: errorText ?? "Remote run failed"
|
|
3567
|
+
});
|
|
3568
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
3569
|
+
id: `log:${runId}:remote-fail`,
|
|
3570
|
+
title: "Remote run failed",
|
|
3571
|
+
detail: errorText,
|
|
3572
|
+
tone: "error",
|
|
3573
|
+
status: "failed",
|
|
3574
|
+
createdAt: new Date().toISOString()
|
|
3575
|
+
}, "remote-run-failed");
|
|
3576
|
+
await deps.reconcileScheduler(state, "remote-run-failed");
|
|
3577
|
+
return deps.jsonResponse({
|
|
3578
|
+
ok: true,
|
|
3579
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3580
|
+
hostId,
|
|
3581
|
+
runId,
|
|
3582
|
+
leaseId,
|
|
3583
|
+
acceptedAt: new Date().toISOString()
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
if (url.pathname === "/api/remote/runs/release" && req.method === "POST") {
|
|
3587
|
+
const body = await deps.readJsonBody(req);
|
|
3588
|
+
const runId = normalizeString(body.runId);
|
|
3589
|
+
const leaseId = normalizeString(body.leaseId);
|
|
3590
|
+
const hostId = normalizeString(body.hostId);
|
|
3591
|
+
if (!runId || !leaseId || !hostId) {
|
|
3592
|
+
return deps.badRequest("runId, leaseId, and hostId are required");
|
|
3593
|
+
}
|
|
3594
|
+
const leaseValidation = validateRemoteLease(deps, state, { runId, hostId, leaseId });
|
|
3595
|
+
if (!leaseValidation.ok) {
|
|
3596
|
+
return leaseValidation.response;
|
|
3597
|
+
}
|
|
3598
|
+
const run = leaseValidation.run;
|
|
3599
|
+
if (run.status !== "completed" && run.status !== "failed" && run.status !== "stopped") {
|
|
3600
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
3601
|
+
status: "queued",
|
|
3602
|
+
hostId: null,
|
|
3603
|
+
endpointId: null
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
3607
|
+
await deps.reconcileScheduler(state, "remote-run-released");
|
|
3608
|
+
return deps.jsonResponse({
|
|
3609
|
+
ok: true,
|
|
3610
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
3611
|
+
hostId,
|
|
3612
|
+
runId,
|
|
3613
|
+
leaseId,
|
|
3614
|
+
acceptedAt: new Date().toISOString()
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
if (url.pathname === "/api/remote/runs/artifacts" && req.method === "GET") {
|
|
3618
|
+
const runId = url.searchParams.get("runId");
|
|
3619
|
+
if (!runId) {
|
|
3620
|
+
return deps.badRequest("runId is required");
|
|
3621
|
+
}
|
|
3622
|
+
return deps.jsonResponse({
|
|
3623
|
+
workspaceId: url.searchParams.get("workspaceId") ?? RIG_WORKSPACE_ID,
|
|
3624
|
+
runId,
|
|
3625
|
+
artifacts: deps.listRemoteRunArtifacts(state.projectRoot, runId)
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
if (url.pathname.startsWith("/api/remote/runs/artifacts/") && req.method === "GET") {
|
|
3629
|
+
const [, , , , , runIdEncoded, fileNameEncoded] = url.pathname.split("/");
|
|
3630
|
+
const runId = decodeURIComponent(runIdEncoded ?? "");
|
|
3631
|
+
const fileName = decodeURIComponent(fileNameEncoded ?? "");
|
|
3632
|
+
let artifactPath;
|
|
3633
|
+
try {
|
|
3634
|
+
const runsRoot = resolveAuthorityPaths(state.projectRoot).runsDir;
|
|
3635
|
+
const runRoot = deps.normalizeRelativePath(runsRoot, runId);
|
|
3636
|
+
const artifactsRoot = resolve11(runRoot, "remote-artifacts");
|
|
3637
|
+
artifactPath = deps.normalizeRelativePath(artifactsRoot, fileName);
|
|
3638
|
+
} catch {
|
|
3639
|
+
return deps.badRequest("Invalid artifact path");
|
|
3640
|
+
}
|
|
3641
|
+
if (!existsSync8(artifactPath)) {
|
|
3642
|
+
return deps.notFound();
|
|
3643
|
+
}
|
|
3644
|
+
return new Response(Bun.file(artifactPath));
|
|
3645
|
+
}
|
|
3646
|
+
const runLogsMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/logs$/);
|
|
3647
|
+
if (runLogsMatch) {
|
|
3648
|
+
const runId = decodeURIComponent(runLogsMatch[1]);
|
|
3649
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
3650
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
3651
|
+
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
3652
|
+
return deps.jsonResponse(page);
|
|
3653
|
+
}
|
|
3654
|
+
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
3655
|
+
if (runSteerMatch && req.method === "POST") {
|
|
3656
|
+
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
3657
|
+
const body = await deps.readJsonBody(req);
|
|
3658
|
+
const message = normalizeString(body.message);
|
|
3659
|
+
if (!message) {
|
|
3660
|
+
return deps.badRequest("message is required");
|
|
3661
|
+
}
|
|
3662
|
+
try {
|
|
3663
|
+
const entry = queueRunSteeringMessage(state.projectRoot, runId, {
|
|
3664
|
+
message,
|
|
3665
|
+
actor: normalizeString(body.actor) ?? requestAuth.actor ?? "operator"
|
|
3666
|
+
});
|
|
3667
|
+
deps.broadcastSnapshotInvalidation(state, "run-steering-queued");
|
|
3668
|
+
return deps.jsonResponse({ ok: true, runId, queued: true, message: entry });
|
|
3669
|
+
} catch (error) {
|
|
3670
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
const runSteeringAckMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering\/ack$/);
|
|
3674
|
+
if (runSteeringAckMatch && req.method === "POST") {
|
|
3675
|
+
const runId = decodeURIComponent(runSteeringAckMatch[1]);
|
|
3676
|
+
const body = await deps.readJsonBody(req);
|
|
3677
|
+
const ids = Array.isArray(body.ids) ? body.ids.flatMap((entry) => typeof entry === "string" ? [entry] : []) : [];
|
|
3678
|
+
const delivered = markQueuedRunSteeringMessagesDelivered(state.projectRoot, runId, ids);
|
|
3679
|
+
if (delivered.length > 0)
|
|
3680
|
+
deps.broadcastSnapshotInvalidation(state, "run-steering-delivered");
|
|
3681
|
+
return deps.jsonResponse({ ok: true, runId, delivered });
|
|
3682
|
+
}
|
|
3683
|
+
const runSteeringMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steering$/);
|
|
3684
|
+
if (runSteeringMatch) {
|
|
3685
|
+
const runId = decodeURIComponent(runSteeringMatch[1]);
|
|
3686
|
+
const messages = readQueuedRunSteeringMessages(state.projectRoot, runId).filter((entry) => !entry.delivered);
|
|
3687
|
+
if (url.searchParams.get("ack") === "1" && messages.length > 0) {
|
|
3688
|
+
const delivered = markQueuedRunSteeringMessagesDelivered(state.projectRoot, runId, messages.map((entry) => entry.id));
|
|
3689
|
+
deps.broadcastSnapshotInvalidation(state, "run-steering-delivered");
|
|
3690
|
+
return deps.jsonResponse({ ok: true, runId, messages, delivered });
|
|
3691
|
+
}
|
|
3692
|
+
return deps.jsonResponse({ ok: true, runId, messages });
|
|
3693
|
+
}
|
|
3694
|
+
const runEventMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/events$/);
|
|
3695
|
+
if (runEventMatch && req.method === "POST") {
|
|
3696
|
+
const runId = decodeURIComponent(runEventMatch[1]);
|
|
3697
|
+
const body = await deps.readJsonBody(req);
|
|
3698
|
+
try {
|
|
3699
|
+
appendRunBridgeEvent(state.projectRoot, runId, body);
|
|
3700
|
+
deps.broadcastSnapshotInvalidation(state, "run-bridge-event");
|
|
3701
|
+
return deps.jsonResponse({ ok: true, runId });
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
if (url.pathname.startsWith("/api/runs/")) {
|
|
3707
|
+
const runId = decodeURIComponent(url.pathname.slice("/api/runs/".length));
|
|
3708
|
+
const details = readRunDetails(state.projectRoot, runId);
|
|
3709
|
+
return details ? deps.jsonResponse(details) : deps.notFound();
|
|
3710
|
+
}
|
|
3711
|
+
if (url.pathname === "/api/inbox/approvals") {
|
|
3712
|
+
return deps.jsonResponse(readApprovals(state.projectRoot, {
|
|
3713
|
+
runId: url.searchParams.get("runId"),
|
|
3714
|
+
taskId: url.searchParams.get("taskId")
|
|
3715
|
+
}));
|
|
3716
|
+
}
|
|
3717
|
+
if (url.pathname === "/api/inbox/approvals/resolve" && req.method === "POST") {
|
|
3718
|
+
const body = await deps.readJsonBody(req);
|
|
3719
|
+
const runId = normalizeString(body.runId);
|
|
3720
|
+
const requestId = normalizeString(body.requestId);
|
|
3721
|
+
const decision = normalizeString(body.decision);
|
|
3722
|
+
if (!runId || !requestId || decision !== "approve" && decision !== "reject") {
|
|
3723
|
+
return deps.badRequest("runId, requestId, and decision=approve|reject are required");
|
|
3724
|
+
}
|
|
3725
|
+
return deps.jsonResponse(resolveApproval(state.projectRoot, {
|
|
3726
|
+
runId,
|
|
3727
|
+
requestId,
|
|
3728
|
+
decision,
|
|
3729
|
+
note: normalizeString(body.note)
|
|
3730
|
+
}));
|
|
3731
|
+
}
|
|
3732
|
+
if (url.pathname === "/api/inbox/inputs") {
|
|
3733
|
+
return deps.jsonResponse(readUserInputs(state.projectRoot, {
|
|
3734
|
+
runId: url.searchParams.get("runId"),
|
|
3735
|
+
taskId: url.searchParams.get("taskId")
|
|
3736
|
+
}));
|
|
3737
|
+
}
|
|
3738
|
+
if (url.pathname === "/api/inbox/inputs/respond" && req.method === "POST") {
|
|
3739
|
+
const body = await deps.readJsonBody(req);
|
|
3740
|
+
const runId = normalizeString(body.runId);
|
|
3741
|
+
const requestId = normalizeString(body.requestId);
|
|
3742
|
+
const answers = body.answers && typeof body.answers === "object" ? Object.fromEntries(Object.entries(body.answers).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : [])) : {};
|
|
3743
|
+
if (!runId || !requestId || Object.keys(answers).length === 0) {
|
|
3744
|
+
return deps.badRequest("runId, requestId, and string-valued answers are required");
|
|
3745
|
+
}
|
|
3746
|
+
return deps.jsonResponse(respondToUserInput(state.projectRoot, { runId, requestId, answers }));
|
|
3747
|
+
}
|
|
3748
|
+
if (url.pathname === "/api/linear/status") {
|
|
3749
|
+
return deps.jsonResponse({ removed: true, reason: "linear/beads sync removed" }, 410);
|
|
3750
|
+
}
|
|
3751
|
+
if (url.pathname === "/api/linear/webhook" && req.method === "POST") {
|
|
3752
|
+
const body = await deps.readJsonBody(req);
|
|
3753
|
+
const eventId = normalizeString(body.id) ?? `linear-webhook-${Date.now()}`;
|
|
3754
|
+
await deps.appendLinearInboxEvent(state.projectRoot, {
|
|
3755
|
+
eventId,
|
|
3756
|
+
source: "webhook",
|
|
3757
|
+
event: body
|
|
3758
|
+
});
|
|
3759
|
+
return deps.jsonResponse({ ok: true, eventId });
|
|
3760
|
+
}
|
|
3761
|
+
if (url.pathname === "/api/artifacts/preview") {
|
|
3762
|
+
const taskId = url.searchParams.get("taskId");
|
|
3763
|
+
const fileName = url.searchParams.get("fileName");
|
|
3764
|
+
const maxBytes = Number.parseInt(url.searchParams.get("maxBytes") || "65536", 10);
|
|
3765
|
+
if (!taskId || !fileName) {
|
|
3766
|
+
return deps.badRequest("taskId and fileName are required");
|
|
3767
|
+
}
|
|
3768
|
+
try {
|
|
3769
|
+
return deps.jsonResponse(readTaskArtifactPreview2(state.projectRoot, taskId, fileName, Number.isFinite(maxBytes) ? Math.max(1, maxBytes) : 64 * 1024));
|
|
3770
|
+
} catch (error) {
|
|
3771
|
+
return deps.jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, 404);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return deps.notFound();
|
|
3775
|
+
})());
|
|
3776
|
+
});
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
export {
|
|
3780
|
+
createRigServerFetch
|
|
3781
|
+
};
|