@wopr-network/defcon 1.0.3 → 1.1.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/dist/src/api/server.js +76 -0
- package/dist/src/api/wire-types.d.ts +1 -1
- package/dist/src/api/wire-types.js +1 -1
- package/dist/src/engine/engine.d.ts +5 -0
- package/dist/src/engine/engine.js +19 -0
- package/dist/src/execution/admin-schemas.d.ts +17 -0
- package/dist/src/execution/admin-schemas.js +17 -0
- package/dist/src/execution/mcp-server.js +158 -1
- package/dist/src/repositories/drizzle/entity.repo.d.ts +2 -0
- package/dist/src/repositories/drizzle/entity.repo.js +17 -0
- package/dist/src/repositories/drizzle/flow.repo.js +4 -0
- package/dist/src/repositories/drizzle/gate.repo.d.ts +1 -0
- package/dist/src/repositories/drizzle/gate.repo.js +7 -1
- package/dist/src/repositories/drizzle/schema.d.ts +17 -0
- package/dist/src/repositories/drizzle/schema.js +1 -0
- package/dist/src/repositories/interfaces.d.ts +8 -0
- package/drizzle/0011_modern_anita_blake.sql +19 -0
- package/drizzle/meta/0011_snapshot.json +1072 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +1 -1
package/dist/src/api/server.js
CHANGED
|
@@ -11,6 +11,21 @@ function extractBearerToken(header) {
|
|
|
11
11
|
return undefined;
|
|
12
12
|
return header.slice(7).trim() || undefined;
|
|
13
13
|
}
|
|
14
|
+
function requireAdminToken(deps, req) {
|
|
15
|
+
const configuredToken = deps.adminToken?.trim() || undefined;
|
|
16
|
+
if (!configuredToken)
|
|
17
|
+
return null; // open mode
|
|
18
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
19
|
+
if (!callerToken) {
|
|
20
|
+
return { status: 401, body: { error: "Unauthorized: admin endpoints require authentication." } };
|
|
21
|
+
}
|
|
22
|
+
const hashA = createHash("sha256").update(configuredToken.trim()).digest();
|
|
23
|
+
const hashB = createHash("sha256").update(callerToken.trim()).digest();
|
|
24
|
+
if (!timingSafeEqual(hashA, hashB)) {
|
|
25
|
+
return { status: 401, body: { error: "Unauthorized: admin endpoints require authentication." } };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
14
29
|
function requireWorkerToken(deps, req) {
|
|
15
30
|
const configuredToken = deps.workerToken?.trim() || undefined; // treat "" and whitespace-only as unset
|
|
16
31
|
if (!configuredToken)
|
|
@@ -198,6 +213,67 @@ export function createHttpServer(deps) {
|
|
|
198
213
|
router.add("DELETE", "/api/flows/:id", async () => {
|
|
199
214
|
return { status: 501, body: { error: "Flow deletion not implemented" } };
|
|
200
215
|
});
|
|
216
|
+
// --- Admin: Pause/Resume Flow ---
|
|
217
|
+
router.add("POST", "/api/admin/flows/:flow/pause", async (req) => {
|
|
218
|
+
const authErr = requireAdminToken(deps, req);
|
|
219
|
+
if (authErr)
|
|
220
|
+
return authErr;
|
|
221
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
222
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.flow.pause", { flow_name: req.params.flow }, { adminToken: deps.adminToken, callerToken });
|
|
223
|
+
return mcpResultToApi(result);
|
|
224
|
+
});
|
|
225
|
+
router.add("POST", "/api/admin/flows/:flow/resume", async (req) => {
|
|
226
|
+
const authErr = requireAdminToken(deps, req);
|
|
227
|
+
if (authErr)
|
|
228
|
+
return authErr;
|
|
229
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
230
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.flow.resume", { flow_name: req.params.flow }, { adminToken: deps.adminToken, callerToken });
|
|
231
|
+
return mcpResultToApi(result);
|
|
232
|
+
});
|
|
233
|
+
// --- Admin: Cancel Entity ---
|
|
234
|
+
router.add("POST", "/api/admin/entities/:id/cancel", async (req) => {
|
|
235
|
+
const authErr = requireAdminToken(deps, req);
|
|
236
|
+
if (authErr)
|
|
237
|
+
return authErr;
|
|
238
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
239
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.entity.cancel", { entity_id: req.params.id }, { adminToken: deps.adminToken, callerToken });
|
|
240
|
+
return mcpResultToApi(result);
|
|
241
|
+
});
|
|
242
|
+
// --- Admin: Reset Entity ---
|
|
243
|
+
router.add("POST", "/api/admin/entities/:id/reset", async (req) => {
|
|
244
|
+
const authErr = requireAdminToken(deps, req);
|
|
245
|
+
if (authErr)
|
|
246
|
+
return authErr;
|
|
247
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
248
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.entity.reset", { entity_id: req.params.id, target_state: req.body?.target_state }, { adminToken: deps.adminToken, callerToken });
|
|
249
|
+
return mcpResultToApi(result);
|
|
250
|
+
});
|
|
251
|
+
// --- Admin: Drain/Undrain Worker ---
|
|
252
|
+
router.add("POST", "/api/admin/workers/:workerId/drain", async (req) => {
|
|
253
|
+
const authErr = requireAdminToken(deps, req);
|
|
254
|
+
if (authErr)
|
|
255
|
+
return authErr;
|
|
256
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
257
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.worker.drain", { worker_id: req.params.workerId }, { adminToken: deps.adminToken, callerToken });
|
|
258
|
+
return mcpResultToApi(result);
|
|
259
|
+
});
|
|
260
|
+
router.add("POST", "/api/admin/workers/:workerId/undrain", async (req) => {
|
|
261
|
+
const authErr = requireAdminToken(deps, req);
|
|
262
|
+
if (authErr)
|
|
263
|
+
return authErr;
|
|
264
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
265
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.worker.undrain", { worker_id: req.params.workerId }, { adminToken: deps.adminToken, callerToken });
|
|
266
|
+
return mcpResultToApi(result);
|
|
267
|
+
});
|
|
268
|
+
// --- Admin: Rerun Gate ---
|
|
269
|
+
router.add("POST", "/api/admin/entities/:id/gates/:gateName/rerun", async (req) => {
|
|
270
|
+
const authErr = requireAdminToken(deps, req);
|
|
271
|
+
if (authErr)
|
|
272
|
+
return authErr;
|
|
273
|
+
const callerToken = extractBearerToken(req.authorization);
|
|
274
|
+
const result = await callToolHandler(deps.mcpDeps, "admin.gate.rerun", { entity_id: req.params.id, gate_name: req.params.gateName }, { adminToken: deps.adminToken, callerToken });
|
|
275
|
+
return mcpResultToApi(result);
|
|
276
|
+
});
|
|
201
277
|
// --- HTTP server ---
|
|
202
278
|
const server = http.createServer(async (req, res) => {
|
|
203
279
|
// CORS
|
|
@@ -48,7 +48,12 @@ export declare class Engine {
|
|
|
48
48
|
readonly adapters: Map<string, unknown>;
|
|
49
49
|
private eventEmitter;
|
|
50
50
|
private readonly logger;
|
|
51
|
+
private drainingWorkers;
|
|
51
52
|
constructor(deps: EngineDeps);
|
|
53
|
+
drainWorker(workerId: string): void;
|
|
54
|
+
undrainWorker(workerId: string): void;
|
|
55
|
+
isDraining(workerId: string): boolean;
|
|
56
|
+
listDrainingWorkers(): string[];
|
|
52
57
|
processSignal(entityId: string, signal: string, artifacts?: Artifacts, triggeringInvocationId?: string): Promise<ProcessSignalResult>;
|
|
53
58
|
createEntity(flowName: string, refs?: Record<string, {
|
|
54
59
|
adapter: string;
|
|
@@ -16,6 +16,7 @@ export class Engine {
|
|
|
16
16
|
adapters;
|
|
17
17
|
eventEmitter;
|
|
18
18
|
logger;
|
|
19
|
+
drainingWorkers = new Set();
|
|
19
20
|
constructor(deps) {
|
|
20
21
|
this.entityRepo = deps.entityRepo;
|
|
21
22
|
this.flowRepo = deps.flowRepo;
|
|
@@ -26,6 +27,18 @@ export class Engine {
|
|
|
26
27
|
this.eventEmitter = deps.eventEmitter;
|
|
27
28
|
this.logger = deps.logger ?? consoleLogger;
|
|
28
29
|
}
|
|
30
|
+
drainWorker(workerId) {
|
|
31
|
+
this.drainingWorkers.add(workerId);
|
|
32
|
+
}
|
|
33
|
+
undrainWorker(workerId) {
|
|
34
|
+
this.drainingWorkers.delete(workerId);
|
|
35
|
+
}
|
|
36
|
+
isDraining(workerId) {
|
|
37
|
+
return this.drainingWorkers.has(workerId);
|
|
38
|
+
}
|
|
39
|
+
listDrainingWorkers() {
|
|
40
|
+
return Array.from(this.drainingWorkers);
|
|
41
|
+
}
|
|
29
42
|
async processSignal(entityId, signal, artifacts, triggeringInvocationId) {
|
|
30
43
|
// 1. Load entity
|
|
31
44
|
const entity = await this.entityRepo.get(entityId);
|
|
@@ -304,6 +317,10 @@ export class Engine {
|
|
|
304
317
|
return entity;
|
|
305
318
|
}
|
|
306
319
|
async claimWork(role, flowName, worker_id) {
|
|
320
|
+
// Skip draining workers entirely
|
|
321
|
+
if (worker_id && this.drainingWorkers.has(worker_id)) {
|
|
322
|
+
return "all_claimed";
|
|
323
|
+
}
|
|
307
324
|
// 1. Find candidate flows filtered by discipline
|
|
308
325
|
let flows;
|
|
309
326
|
if (flowName) {
|
|
@@ -316,6 +333,8 @@ export class Engine {
|
|
|
316
333
|
const allFlows = await this.flowRepo.listAll();
|
|
317
334
|
flows = allFlows.filter((f) => f.discipline === null || f.discipline === role);
|
|
318
335
|
}
|
|
336
|
+
// Filter out paused flows
|
|
337
|
+
flows = flows.filter((f) => !f.paused);
|
|
319
338
|
if (flows.length === 0)
|
|
320
339
|
return null;
|
|
321
340
|
const candidates = [];
|
|
@@ -114,3 +114,20 @@ export declare const AdminFlowRestoreSchema: z.ZodObject<{
|
|
|
114
114
|
flow_name: z.ZodString;
|
|
115
115
|
version: z.ZodNumber;
|
|
116
116
|
}, z.core.$strip>;
|
|
117
|
+
export declare const AdminFlowPauseSchema: z.ZodObject<{
|
|
118
|
+
flow_name: z.ZodString;
|
|
119
|
+
}, z.core.$strip>;
|
|
120
|
+
export declare const AdminEntityCancelSchema: z.ZodObject<{
|
|
121
|
+
entity_id: z.ZodString;
|
|
122
|
+
}, z.core.$strip>;
|
|
123
|
+
export declare const AdminEntityResetSchema: z.ZodObject<{
|
|
124
|
+
entity_id: z.ZodString;
|
|
125
|
+
target_state: z.ZodString;
|
|
126
|
+
}, z.core.$strip>;
|
|
127
|
+
export declare const AdminWorkerDrainSchema: z.ZodObject<{
|
|
128
|
+
worker_id: z.ZodString;
|
|
129
|
+
}, z.core.$strip>;
|
|
130
|
+
export declare const AdminGateRerunSchema: z.ZodObject<{
|
|
131
|
+
entity_id: z.ZodString;
|
|
132
|
+
gate_name: z.ZodString;
|
|
133
|
+
}, z.core.$strip>;
|
|
@@ -123,3 +123,20 @@ export const AdminFlowRestoreSchema = z.object({
|
|
|
123
123
|
flow_name: z.string().min(1),
|
|
124
124
|
version: z.number().int().min(1),
|
|
125
125
|
});
|
|
126
|
+
export const AdminFlowPauseSchema = z.object({
|
|
127
|
+
flow_name: z.string().min(1),
|
|
128
|
+
});
|
|
129
|
+
export const AdminEntityCancelSchema = z.object({
|
|
130
|
+
entity_id: z.string().min(1),
|
|
131
|
+
});
|
|
132
|
+
export const AdminEntityResetSchema = z.object({
|
|
133
|
+
entity_id: z.string().min(1),
|
|
134
|
+
target_state: z.string().min(1),
|
|
135
|
+
});
|
|
136
|
+
export const AdminWorkerDrainSchema = z.object({
|
|
137
|
+
worker_id: z.string().min(1),
|
|
138
|
+
});
|
|
139
|
+
export const AdminGateRerunSchema = z.object({
|
|
140
|
+
entity_id: z.string().min(1),
|
|
141
|
+
gate_name: z.string().min(1),
|
|
142
|
+
});
|
|
@@ -6,7 +6,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
6
6
|
import { DEFAULT_TIMEOUT_PROMPT } from "../engine/constants.js";
|
|
7
7
|
import { isTerminal } from "../engine/state-machine.js";
|
|
8
8
|
import { consoleLogger } from "../logger.js";
|
|
9
|
-
import { AdminFlowCreateSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, } from "./admin-schemas.js";
|
|
9
|
+
import { AdminEntityCancelSchema, AdminEntityResetSchema, AdminFlowCreateSchema, AdminFlowPauseSchema, AdminFlowRestoreSchema, AdminFlowSnapshotSchema, AdminFlowUpdateSchema, AdminGateAttachSchema, AdminGateCreateSchema, AdminGateRerunSchema, AdminStateCreateSchema, AdminStateUpdateSchema, AdminTransitionCreateSchema, AdminTransitionUpdateSchema, AdminWorkerDrainSchema, } from "./admin-schemas.js";
|
|
10
10
|
import { FlowClaimSchema, FlowFailSchema, FlowGetPromptSchema, FlowReportSchema, FlowSeedSchema, QueryEntitiesSchema, QueryEntitySchema, QueryFlowSchema, QueryInvocationsSchema, } from "./tool-schemas.js";
|
|
11
11
|
function getSystemDefaultGateTimeoutMs() {
|
|
12
12
|
const parsed = parseInt(process.env.DEFCON_DEFAULT_GATE_TIMEOUT_MS ?? "", 10);
|
|
@@ -337,6 +337,49 @@ const TOOL_DEFINITIONS = [
|
|
|
337
337
|
required: ["flow"],
|
|
338
338
|
},
|
|
339
339
|
},
|
|
340
|
+
{
|
|
341
|
+
name: "admin.flow.pause",
|
|
342
|
+
description: "Pause a flow — claimWork() will skip paused flows until resumed.",
|
|
343
|
+
inputSchema: { type: "object", properties: { flow_name: { type: "string" } }, required: ["flow_name"] },
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "admin.flow.resume",
|
|
347
|
+
description: "Resume a previously paused flow.",
|
|
348
|
+
inputSchema: { type: "object", properties: { flow_name: { type: "string" } }, required: ["flow_name"] },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "admin.entity.cancel",
|
|
352
|
+
description: "Cancel an entity — fails active invocation, moves to 'cancelled' terminal state.",
|
|
353
|
+
inputSchema: { type: "object", properties: { entity_id: { type: "string" } }, required: ["entity_id"] },
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "admin.entity.reset",
|
|
357
|
+
description: "Reset an entity to a target state — clears claimedBy.",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: { entity_id: { type: "string" }, target_state: { type: "string" } },
|
|
361
|
+
required: ["entity_id", "target_state"],
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "admin.worker.drain",
|
|
366
|
+
description: "Mark a worker as draining — claimWork() will skip it.",
|
|
367
|
+
inputSchema: { type: "object", properties: { worker_id: { type: "string" } }, required: ["worker_id"] },
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: "admin.worker.undrain",
|
|
371
|
+
description: "Remove draining flag from a worker.",
|
|
372
|
+
inputSchema: { type: "object", properties: { worker_id: { type: "string" } }, required: ["worker_id"] },
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "admin.gate.rerun",
|
|
376
|
+
description: "Clear a gate result for an entity and release it so the gate can be re-evaluated.",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: "object",
|
|
379
|
+
properties: { entity_id: { type: "string" }, gate_name: { type: "string" } },
|
|
380
|
+
required: ["entity_id", "gate_name"],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
340
383
|
];
|
|
341
384
|
export function createMcpServer(deps, opts) {
|
|
342
385
|
const server = new Server({ name: "defcon", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
@@ -413,6 +456,20 @@ export async function callToolHandler(deps, name, safeArgs, opts) {
|
|
|
413
456
|
return await handleAdminFlowRestore(deps, safeArgs);
|
|
414
457
|
case "admin.entity.create":
|
|
415
458
|
return await handleAdminEntityCreate(deps, safeArgs);
|
|
459
|
+
case "admin.flow.pause":
|
|
460
|
+
return await handleAdminFlowPause(deps, safeArgs);
|
|
461
|
+
case "admin.flow.resume":
|
|
462
|
+
return await handleAdminFlowResume(deps, safeArgs);
|
|
463
|
+
case "admin.entity.cancel":
|
|
464
|
+
return await handleAdminEntityCancel(deps, safeArgs);
|
|
465
|
+
case "admin.entity.reset":
|
|
466
|
+
return await handleAdminEntityReset(deps, safeArgs);
|
|
467
|
+
case "admin.worker.drain":
|
|
468
|
+
return await handleAdminWorkerDrain(deps, safeArgs);
|
|
469
|
+
case "admin.worker.undrain":
|
|
470
|
+
return await handleAdminWorkerUndrain(deps, safeArgs);
|
|
471
|
+
case "admin.gate.rerun":
|
|
472
|
+
return await handleAdminGateRerun(deps, safeArgs);
|
|
416
473
|
default:
|
|
417
474
|
return errorResult(`Unknown tool: ${name}`);
|
|
418
475
|
}
|
|
@@ -707,6 +764,106 @@ async function handleAdminEntityCreate(deps, args) {
|
|
|
707
764
|
}
|
|
708
765
|
return jsonResult(result);
|
|
709
766
|
}
|
|
767
|
+
async function handleAdminFlowPause(deps, args) {
|
|
768
|
+
const v = validateInput(AdminFlowPauseSchema, args);
|
|
769
|
+
if (!v.ok)
|
|
770
|
+
return v.result;
|
|
771
|
+
const flow = await deps.flows.getByName(v.data.flow_name);
|
|
772
|
+
if (!flow)
|
|
773
|
+
return errorResult(`Flow not found: ${v.data.flow_name}`);
|
|
774
|
+
await deps.flows.update(flow.id, { paused: true });
|
|
775
|
+
emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.pause", { name: v.data.flow_name });
|
|
776
|
+
return jsonResult({ paused: true, flow: v.data.flow_name });
|
|
777
|
+
}
|
|
778
|
+
async function handleAdminFlowResume(deps, args) {
|
|
779
|
+
const v = validateInput(AdminFlowPauseSchema, args);
|
|
780
|
+
if (!v.ok)
|
|
781
|
+
return v.result;
|
|
782
|
+
const flow = await deps.flows.getByName(v.data.flow_name);
|
|
783
|
+
if (!flow)
|
|
784
|
+
return errorResult(`Flow not found: ${v.data.flow_name}`);
|
|
785
|
+
await deps.flows.update(flow.id, { paused: false });
|
|
786
|
+
emitDefinitionChanged(deps.eventRepo, flow.id, "admin.flow.resume", { name: v.data.flow_name });
|
|
787
|
+
return jsonResult({ paused: false, flow: v.data.flow_name });
|
|
788
|
+
}
|
|
789
|
+
async function handleAdminEntityCancel(deps, args) {
|
|
790
|
+
const v = validateInput(AdminEntityCancelSchema, args);
|
|
791
|
+
if (!v.ok)
|
|
792
|
+
return v.result;
|
|
793
|
+
const entity = await deps.entities.get(v.data.entity_id);
|
|
794
|
+
if (!entity)
|
|
795
|
+
return errorResult(`Entity not found: ${v.data.entity_id}`);
|
|
796
|
+
const invocations = await deps.invocations.findByEntity(v.data.entity_id);
|
|
797
|
+
for (const inv of invocations) {
|
|
798
|
+
if (inv.completedAt === null && inv.failedAt === null) {
|
|
799
|
+
await deps.invocations.fail(inv.id, "Cancelled by admin");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
await deps.entities.cancelEntity(v.data.entity_id);
|
|
803
|
+
return jsonResult({ cancelled: true, entity_id: v.data.entity_id });
|
|
804
|
+
}
|
|
805
|
+
async function handleAdminEntityReset(deps, args) {
|
|
806
|
+
const v = validateInput(AdminEntityResetSchema, args);
|
|
807
|
+
if (!v.ok)
|
|
808
|
+
return v.result;
|
|
809
|
+
const entity = await deps.entities.get(v.data.entity_id);
|
|
810
|
+
if (!entity)
|
|
811
|
+
return errorResult(`Entity not found: ${v.data.entity_id}`);
|
|
812
|
+
const flow = await deps.flows.get(entity.flowId);
|
|
813
|
+
if (!flow)
|
|
814
|
+
return errorResult(`Flow not found for entity: ${v.data.entity_id}`);
|
|
815
|
+
const targetState = flow.states.find((s) => s.name === v.data.target_state);
|
|
816
|
+
if (!targetState)
|
|
817
|
+
return errorResult(`State '${v.data.target_state}' not found in flow '${flow.name}'`);
|
|
818
|
+
const invocations = await deps.invocations.findByEntity(v.data.entity_id);
|
|
819
|
+
for (const inv of invocations) {
|
|
820
|
+
if (inv.completedAt === null && inv.failedAt === null) {
|
|
821
|
+
await deps.invocations.fail(inv.id, "Reset by admin");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const updated = await deps.entities.resetEntity(v.data.entity_id, v.data.target_state);
|
|
825
|
+
return jsonResult({ reset: true, entity_id: v.data.entity_id, state: updated.state });
|
|
826
|
+
}
|
|
827
|
+
async function handleAdminWorkerDrain(deps, args) {
|
|
828
|
+
const v = validateInput(AdminWorkerDrainSchema, args);
|
|
829
|
+
if (!v.ok)
|
|
830
|
+
return v.result;
|
|
831
|
+
if (!deps.engine)
|
|
832
|
+
return errorResult("Engine not available");
|
|
833
|
+
deps.engine.drainWorker(v.data.worker_id);
|
|
834
|
+
return jsonResult({ draining: true, worker_id: v.data.worker_id });
|
|
835
|
+
}
|
|
836
|
+
async function handleAdminWorkerUndrain(deps, args) {
|
|
837
|
+
const v = validateInput(AdminWorkerDrainSchema, args);
|
|
838
|
+
if (!v.ok)
|
|
839
|
+
return v.result;
|
|
840
|
+
if (!deps.engine)
|
|
841
|
+
return errorResult("Engine not available");
|
|
842
|
+
deps.engine.undrainWorker(v.data.worker_id);
|
|
843
|
+
return jsonResult({ draining: false, worker_id: v.data.worker_id });
|
|
844
|
+
}
|
|
845
|
+
async function handleAdminGateRerun(deps, args) {
|
|
846
|
+
const v = validateInput(AdminGateRerunSchema, args);
|
|
847
|
+
if (!v.ok)
|
|
848
|
+
return v.result;
|
|
849
|
+
const entity = await deps.entities.get(v.data.entity_id);
|
|
850
|
+
if (!entity)
|
|
851
|
+
return errorResult(`Entity not found: ${v.data.entity_id}`);
|
|
852
|
+
const gate = await deps.gates.getByName(v.data.gate_name);
|
|
853
|
+
if (!gate)
|
|
854
|
+
return errorResult(`Gate not found: ${v.data.gate_name}`);
|
|
855
|
+
await deps.gates.clearResult(v.data.entity_id, gate.id);
|
|
856
|
+
const invocations = await deps.invocations.findByEntity(v.data.entity_id);
|
|
857
|
+
for (const inv of invocations) {
|
|
858
|
+
if (inv.completedAt === null && inv.failedAt === null) {
|
|
859
|
+
await deps.invocations.fail(inv.id, "Gate rerun by admin");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (entity.claimedBy) {
|
|
863
|
+
await deps.entities.release(entity.id, entity.claimedBy);
|
|
864
|
+
}
|
|
865
|
+
return jsonResult({ rerun: true, entity_id: v.data.entity_id, gate: v.data.gate_name });
|
|
866
|
+
}
|
|
710
867
|
/** Start the MCP server on stdio transport. */
|
|
711
868
|
export async function startStdioServer(deps, opts) {
|
|
712
869
|
const server = createMcpServer(deps, opts);
|
|
@@ -23,5 +23,7 @@ export declare class DrizzleEntityRepository implements IEntityRepository {
|
|
|
23
23
|
reapExpired(ttlMs: number): Promise<string[]>;
|
|
24
24
|
setAffinity(entityId: string, workerId: string, role: string, expiresAt: Date): Promise<void>;
|
|
25
25
|
clearExpiredAffinity(): Promise<string[]>;
|
|
26
|
+
cancelEntity(entityId: string): Promise<void>;
|
|
27
|
+
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
26
28
|
}
|
|
27
29
|
export {};
|
|
@@ -188,4 +188,21 @@ export class DrizzleEntityRepository {
|
|
|
188
188
|
.all();
|
|
189
189
|
return rows.map((r) => r.id);
|
|
190
190
|
}
|
|
191
|
+
async cancelEntity(entityId) {
|
|
192
|
+
await this.db
|
|
193
|
+
.update(entities)
|
|
194
|
+
.set({ state: "cancelled", claimedBy: null, claimedAt: null, updatedAt: Date.now() })
|
|
195
|
+
.where(eq(entities.id, entityId));
|
|
196
|
+
}
|
|
197
|
+
async resetEntity(entityId, targetState) {
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
await this.db
|
|
200
|
+
.update(entities)
|
|
201
|
+
.set({ state: targetState, claimedBy: null, claimedAt: null, updatedAt: now })
|
|
202
|
+
.where(eq(entities.id, entityId));
|
|
203
|
+
const rows = await this.db.select().from(entities).where(eq(entities.id, entityId)).limit(1);
|
|
204
|
+
if (rows.length === 0)
|
|
205
|
+
throw new NotFoundError(`Entity not found: ${entityId}`);
|
|
206
|
+
return this.toEntity(rows[0]);
|
|
207
|
+
}
|
|
191
208
|
}
|
|
@@ -47,6 +47,7 @@ function rowToFlow(r, states, transitions) {
|
|
|
47
47
|
discipline: r.discipline ?? null,
|
|
48
48
|
defaultModelTier: r.defaultModelTier ?? null,
|
|
49
49
|
timeoutPrompt: r.timeoutPrompt ?? null,
|
|
50
|
+
paused: !!r.paused,
|
|
50
51
|
createdAt: toDate(r.createdAt),
|
|
51
52
|
updatedAt: toDate(r.updatedAt),
|
|
52
53
|
states,
|
|
@@ -91,6 +92,7 @@ export class DrizzleFlowRepository {
|
|
|
91
92
|
discipline: input.discipline ?? null,
|
|
92
93
|
defaultModelTier: input.defaultModelTier ?? null,
|
|
93
94
|
timeoutPrompt: input.timeoutPrompt ?? null,
|
|
95
|
+
paused: input.paused ? 1 : 0,
|
|
94
96
|
createdAt: now,
|
|
95
97
|
updatedAt: now,
|
|
96
98
|
};
|
|
@@ -148,6 +150,8 @@ export class DrizzleFlowRepository {
|
|
|
148
150
|
updateValues.defaultModelTier = changes.defaultModelTier;
|
|
149
151
|
if (changes.timeoutPrompt !== undefined)
|
|
150
152
|
updateValues.timeoutPrompt = changes.timeoutPrompt;
|
|
153
|
+
if (changes.paused !== undefined)
|
|
154
|
+
updateValues.paused = changes.paused ? 1 : 0;
|
|
151
155
|
this.db.update(flowDefinitions).set(updateValues).where(eq(flowDefinitions.id, id)).run();
|
|
152
156
|
const updated = this.db.select().from(flowDefinitions).where(eq(flowDefinitions.id, id)).all();
|
|
153
157
|
return this.hydrateFlow(updated[0]);
|
|
@@ -12,5 +12,6 @@ export declare class DrizzleGateRepository implements IGateRepository {
|
|
|
12
12
|
record(entityId: string, gateId: string, passed: boolean, output: string): Promise<GateResult>;
|
|
13
13
|
update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt">>): Promise<Gate>;
|
|
14
14
|
resultsFor(entityId: string): Promise<GateResult[]>;
|
|
15
|
+
clearResult(entityId: string, gateId: string): Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { asc, eq, sql } from "drizzle-orm";
|
|
2
|
+
import { and, asc, eq, sql } from "drizzle-orm";
|
|
3
3
|
import { InternalError } from "../../errors.js";
|
|
4
4
|
import { gateDefinitions, gateResults } from "./schema.js";
|
|
5
5
|
function toGate(row) {
|
|
@@ -96,4 +96,10 @@ export class DrizzleGateRepository {
|
|
|
96
96
|
.all();
|
|
97
97
|
return rows.map(toGateResult);
|
|
98
98
|
}
|
|
99
|
+
async clearResult(entityId, gateId) {
|
|
100
|
+
this.db
|
|
101
|
+
.delete(gateResults)
|
|
102
|
+
.where(and(eq(gateResults.entityId, entityId), eq(gateResults.gateId, gateId)))
|
|
103
|
+
.run();
|
|
104
|
+
}
|
|
99
105
|
}
|
|
@@ -256,6 +256,23 @@ export declare const flowDefinitions: import("drizzle-orm/sqlite-core").SQLiteTa
|
|
|
256
256
|
}, {}, {
|
|
257
257
|
length: number | undefined;
|
|
258
258
|
}>;
|
|
259
|
+
paused: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
260
|
+
name: "paused";
|
|
261
|
+
tableName: "flow_definitions";
|
|
262
|
+
dataType: "number";
|
|
263
|
+
columnType: "SQLiteInteger";
|
|
264
|
+
data: number;
|
|
265
|
+
driverParam: number;
|
|
266
|
+
notNull: false;
|
|
267
|
+
hasDefault: true;
|
|
268
|
+
isPrimaryKey: false;
|
|
269
|
+
isAutoincrement: false;
|
|
270
|
+
hasRuntimeDefault: false;
|
|
271
|
+
enumValues: undefined;
|
|
272
|
+
baseColumn: never;
|
|
273
|
+
identity: undefined;
|
|
274
|
+
generated: undefined;
|
|
275
|
+
}, {}, {}>;
|
|
259
276
|
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
260
277
|
name: "created_at";
|
|
261
278
|
tableName: "flow_definitions";
|
|
@@ -15,6 +15,7 @@ export const flowDefinitions = sqliteTable("flow_definitions", {
|
|
|
15
15
|
discipline: text("discipline"),
|
|
16
16
|
defaultModelTier: text("default_model_tier"),
|
|
17
17
|
timeoutPrompt: text("timeout_prompt"),
|
|
18
|
+
paused: integer("paused").default(0),
|
|
18
19
|
createdAt: integer("created_at"),
|
|
19
20
|
updatedAt: integer("updated_at"),
|
|
20
21
|
});
|
|
@@ -130,6 +130,7 @@ export interface Flow {
|
|
|
130
130
|
discipline: string | null;
|
|
131
131
|
defaultModelTier: string | null;
|
|
132
132
|
timeoutPrompt: string | null;
|
|
133
|
+
paused: boolean;
|
|
133
134
|
createdAt: Date | null;
|
|
134
135
|
updatedAt: Date | null;
|
|
135
136
|
states: State[];
|
|
@@ -159,6 +160,7 @@ export interface CreateFlowInput {
|
|
|
159
160
|
discipline?: string;
|
|
160
161
|
defaultModelTier?: string;
|
|
161
162
|
timeoutPrompt?: string;
|
|
163
|
+
paused?: boolean;
|
|
162
164
|
}
|
|
163
165
|
/** Input for adding a state to a flow */
|
|
164
166
|
export interface CreateStateInput {
|
|
@@ -225,6 +227,10 @@ export interface IEntityRepository {
|
|
|
225
227
|
childFlow: string;
|
|
226
228
|
spawnedAt: string;
|
|
227
229
|
}): Promise<void>;
|
|
230
|
+
/** Move entity to 'cancelled' terminal state and clear claimedBy/claimedAt. */
|
|
231
|
+
cancelEntity(entityId: string): Promise<void>;
|
|
232
|
+
/** Move entity to targetState and clear claimedBy/claimedAt. Returns the updated entity. */
|
|
233
|
+
resetEntity(entityId: string, targetState: string): Promise<Entity>;
|
|
228
234
|
}
|
|
229
235
|
/** Fields that can be updated on a flow's top-level definition */
|
|
230
236
|
export type UpdateFlowInput = Partial<Omit<Flow, "id" | "states" | "transitions" | "createdAt" | "updatedAt">>;
|
|
@@ -318,4 +324,6 @@ export interface IGateRepository {
|
|
|
318
324
|
resultsFor(entityId: string): Promise<GateResult[]>;
|
|
319
325
|
/** Update mutable fields on a gate definition. */
|
|
320
326
|
update(id: string, changes: Partial<Pick<Gate, "command" | "functionRef" | "apiConfig" | "timeoutMs" | "failurePrompt" | "timeoutPrompt">>): Promise<Gate>;
|
|
327
|
+
/** Delete the gate result for a specific entity+gate combination. */
|
|
328
|
+
clearResult(entityId: string, gateId: string): Promise<void>;
|
|
321
329
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
2
|
+
CREATE TABLE `__new_gate_definitions` (
|
|
3
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
4
|
+
`name` text NOT NULL,
|
|
5
|
+
`type` text NOT NULL,
|
|
6
|
+
`command` text,
|
|
7
|
+
`function_ref` text,
|
|
8
|
+
`api_config` text,
|
|
9
|
+
`timeout_ms` integer,
|
|
10
|
+
`failure_prompt` text,
|
|
11
|
+
`timeout_prompt` text
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
INSERT INTO `__new_gate_definitions`("id", "name", "type", "command", "function_ref", "api_config", "timeout_ms", "failure_prompt", "timeout_prompt") SELECT "id", "name", "type", "command", "function_ref", "api_config", "timeout_ms", "failure_prompt", "timeout_prompt" FROM `gate_definitions`;--> statement-breakpoint
|
|
15
|
+
DROP TABLE `gate_definitions`;--> statement-breakpoint
|
|
16
|
+
ALTER TABLE `__new_gate_definitions` RENAME TO `gate_definitions`;--> statement-breakpoint
|
|
17
|
+
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
|
18
|
+
CREATE UNIQUE INDEX `gate_definitions_name_unique` ON `gate_definitions` (`name`);--> statement-breakpoint
|
|
19
|
+
ALTER TABLE `flow_definitions` ADD `paused` integer DEFAULT 0;
|