@app-vitals/agent-admin 0.3.0 → 0.4.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/openapi.platform.json +101 -0
- package/package.json +2 -2
- package/prisma/client/browser.ts +5 -0
- package/prisma/client/client.ts +5 -0
- package/prisma/client/internal/class.ts +14 -4
- package/prisma/client/internal/prismaNamespace.ts +93 -1
- package/prisma/client/internal/prismaNamespaceBrowser.ts +17 -0
- package/prisma/client/models/AgentCronJob.ts +107 -1
- package/prisma/client/models/AgentCronRunRequest.ts +1233 -0
- package/prisma/client/models.ts +1 -0
- package/prisma/migrations/20261028000000_add_cron_run_now/migration.sql +24 -0
- package/prisma/schema/schema.prisma +24 -0
- package/src/admin-boot.ts +8 -0
- package/src/admin-ui.fixtures.ts +2 -0
- package/src/admin-ui.smoke.test.ts +2 -0
- package/src/agent-cron-jobs.unit.test.ts +4 -0
- package/src/agent-cron-run-stats.smoke.test.ts +10 -0
- package/src/agent-cron-runs.integration.test.ts +93 -0
- package/src/agent-cron-runs.ts +47 -15
- package/src/agents-api.smoke.test.ts +18 -0
- package/src/agents-api.ts +17 -0
- package/src/api-auth.ts +1 -0
- package/src/api.smoke.test.ts +12 -0
- package/src/chat.smoke.test.ts +2 -0
- package/src/cron-run-now-config.ts +20 -0
- package/src/cron-run-now-config.unit.test.ts +26 -0
- package/src/cron-run-now.smoke.test.ts +218 -0
- package/src/cron-run-now.ts +207 -0
- package/src/openapi-schemas.ts +14 -0
- package/src/openapi-schemas.unit.test.ts +4 -0
package/prisma/client/models.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type * from './models/AgentCronRunSkillUsage.ts'
|
|
|
27
27
|
export type * from './models/AgentCronRunModelBreakdown.ts'
|
|
28
28
|
export type * from './models/AgentChatTokenUsageDailyByModel.ts'
|
|
29
29
|
export type * from './models/AgentAsk.ts'
|
|
30
|
+
export type * from './models/AgentCronRunRequest.ts'
|
|
30
31
|
export type * from './models/AgentCronRunTranscript.ts'
|
|
31
32
|
export type * from './models/AgentTranscriptRead.ts'
|
|
32
33
|
export type * from './models/AgentGitHubInstallationsSnapshot.ts'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- COS-42.1: cron run-now request columns and audit table (additive only).
|
|
2
|
+
-- AlterTable
|
|
3
|
+
ALTER TABLE "AgentCronJob" ADD COLUMN "runRequestedAt" TIMESTAMP(3),
|
|
4
|
+
ADD COLUMN "runRequestedBy" TEXT;
|
|
5
|
+
|
|
6
|
+
-- CreateTable
|
|
7
|
+
CREATE TABLE "AgentCronRunRequest" (
|
|
8
|
+
"id" TEXT NOT NULL,
|
|
9
|
+
"cronId" TEXT NOT NULL,
|
|
10
|
+
"agentId" TEXT NOT NULL,
|
|
11
|
+
"requestedBy" TEXT NOT NULL,
|
|
12
|
+
"requestedAt" TIMESTAMP(3) NOT NULL,
|
|
13
|
+
"status" TEXT NOT NULL DEFAULT 'requested',
|
|
14
|
+
"honouredRunId" TEXT,
|
|
15
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
|
|
17
|
+
CONSTRAINT "AgentCronRunRequest_pkey" PRIMARY KEY ("id")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- CreateIndex
|
|
21
|
+
CREATE INDEX "AgentCronRunRequest_cronId_requestedAt_idx" ON "AgentCronRunRequest"("cronId", "requestedAt");
|
|
22
|
+
|
|
23
|
+
-- CreateIndex
|
|
24
|
+
CREATE INDEX "AgentCronRunRequest_agentId_requestedAt_idx" ON "AgentCronRunRequest"("agentId", "requestedAt");
|
|
@@ -134,6 +134,11 @@ model AgentCronJob {
|
|
|
134
134
|
// restoreLockdownDisabled() can re-enable exactly those crons. Null for
|
|
135
135
|
// enabled crons and for crons the user disabled manually.
|
|
136
136
|
lockdownDisabledAt DateTime?
|
|
137
|
+
// Run-now request (COS-42.1): stamped by POST .../run-now, cleared when the
|
|
138
|
+
// runtime creates the run (commandVariant "run-now"). The runtime honours a
|
|
139
|
+
// stamp at most once and only while it is fresh (15 minutes).
|
|
140
|
+
runRequestedAt DateTime?
|
|
141
|
+
runRequestedBy String?
|
|
137
142
|
createdAt DateTime @default(now())
|
|
138
143
|
updatedAt DateTime @updatedAt
|
|
139
144
|
|
|
@@ -426,6 +431,25 @@ model AgentAsk {
|
|
|
426
431
|
@@index([fromAgentId, createdAt])
|
|
427
432
|
}
|
|
428
433
|
|
|
434
|
+
// ─── Agent Cron Run Requests ────────────────────────────────────────────────
|
|
435
|
+
// Audit row per POST /agents/{id}/crons/{cronId}/run-now (COS-42.1), written
|
|
436
|
+
// before the 202 returns. status: requested | honoured | expired. Holds no
|
|
437
|
+
// prompt text. honouredRunId links the AgentCronRun the runtime created.
|
|
438
|
+
|
|
439
|
+
model AgentCronRunRequest {
|
|
440
|
+
id String @id @default(cuid())
|
|
441
|
+
cronId String
|
|
442
|
+
agentId String
|
|
443
|
+
requestedBy String
|
|
444
|
+
requestedAt DateTime
|
|
445
|
+
status String @default("requested")
|
|
446
|
+
honouredRunId String?
|
|
447
|
+
createdAt DateTime @default(now())
|
|
448
|
+
|
|
449
|
+
@@index([cronId, requestedAt])
|
|
450
|
+
@@index([agentId, requestedAt])
|
|
451
|
+
}
|
|
452
|
+
|
|
429
453
|
// ─── Cron-run transcripts (COS-43.1) ────────────────────────────────────────
|
|
430
454
|
// One redacted session transcript per cron run, uploaded by the agent runtime
|
|
431
455
|
// (already redacted + tail-truncated to 1 MiB) and served only to admin /
|
package/src/admin-boot.ts
CHANGED
|
@@ -72,6 +72,8 @@ import {
|
|
|
72
72
|
import { createAdminUIApp } from "./admin-ui.ts";
|
|
73
73
|
import { AgentAskService } from "./agent-ask.ts";
|
|
74
74
|
import { parseAgentAskConfig } from "./agent-ask-config.ts";
|
|
75
|
+
import { CronRunNowService } from "./cron-run-now.ts";
|
|
76
|
+
import { parseCronRunNowConfig } from "./cron-run-now-config.ts";
|
|
75
77
|
import { AgentChatTokenService } from "./agent-chat-tokens.ts";
|
|
76
78
|
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
77
79
|
import { AgentCronRunStatsService } from "./agent-cron-run-stats.ts";
|
|
@@ -292,6 +294,8 @@ export async function startAdmin(
|
|
|
292
294
|
|
|
293
295
|
// Agent-to-agent ask (COS-31.1). Throws at boot on a malformed allowlist.
|
|
294
296
|
const agentAskConfig = parseAgentAskConfig(process.env);
|
|
297
|
+
// Cron run-now (COS-42.1).
|
|
298
|
+
const cronRunNowConfig = parseCronRunNowConfig(process.env);
|
|
295
299
|
|
|
296
300
|
// Cron-run transcript API (COS-43.1): flag off by default.
|
|
297
301
|
const transcriptConfig = parseTranscriptConfig(process.env);
|
|
@@ -462,6 +466,10 @@ export async function startAdmin(
|
|
|
462
466
|
config: agentAskConfig,
|
|
463
467
|
store: new AgentAskService(prisma),
|
|
464
468
|
},
|
|
469
|
+
cronRunNow: {
|
|
470
|
+
config: cronRunNowConfig,
|
|
471
|
+
store: new CronRunNowService(prisma),
|
|
472
|
+
},
|
|
465
473
|
...(chatClient ? { chatClient } : {}),
|
|
466
474
|
transcripts: {
|
|
467
475
|
config: transcriptConfig,
|
package/src/admin-ui.fixtures.ts
CHANGED
|
@@ -116,6 +116,8 @@ function makeFakePrisma(opts: {
|
|
|
116
116
|
system: true,
|
|
117
117
|
parentCronId: null,
|
|
118
118
|
lockdownDisabledAt: null,
|
|
119
|
+
runRequestedAt: null,
|
|
120
|
+
runRequestedBy: null,
|
|
119
121
|
createdAt: new Date("2024-01-01"),
|
|
120
122
|
updatedAt: new Date("2024-01-01"),
|
|
121
123
|
...seed,
|
|
@@ -156,6 +158,8 @@ function makeFakePrisma(opts: {
|
|
|
156
158
|
system: true,
|
|
157
159
|
parentCronId: null,
|
|
158
160
|
lockdownDisabledAt: null,
|
|
161
|
+
runRequestedAt: null,
|
|
162
|
+
runRequestedBy: null,
|
|
159
163
|
createdAt: new Date("2024-01-01"),
|
|
160
164
|
updatedAt: new Date("2024-01-01"),
|
|
161
165
|
...data,
|
|
@@ -276,6 +276,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
276
276
|
system: false,
|
|
277
277
|
parentCronId: null,
|
|
278
278
|
lockdownDisabledAt: null,
|
|
279
|
+
runRequestedAt: null,
|
|
280
|
+
runRequestedBy: null,
|
|
279
281
|
createdAt: new Date("2024-01-01"),
|
|
280
282
|
updatedAt: new Date("2024-01-01"),
|
|
281
283
|
}),
|
|
@@ -293,6 +295,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
293
295
|
system: false,
|
|
294
296
|
parentCronId: null,
|
|
295
297
|
lockdownDisabledAt: null,
|
|
298
|
+
runRequestedAt: null,
|
|
299
|
+
runRequestedBy: null,
|
|
296
300
|
createdAt: new Date("2024-01-01"),
|
|
297
301
|
updatedAt: new Date("2024-01-01"),
|
|
298
302
|
}),
|
|
@@ -311,6 +315,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
311
315
|
system: false,
|
|
312
316
|
parentCronId: null,
|
|
313
317
|
lockdownDisabledAt: null,
|
|
318
|
+
runRequestedAt: null,
|
|
319
|
+
runRequestedBy: null,
|
|
314
320
|
createdAt: new Date("2024-01-01"),
|
|
315
321
|
updatedAt: new Date("2024-01-01"),
|
|
316
322
|
}),
|
|
@@ -328,6 +334,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
328
334
|
system: false,
|
|
329
335
|
parentCronId: null,
|
|
330
336
|
lockdownDisabledAt: null,
|
|
337
|
+
runRequestedAt: null,
|
|
338
|
+
runRequestedBy: null,
|
|
331
339
|
createdAt: new Date("2024-01-01"),
|
|
332
340
|
updatedAt: new Date("2024-01-01"),
|
|
333
341
|
}),
|
|
@@ -345,6 +353,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
345
353
|
system: false,
|
|
346
354
|
parentCronId: null,
|
|
347
355
|
lockdownDisabledAt: null,
|
|
356
|
+
runRequestedAt: null,
|
|
357
|
+
runRequestedBy: null,
|
|
348
358
|
createdAt: new Date("2024-01-01"),
|
|
349
359
|
updatedAt: new Date("2024-01-01"),
|
|
350
360
|
}),
|
|
@@ -1489,3 +1489,96 @@ describeOrSkip("AgentCronRunService — run telemetry (integration)", () => {
|
|
|
1489
1489
|
).toBe(0);
|
|
1490
1490
|
});
|
|
1491
1491
|
});
|
|
1492
|
+
|
|
1493
|
+
describeOrSkip(
|
|
1494
|
+
"AgentCronRunService run-now clear-on-create (integration)",
|
|
1495
|
+
() => {
|
|
1496
|
+
let prisma: PrismaClient;
|
|
1497
|
+
let agentId: string;
|
|
1498
|
+
let cronId: string;
|
|
1499
|
+
let runService: AgentCronRunService;
|
|
1500
|
+
|
|
1501
|
+
async function stamp(requestedAt: Date) {
|
|
1502
|
+
await prisma.agentCronJob.update({
|
|
1503
|
+
where: { id: cronId },
|
|
1504
|
+
data: { runRequestedAt: requestedAt, runRequestedBy: "tester" },
|
|
1505
|
+
});
|
|
1506
|
+
return prisma.agentCronRunRequest.create({
|
|
1507
|
+
data: {
|
|
1508
|
+
cronId,
|
|
1509
|
+
agentId,
|
|
1510
|
+
requestedBy: "tester",
|
|
1511
|
+
requestedAt,
|
|
1512
|
+
status: "requested",
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
beforeEach(async () => {
|
|
1518
|
+
prisma = makePrisma();
|
|
1519
|
+
await prisma.agentCronRunRequest.deleteMany();
|
|
1520
|
+
await prisma.agentCronRun.deleteMany();
|
|
1521
|
+
await prisma.agentCronJob.deleteMany();
|
|
1522
|
+
agentId = await createAgent(prisma, "Run-now Agent");
|
|
1523
|
+
cronId = await createCron(new AgentCronJobService(prisma), agentId);
|
|
1524
|
+
runService = new AgentCronRunService(prisma);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
afterEach(async () => {
|
|
1528
|
+
await prisma.agentCronRunRequest.deleteMany();
|
|
1529
|
+
await prisma.agentCronRun.deleteMany();
|
|
1530
|
+
await prisma.agentCronJob.deleteMany();
|
|
1531
|
+
await prisma.agent.deleteMany({ where: { id: agentId } });
|
|
1532
|
+
await prisma.$disconnect();
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
it("commandVariant run-now clears the stamp and honours the newest request", async () => {
|
|
1536
|
+
await stamp(new Date("2026-10-26T11:00:00Z"));
|
|
1537
|
+
const newest = await stamp(new Date("2026-10-26T11:30:00Z"));
|
|
1538
|
+
|
|
1539
|
+
const run = await runService.create(cronId, agentId, {
|
|
1540
|
+
startedAt: new Date("2026-10-26T11:31:00Z"),
|
|
1541
|
+
commandVariant: "run-now",
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
const job = await prisma.agentCronJob.findUniqueOrThrow({
|
|
1545
|
+
where: { id: cronId },
|
|
1546
|
+
});
|
|
1547
|
+
expect(job.runRequestedAt).toBeNull();
|
|
1548
|
+
expect(job.runRequestedBy).toBeNull();
|
|
1549
|
+
const honoured = await prisma.agentCronRunRequest.findUniqueOrThrow({
|
|
1550
|
+
where: { id: newest.id },
|
|
1551
|
+
});
|
|
1552
|
+
expect(honoured.status).toBe("honoured");
|
|
1553
|
+
expect(honoured.honouredRunId).toBe(run.id);
|
|
1554
|
+
const stillRequested = await prisma.agentCronRunRequest.count({
|
|
1555
|
+
where: { cronId, status: "requested" },
|
|
1556
|
+
});
|
|
1557
|
+
expect(stillRequested).toBe(0);
|
|
1558
|
+
const superseded = await prisma.agentCronRunRequest.count({
|
|
1559
|
+
where: { cronId, status: "expired" },
|
|
1560
|
+
});
|
|
1561
|
+
expect(superseded).toBe(1);
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
it("any other variant leaves the stamp and the request alone", async () => {
|
|
1565
|
+
const req = await stamp(new Date("2026-10-26T11:30:00Z"));
|
|
1566
|
+
await runService.create(cronId, agentId, {
|
|
1567
|
+
startedAt: new Date("2026-10-26T11:31:00Z"),
|
|
1568
|
+
commandVariant: "default",
|
|
1569
|
+
});
|
|
1570
|
+
await runService.create(cronId, agentId, {
|
|
1571
|
+
startedAt: new Date("2026-10-26T11:32:00Z"),
|
|
1572
|
+
});
|
|
1573
|
+
const job = await prisma.agentCronJob.findUniqueOrThrow({
|
|
1574
|
+
where: { id: cronId },
|
|
1575
|
+
});
|
|
1576
|
+
expect(job.runRequestedAt).not.toBeNull();
|
|
1577
|
+
const row = await prisma.agentCronRunRequest.findUniqueOrThrow({
|
|
1578
|
+
where: { id: req.id },
|
|
1579
|
+
});
|
|
1580
|
+
expect(row.status).toBe("requested");
|
|
1581
|
+
expect(row.honouredRunId).toBeNull();
|
|
1582
|
+
});
|
|
1583
|
+
},
|
|
1584
|
+
);
|
package/src/agent-cron-runs.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { NotFoundError } from "./errors.ts";
|
|
|
15
15
|
|
|
16
16
|
export type { AgentCronRun };
|
|
17
17
|
|
|
18
|
+
/** commandVariant a runtime reports when it honours a run-now request. */
|
|
19
|
+
const RUN_NOW_VARIANT = "run-now";
|
|
20
|
+
|
|
18
21
|
/** An AgentCronRun with its per-model token/cost breakdown rows attached. */
|
|
19
22
|
type AgentCronRunWithModelBreakdown = Prisma.AgentCronRunGetPayload<{
|
|
20
23
|
include: { modelBreakdown: true };
|
|
@@ -193,21 +196,50 @@ export class AgentCronRunService {
|
|
|
193
196
|
throw new NotFoundError(`cron job ${cronId} not found`);
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
const data = {
|
|
200
|
+
cronId,
|
|
201
|
+
agentId,
|
|
202
|
+
startedAt: input.startedAt,
|
|
203
|
+
completedAt: input.completedAt ?? null,
|
|
204
|
+
skipped: input.skipped ?? false,
|
|
205
|
+
skipReason: input.skipReason ?? null,
|
|
206
|
+
outcome: input.outcome ?? null,
|
|
207
|
+
error: input.error ?? null,
|
|
208
|
+
phaseId: input.phaseId ?? null,
|
|
209
|
+
itemType: input.itemType ?? null,
|
|
210
|
+
itemId: input.itemId ?? null,
|
|
211
|
+
commandVariant: input.commandVariant ?? null,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (input.commandVariant !== RUN_NOW_VARIANT) {
|
|
215
|
+
return this.prisma.agentCronRun.create({ data });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Run-now (COS-42.1): creating the run honours the pending request, so
|
|
219
|
+
// clear the stamp and link the newest requested audit row to this run.
|
|
220
|
+
return this.prisma.$transaction(async (tx) => {
|
|
221
|
+
const run = await tx.agentCronRun.create({ data });
|
|
222
|
+
await tx.agentCronJob.update({
|
|
223
|
+
where: { id: cronId },
|
|
224
|
+
data: { runRequestedAt: null, runRequestedBy: null },
|
|
225
|
+
});
|
|
226
|
+
const request = await tx.agentCronRunRequest.findFirst({
|
|
227
|
+
where: { cronId, status: "requested" },
|
|
228
|
+
orderBy: [{ requestedAt: "desc" }, { createdAt: "desc" }],
|
|
229
|
+
});
|
|
230
|
+
if (request) {
|
|
231
|
+
await tx.agentCronRunRequest.update({
|
|
232
|
+
where: { id: request.id },
|
|
233
|
+
data: { status: "honoured", honouredRunId: run.id },
|
|
234
|
+
});
|
|
235
|
+
// Older still-pending requests were superseded by this one and can
|
|
236
|
+
// never be honoured: record them as expired rather than pending.
|
|
237
|
+
await tx.agentCronRunRequest.updateMany({
|
|
238
|
+
where: { cronId, status: "requested", id: { not: request.id } },
|
|
239
|
+
data: { status: "expired" },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return run;
|
|
211
243
|
});
|
|
212
244
|
}
|
|
213
245
|
|
|
@@ -183,6 +183,8 @@ const MOCK_CRON = {
|
|
|
183
183
|
system: false,
|
|
184
184
|
parentCronId: null as string | null,
|
|
185
185
|
lockdownDisabledAt: null,
|
|
186
|
+
runRequestedAt: null,
|
|
187
|
+
runRequestedBy: null,
|
|
186
188
|
createdAt: new Date("2024-01-01"),
|
|
187
189
|
updatedAt: new Date("2024-01-01"),
|
|
188
190
|
};
|
|
@@ -380,6 +382,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
380
382
|
system: false,
|
|
381
383
|
parentCronId: null,
|
|
382
384
|
lockdownDisabledAt: null,
|
|
385
|
+
runRequestedAt: null,
|
|
386
|
+
runRequestedBy: null,
|
|
383
387
|
createdAt: new Date("2024-01-01"),
|
|
384
388
|
updatedAt: new Date("2024-01-01"),
|
|
385
389
|
}),
|
|
@@ -397,6 +401,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
397
401
|
system: false,
|
|
398
402
|
parentCronId: null,
|
|
399
403
|
lockdownDisabledAt: null,
|
|
404
|
+
runRequestedAt: null,
|
|
405
|
+
runRequestedBy: null,
|
|
400
406
|
createdAt: new Date("2024-01-01"),
|
|
401
407
|
updatedAt: new Date("2024-01-01"),
|
|
402
408
|
}),
|
|
@@ -415,6 +421,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
415
421
|
system: false,
|
|
416
422
|
parentCronId: null,
|
|
417
423
|
lockdownDisabledAt: null,
|
|
424
|
+
runRequestedAt: null,
|
|
425
|
+
runRequestedBy: null,
|
|
418
426
|
createdAt: new Date("2024-01-01"),
|
|
419
427
|
updatedAt: new Date("2024-01-01"),
|
|
420
428
|
}),
|
|
@@ -432,6 +440,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
432
440
|
system: false,
|
|
433
441
|
parentCronId: null,
|
|
434
442
|
lockdownDisabledAt: null,
|
|
443
|
+
runRequestedAt: null,
|
|
444
|
+
runRequestedBy: null,
|
|
435
445
|
createdAt: new Date("2024-01-01"),
|
|
436
446
|
updatedAt: new Date("2024-01-01"),
|
|
437
447
|
}),
|
|
@@ -449,6 +459,8 @@ function makeMockDeps(): AdminDeps {
|
|
|
449
459
|
system: false,
|
|
450
460
|
parentCronId: null,
|
|
451
461
|
lockdownDisabledAt: null,
|
|
462
|
+
runRequestedAt: null,
|
|
463
|
+
runRequestedBy: null,
|
|
452
464
|
createdAt: new Date("2024-01-01"),
|
|
453
465
|
updatedAt: new Date("2024-01-01"),
|
|
454
466
|
}),
|
|
@@ -4631,6 +4643,8 @@ describe("admin API — cron runs", () => {
|
|
|
4631
4643
|
system: false,
|
|
4632
4644
|
parentCronId: null,
|
|
4633
4645
|
lockdownDisabledAt: null,
|
|
4646
|
+
runRequestedAt: null,
|
|
4647
|
+
runRequestedBy: null,
|
|
4634
4648
|
createdAt: new Date("2024-01-01"),
|
|
4635
4649
|
updatedAt: new Date("2024-01-01"),
|
|
4636
4650
|
lastRun: null,
|
|
@@ -4649,6 +4663,8 @@ describe("admin API — cron runs", () => {
|
|
|
4649
4663
|
name: "shipwright-dev-task",
|
|
4650
4664
|
system: true,
|
|
4651
4665
|
lockdownDisabledAt: null,
|
|
4666
|
+
runRequestedAt: null,
|
|
4667
|
+
runRequestedBy: null,
|
|
4652
4668
|
parentCronId: CRON_ID,
|
|
4653
4669
|
createdAt: new Date("2024-01-01"),
|
|
4654
4670
|
updatedAt: new Date("2024-01-01"),
|
|
@@ -4936,6 +4952,8 @@ function makeMockDepsWithRunSummary(): AdminDeps {
|
|
|
4936
4952
|
system: false,
|
|
4937
4953
|
parentCronId: null,
|
|
4938
4954
|
lockdownDisabledAt: null,
|
|
4955
|
+
runRequestedAt: null,
|
|
4956
|
+
runRequestedBy: null,
|
|
4939
4957
|
createdAt: new Date("2024-01-01"),
|
|
4940
4958
|
updatedAt: new Date("2024-01-01"),
|
|
4941
4959
|
lastRun: {
|
package/src/agents-api.ts
CHANGED
|
@@ -46,6 +46,10 @@ import {
|
|
|
46
46
|
registerAgentAskRoutes,
|
|
47
47
|
} from "./agent-ask.ts";
|
|
48
48
|
import type { AgentChatTokenService } from "./agent-chat-tokens.ts";
|
|
49
|
+
import {
|
|
50
|
+
type CronRunNowDeps,
|
|
51
|
+
registerCronRunNowRoutes,
|
|
52
|
+
} from "./cron-run-now.ts";
|
|
49
53
|
import type {
|
|
50
54
|
AgentCronJobService,
|
|
51
55
|
AgentCronJobWithRunSummary,
|
|
@@ -289,6 +293,12 @@ export interface AdminDeps {
|
|
|
289
293
|
* 404. See agent-ask.ts.
|
|
290
294
|
*/
|
|
291
295
|
agentAsk?: AgentAskDeps;
|
|
296
|
+
/**
|
|
297
|
+
* Cron run-now (COS-42.1): flag config, the stamp+audit store and an
|
|
298
|
+
* injectable clock. Absent or flag off -> the run-now route 404s. See
|
|
299
|
+
* cron-run-now.ts.
|
|
300
|
+
*/
|
|
301
|
+
cronRunNow?: CronRunNowDeps;
|
|
292
302
|
/**
|
|
293
303
|
* Chat-service thread/message client behind POST /agents/:id/ask
|
|
294
304
|
* (admin-boot wires the HttpChatClient the chat UI uses). Absent → the ask
|
|
@@ -2344,6 +2354,10 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
2344
2354
|
agentService,
|
|
2345
2355
|
});
|
|
2346
2356
|
|
|
2357
|
+
// ─── Cron run-now (COS-42.1) ──────────────────────────────────────────────
|
|
2358
|
+
|
|
2359
|
+
registerCronRunNowRoutes(app, { cronRunNow: deps.cronRunNow });
|
|
2360
|
+
|
|
2347
2361
|
// ─── Cron-run transcripts (COS-43.1) ──────────────────────────────────────
|
|
2348
2362
|
|
|
2349
2363
|
registerCronRunTranscriptRoutes(app, { transcripts: deps.transcripts });
|
|
@@ -2529,10 +2543,12 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
2529
2543
|
function serializeCron(cron: {
|
|
2530
2544
|
createdAt: Date;
|
|
2531
2545
|
updatedAt: Date;
|
|
2546
|
+
runRequestedAt?: Date | null;
|
|
2532
2547
|
[k: string]: unknown;
|
|
2533
2548
|
}): z.infer<typeof AgentCronJobSchema> {
|
|
2534
2549
|
return {
|
|
2535
2550
|
...cron,
|
|
2551
|
+
runRequestedAt: cron.runRequestedAt?.toISOString() ?? null,
|
|
2536
2552
|
createdAt: cron.createdAt.toISOString(),
|
|
2537
2553
|
updatedAt: cron.updatedAt.toISOString(),
|
|
2538
2554
|
} as z.infer<typeof AgentCronJobSchema>;
|
|
@@ -2562,6 +2578,7 @@ function serializeCronWithSummary(job: AgentCronJobWithRunSummary): z.infer<
|
|
|
2562
2578
|
name: job.name,
|
|
2563
2579
|
system: job.system,
|
|
2564
2580
|
parentCronId: job.parentCronId,
|
|
2581
|
+
runRequestedAt: job.runRequestedAt?.toISOString() ?? null,
|
|
2565
2582
|
createdAt: job.createdAt.toISOString(),
|
|
2566
2583
|
updatedAt: job.updatedAt.toISOString(),
|
|
2567
2584
|
lastRun: job.lastRun
|
package/src/api-auth.ts
CHANGED
|
@@ -106,6 +106,7 @@ export const PLATFORM_OPS_ROUTES: readonly PlatformOpsRoute[] = [
|
|
|
106
106
|
{ method: "DELETE", path: "/agents/{id}/repos", subpaths: true },
|
|
107
107
|
{ method: "PATCH", path: "/agents/{id}/crons/{cronId}" },
|
|
108
108
|
{ method: "POST", path: "/agents/{id}/ask" },
|
|
109
|
+
{ method: "POST", path: "/agents/{id}/crons/{cronId}/run-now" },
|
|
109
110
|
{ method: "POST", path: "/agents/{id}/chat-credentials", subpaths: true },
|
|
110
111
|
{ method: "DELETE", path: "/agents/{id}/chat-credentials", subpaths: true },
|
|
111
112
|
];
|
package/src/api.smoke.test.ts
CHANGED
|
@@ -208,6 +208,8 @@ function makeCron(agentId: string, id: string): AgentCronJob {
|
|
|
208
208
|
system: false,
|
|
209
209
|
parentCronId: null,
|
|
210
210
|
lockdownDisabledAt: null,
|
|
211
|
+
runRequestedAt: null,
|
|
212
|
+
runRequestedBy: null,
|
|
211
213
|
createdAt: makeDate(),
|
|
212
214
|
updatedAt: makeDate(),
|
|
213
215
|
};
|
|
@@ -814,6 +816,8 @@ function buildCombinedApp() {
|
|
|
814
816
|
system: false,
|
|
815
817
|
parentCronId: null,
|
|
816
818
|
lockdownDisabledAt: null,
|
|
819
|
+
runRequestedAt: null,
|
|
820
|
+
runRequestedBy: null,
|
|
817
821
|
createdAt: new Date(),
|
|
818
822
|
updatedAt: new Date(),
|
|
819
823
|
}),
|
|
@@ -831,6 +835,8 @@ function buildCombinedApp() {
|
|
|
831
835
|
system: false,
|
|
832
836
|
parentCronId: null,
|
|
833
837
|
lockdownDisabledAt: null,
|
|
838
|
+
runRequestedAt: null,
|
|
839
|
+
runRequestedBy: null,
|
|
834
840
|
createdAt: new Date(),
|
|
835
841
|
updatedAt: new Date(),
|
|
836
842
|
}),
|
|
@@ -849,6 +855,8 @@ function buildCombinedApp() {
|
|
|
849
855
|
system: false,
|
|
850
856
|
parentCronId: null,
|
|
851
857
|
lockdownDisabledAt: null,
|
|
858
|
+
runRequestedAt: null,
|
|
859
|
+
runRequestedBy: null,
|
|
852
860
|
createdAt: new Date(),
|
|
853
861
|
updatedAt: new Date(),
|
|
854
862
|
}),
|
|
@@ -866,6 +874,8 @@ function buildCombinedApp() {
|
|
|
866
874
|
system: false,
|
|
867
875
|
parentCronId: null,
|
|
868
876
|
lockdownDisabledAt: null,
|
|
877
|
+
runRequestedAt: null,
|
|
878
|
+
runRequestedBy: null,
|
|
869
879
|
createdAt: new Date(),
|
|
870
880
|
updatedAt: new Date(),
|
|
871
881
|
}),
|
|
@@ -888,6 +898,8 @@ function buildCombinedApp() {
|
|
|
888
898
|
system: false,
|
|
889
899
|
parentCronId: null,
|
|
890
900
|
lockdownDisabledAt: null,
|
|
901
|
+
runRequestedAt: null,
|
|
902
|
+
runRequestedBy: null,
|
|
891
903
|
createdAt: new Date(),
|
|
892
904
|
updatedAt: new Date(),
|
|
893
905
|
}),
|
package/src/chat.smoke.test.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/cron-run-now-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure parser for the cron run-now flag (COS-42.1). Takes an injected env
|
|
5
|
+
* object (no process.env reads) so it is deterministically unit-testable;
|
|
6
|
+
* admin-boot reads it once at startup and injects the resolved config as
|
|
7
|
+
* deps. Same "enabled" convention as the agent-ask and self-serve flags.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Structural env shape — accepts `process.env` or a plain test object. */
|
|
11
|
+
type CronRunNowEnv = Readonly<Record<string, string | undefined>>;
|
|
12
|
+
|
|
13
|
+
export interface CronRunNowConfig {
|
|
14
|
+
/** True only when SHIPWRIGHT_CRON_RUN_NOW_ENABLED === "enabled". */
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseCronRunNowConfig(env: CronRunNowEnv): CronRunNowConfig {
|
|
19
|
+
return { enabled: env.SHIPWRIGHT_CRON_RUN_NOW_ENABLED === "enabled" };
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/cron-run-now-config.unit.test.ts
|
|
3
|
+
* Pure parser tests for the cron run-now flag (COS-42.1).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import { parseCronRunNowConfig } from "./cron-run-now-config.ts";
|
|
8
|
+
|
|
9
|
+
describe("parseCronRunNowConfig", () => {
|
|
10
|
+
it('is on only for the literal "enabled"', () => {
|
|
11
|
+
expect(
|
|
12
|
+
parseCronRunNowConfig({ SHIPWRIGHT_CRON_RUN_NOW_ENABLED: "enabled" })
|
|
13
|
+
.enabled,
|
|
14
|
+
).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('"true", "" and absent are off', () => {
|
|
18
|
+
for (const value of ["true", "", "Enabled", " enabled", undefined]) {
|
|
19
|
+
expect(
|
|
20
|
+
parseCronRunNowConfig({ SHIPWRIGHT_CRON_RUN_NOW_ENABLED: value })
|
|
21
|
+
.enabled,
|
|
22
|
+
).toBe(false);
|
|
23
|
+
}
|
|
24
|
+
expect(parseCronRunNowConfig({}).enabled).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|