@app-vitals/agent-admin 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/e2e/test-server.ts +5 -5
- package/openapi.json +12 -19
- package/openapi.platform.json +11 -18
- package/package.json +2 -2
- package/prisma/client/internal/class.ts +4 -4
- package/prisma/client/internal/prismaNamespace.ts +0 -1
- package/prisma/client/internal/prismaNamespaceBrowser.ts +0 -1
- package/prisma/client/models/Agent.ts +62 -137
- package/prisma/migrations/20261030000000_drop_agent_type_name/migration.sql +8 -0
- package/prisma/schema/schema.prisma +3 -4
- package/src/account-lifecycle.integration.test.ts +1 -1
- package/src/account-trial-warning-sweeper.integration.test.ts +1 -1
- package/src/admin-local-agent.smoke.test.ts +12 -12
- package/src/admin-ui-pages.ts +2 -2
- package/src/admin-ui-pages.unit.test.ts +2 -2
- package/src/admin-ui.fixtures.ts +6 -6
- package/src/admin-ui.smoke.test.ts +39 -39
- package/src/admin-ui.ts +8 -8
- package/src/agent-cron-jobs.integration.test.ts +8 -7
- package/src/agent-cron-jobs.ts +4 -5
- package/src/agent-cron-jobs.unit.test.ts +23 -23
- package/src/agent-patch-author-allowlist.integration.test.ts +4 -4
- package/src/agent-specialty-backfill.integration.test.ts +98 -53
- package/src/agents-api.integration.test.ts +20 -20
- package/src/agents-api.smoke.test.ts +76 -77
- package/src/agents-api.ts +9 -14
- package/src/agents.integration.test.ts +40 -53
- package/src/agents.ts +33 -46
- package/src/agents.unit.test.ts +28 -35
- package/src/api.smoke.test.ts +24 -2
- package/src/api.ts +15 -16
- package/src/openapi-schemas.ts +14 -7
- package/src/slack-provisioning-service.integration.test.ts +2 -2
- package/src/slack-provisioning.integration.test.ts +2 -2
- package/src/specialty-manifest-loader.ts +32 -32
- package/src/specialty-manifest-loader.unit.test.ts +8 -8
- package/src/startup-policy-reconcile.unit.test.ts +1 -1
- package/src/trial-expiry-sweeper.integration.test.ts +1 -1
- package/src/trial-expiry-warning-sweeper.integration.test.ts +1 -1
- package/src/agent-specialty.ts +0 -22
- package/src/agent-specialty.unit.test.ts +0 -43
package/src/agents.unit.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ interface FakeAgentRow {
|
|
|
29
29
|
reviewAuthorAllowlist: string[];
|
|
30
30
|
patchAuthorAllowlist: string[];
|
|
31
31
|
restrictSlackToMembers: boolean;
|
|
32
|
-
|
|
32
|
+
specialty: string;
|
|
33
33
|
createdAt: Date;
|
|
34
34
|
updatedAt: Date;
|
|
35
35
|
// APM-1.3: the six real agent-policy fields (APM-1.1 DB columns).
|
|
@@ -93,7 +93,7 @@ function makeFakePrisma(
|
|
|
93
93
|
reviewAuthorAllowlist: data.reviewAuthorAllowlist ?? [],
|
|
94
94
|
patchAuthorAllowlist: data.patchAuthorAllowlist ?? [],
|
|
95
95
|
restrictSlackToMembers: data.restrictSlackToMembers ?? false,
|
|
96
|
-
|
|
96
|
+
specialty: "coding",
|
|
97
97
|
createdAt: new Date("2024-01-01"),
|
|
98
98
|
updatedAt: new Date("2024-01-01"),
|
|
99
99
|
autoPostReviews: true,
|
|
@@ -257,8 +257,8 @@ function fakeRegistry(
|
|
|
257
257
|
requiredEnvByType: Record<string, string[]>,
|
|
258
258
|
): SpecialtyManifestResolver {
|
|
259
259
|
return {
|
|
260
|
-
getManifest(
|
|
261
|
-
const required = requiredEnvByType[
|
|
260
|
+
getManifest(specialty: string): SpecialtyManifest {
|
|
261
|
+
const required = requiredEnvByType[specialty] ?? [];
|
|
262
262
|
return {
|
|
263
263
|
env: {
|
|
264
264
|
required: required.map((key) => ({
|
|
@@ -270,9 +270,9 @@ function fakeRegistry(
|
|
|
270
270
|
},
|
|
271
271
|
} as unknown as SpecialtyManifest;
|
|
272
272
|
},
|
|
273
|
-
tryGetManifest(
|
|
274
|
-
if (!(
|
|
275
|
-
return this.getManifest(
|
|
273
|
+
tryGetManifest(specialty: string): SpecialtyManifest | undefined {
|
|
274
|
+
if (!(specialty in requiredEnvByType)) return undefined;
|
|
275
|
+
return this.getManifest(specialty);
|
|
276
276
|
},
|
|
277
277
|
listTypes() {
|
|
278
278
|
return Object.keys(requiredEnvByType).map((name) => ({
|
|
@@ -293,7 +293,7 @@ function seedRow(overrides: Partial<FakeAgentRow> = {}): FakeAgentRow {
|
|
|
293
293
|
reviewAuthorAllowlist: [],
|
|
294
294
|
patchAuthorAllowlist: [],
|
|
295
295
|
restrictSlackToMembers: false,
|
|
296
|
-
|
|
296
|
+
specialty: "coding",
|
|
297
297
|
createdAt: new Date("2024-01-01"),
|
|
298
298
|
updatedAt: new Date("2024-01-01"),
|
|
299
299
|
autoPostReviews: true,
|
|
@@ -399,7 +399,7 @@ describe("AgentService.delete", () => {
|
|
|
399
399
|
// ─── list ───────────────────────────────────────────────────────────────────
|
|
400
400
|
|
|
401
401
|
describe("AgentService.list", () => {
|
|
402
|
-
it("returns id/name/selfHosted/
|
|
402
|
+
it("returns id/name/selfHosted/specialty for all agents, ordered by name asc", async () => {
|
|
403
403
|
const prisma = makeFakePrisma([
|
|
404
404
|
seedRow({ id: "a1", name: "Zeta", selfHosted: false }),
|
|
405
405
|
seedRow({ id: "a2", name: "Alpha", selfHosted: true }),
|
|
@@ -413,14 +413,12 @@ describe("AgentService.list", () => {
|
|
|
413
413
|
id: "a2",
|
|
414
414
|
name: "Alpha",
|
|
415
415
|
selfHosted: true,
|
|
416
|
-
typeName: "coding",
|
|
417
416
|
specialty: "coding",
|
|
418
417
|
},
|
|
419
418
|
{
|
|
420
419
|
id: "a1",
|
|
421
420
|
name: "Zeta",
|
|
422
421
|
selfHosted: false,
|
|
423
|
-
typeName: "coding",
|
|
424
422
|
specialty: "coding",
|
|
425
423
|
},
|
|
426
424
|
]);
|
|
@@ -437,7 +435,7 @@ describe("AgentService.list", () => {
|
|
|
437
435
|
// ─── getSummary ─────────────────────────────────────────────────────────────
|
|
438
436
|
|
|
439
437
|
describe("AgentService.getSummary", () => {
|
|
440
|
-
it("returns {id, name, selfHosted,
|
|
438
|
+
it("returns {id, name, selfHosted, specialty} for an existing agent", async () => {
|
|
441
439
|
const row = seedRow({ id: "a1", name: "Existing", selfHosted: true });
|
|
442
440
|
const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
|
|
443
441
|
const service = new AgentService(prisma as never);
|
|
@@ -446,7 +444,6 @@ describe("AgentService.getSummary", () => {
|
|
|
446
444
|
id: "a1",
|
|
447
445
|
name: "Existing",
|
|
448
446
|
selfHosted: true,
|
|
449
|
-
typeName: "coding",
|
|
450
447
|
specialty: "coding",
|
|
451
448
|
});
|
|
452
449
|
});
|
|
@@ -462,7 +459,7 @@ describe("AgentService.getSummary", () => {
|
|
|
462
459
|
// ─── getDetail ──────────────────────────────────────────────────────────────
|
|
463
460
|
|
|
464
461
|
describe("AgentService.getDetail", () => {
|
|
465
|
-
it("returns the full record including repos,
|
|
462
|
+
it("returns the full record including repos, specialty, timestamps, and an empty missingRequiredEnv when the type has no required contract", async () => {
|
|
466
463
|
const row = seedRow({
|
|
467
464
|
id: "a1",
|
|
468
465
|
name: "Existing",
|
|
@@ -487,7 +484,6 @@ describe("AgentService.getDetail", () => {
|
|
|
487
484
|
reviewAuthorAllowlist: ["octocat"],
|
|
488
485
|
patchAuthorAllowlist: [],
|
|
489
486
|
restrictSlackToMembers: false,
|
|
490
|
-
typeName: "coding",
|
|
491
487
|
specialty: "coding",
|
|
492
488
|
createdAt: row.createdAt,
|
|
493
489
|
updatedAt: row.updatedAt,
|
|
@@ -527,7 +523,7 @@ describe("AgentService.getDetail", () => {
|
|
|
527
523
|
expect(detail?.cleanupAfterDays).toBe(30);
|
|
528
524
|
});
|
|
529
525
|
|
|
530
|
-
it("defaults
|
|
526
|
+
it("defaults specialty to 'coding' when the row was seeded without an explicit value", async () => {
|
|
531
527
|
const row = seedRow({ id: "a1" });
|
|
532
528
|
const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
|
|
533
529
|
const service = new AgentService(
|
|
@@ -537,7 +533,7 @@ describe("AgentService.getDetail", () => {
|
|
|
537
533
|
|
|
538
534
|
const detail = await service.getDetail("a1");
|
|
539
535
|
|
|
540
|
-
expect(detail?.
|
|
536
|
+
expect(detail?.specialty).toBe("coding");
|
|
541
537
|
});
|
|
542
538
|
|
|
543
539
|
it("returns null when the agent does not exist", async () => {
|
|
@@ -548,7 +544,7 @@ describe("AgentService.getDetail", () => {
|
|
|
548
544
|
});
|
|
549
545
|
|
|
550
546
|
it("lists a required env key in missingRequiredEnv when no AgentEnv row exists for it", async () => {
|
|
551
|
-
const row = seedRow({ id: "a1",
|
|
547
|
+
const row = seedRow({ id: "a1", specialty: "coding" });
|
|
552
548
|
const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
|
|
553
549
|
const service = new AgentService(
|
|
554
550
|
prisma as never,
|
|
@@ -561,7 +557,7 @@ describe("AgentService.getDetail", () => {
|
|
|
561
557
|
});
|
|
562
558
|
|
|
563
559
|
it("clears a required key from missingRequiredEnv once its AgentEnv row is seeded", async () => {
|
|
564
|
-
const row = seedRow({ id: "a1",
|
|
560
|
+
const row = seedRow({ id: "a1", specialty: "coding" });
|
|
565
561
|
const prisma = makeFakePrisma([row], {
|
|
566
562
|
a1: ["CLAUDE_CODE_OAUTH_TOKEN"],
|
|
567
563
|
}) as unknown as FakePrisma;
|
|
@@ -576,7 +572,7 @@ describe("AgentService.getDetail", () => {
|
|
|
576
572
|
});
|
|
577
573
|
|
|
578
574
|
it("returns an empty missingRequiredEnv array (not undefined/null) when the manifest's required contract is empty, without querying AgentEnv", async () => {
|
|
579
|
-
const row = seedRow({ id: "a1",
|
|
575
|
+
const row = seedRow({ id: "a1", specialty: "coding" });
|
|
580
576
|
const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
|
|
581
577
|
let findManyCalled = false;
|
|
582
578
|
const originalFindMany = prisma.agentEnv.findMany.bind(prisma.agentEnv);
|
|
@@ -597,7 +593,7 @@ describe("AgentService.getDetail", () => {
|
|
|
597
593
|
});
|
|
598
594
|
|
|
599
595
|
it("never includes AgentEnv values in the computed result — only key names are read", async () => {
|
|
600
|
-
const row = seedRow({ id: "a1",
|
|
596
|
+
const row = seedRow({ id: "a1", specialty: "coding" });
|
|
601
597
|
const prisma = makeFakePrisma([row], {
|
|
602
598
|
a1: ["CLAUDE_CODE_OAUTH_TOKEN"],
|
|
603
599
|
}) as unknown as FakePrisma;
|
|
@@ -747,8 +743,7 @@ describe("AgentService.getById", () => {
|
|
|
747
743
|
restrictSlackToMembers: false,
|
|
748
744
|
memberEmails: [],
|
|
749
745
|
accountStatus: null,
|
|
750
|
-
// ARD-6.1: getById() now selects
|
|
751
|
-
typeName: "coding",
|
|
746
|
+
// ARD-6.1: getById() now selects specialty for the runtime config response.
|
|
752
747
|
specialty: "coding",
|
|
753
748
|
selfHosted: false,
|
|
754
749
|
// APM-1.5: getById() now selects these 6 policy fields too.
|
|
@@ -785,8 +780,7 @@ describe("AgentService.getById", () => {
|
|
|
785
780
|
restrictSlackToMembers: false,
|
|
786
781
|
memberEmails: [],
|
|
787
782
|
accountStatus: null,
|
|
788
|
-
// ARD-6.1: getById() now selects
|
|
789
|
-
typeName: "coding",
|
|
783
|
+
// ARD-6.1: getById() now selects specialty for the runtime config response.
|
|
790
784
|
specialty: "coding",
|
|
791
785
|
selfHosted: false,
|
|
792
786
|
// APM-1.5: getById() now selects these 6 policy fields too.
|
|
@@ -1055,7 +1049,6 @@ describe("AgentService.updateFields", () => {
|
|
|
1055
1049
|
reviewAuthorAllowlist: ["octocat"],
|
|
1056
1050
|
patchAuthorAllowlist: [],
|
|
1057
1051
|
restrictSlackToMembers: false,
|
|
1058
|
-
typeName: "coding",
|
|
1059
1052
|
specialty: "coding",
|
|
1060
1053
|
createdAt: row.createdAt,
|
|
1061
1054
|
updatedAt: new Date("2024-01-02"),
|
|
@@ -1151,14 +1144,14 @@ function fakeCreateSpecialtyRegistry(
|
|
|
1151
1144
|
byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST },
|
|
1152
1145
|
): Pick<SpecialtyManifestResolver, "tryGetManifest"> {
|
|
1153
1146
|
return {
|
|
1154
|
-
tryGetManifest: (
|
|
1147
|
+
tryGetManifest: (specialty: string) => byType[specialty],
|
|
1155
1148
|
};
|
|
1156
1149
|
}
|
|
1157
1150
|
|
|
1158
1151
|
interface CreateAgentHarnessCalls {
|
|
1159
1152
|
created: Array<{
|
|
1160
1153
|
name: string;
|
|
1161
|
-
|
|
1154
|
+
specialty?: string;
|
|
1162
1155
|
selfHosted?: boolean;
|
|
1163
1156
|
}>;
|
|
1164
1157
|
deleted: string[];
|
|
@@ -1211,7 +1204,7 @@ function makeCreateAgentHarness(
|
|
|
1211
1204
|
reviewAuthorAllowlist: input.reviewAuthorAllowlist ?? [],
|
|
1212
1205
|
patchAuthorAllowlist: input.patchAuthorAllowlist ?? [],
|
|
1213
1206
|
restrictSlackToMembers: input.restrictSlackToMembers ?? false,
|
|
1214
|
-
|
|
1207
|
+
specialty: input.specialty ?? "coding",
|
|
1215
1208
|
createdAt: new Date("2024-01-01"),
|
|
1216
1209
|
updatedAt: new Date("2024-01-01"),
|
|
1217
1210
|
missingRequiredEnv: [],
|
|
@@ -1219,7 +1212,7 @@ function makeCreateAgentHarness(
|
|
|
1219
1212
|
agents.set(id, detail);
|
|
1220
1213
|
calls.created.push({
|
|
1221
1214
|
name: input.name,
|
|
1222
|
-
|
|
1215
|
+
specialty: input.specialty,
|
|
1223
1216
|
selfHosted: input.selfHosted,
|
|
1224
1217
|
});
|
|
1225
1218
|
return detail;
|
|
@@ -1313,7 +1306,7 @@ function baseCreateAgentInput(
|
|
|
1313
1306
|
): CreateAgentFormInput {
|
|
1314
1307
|
return {
|
|
1315
1308
|
name: "Test Agent",
|
|
1316
|
-
|
|
1309
|
+
specialty: "coding",
|
|
1317
1310
|
runtime: undefined,
|
|
1318
1311
|
reposRaw: undefined,
|
|
1319
1312
|
authorAllowlistRaw: undefined,
|
|
@@ -1434,24 +1427,24 @@ describe("createAgent()", () => {
|
|
|
1434
1427
|
expect(calls.created).toEqual([]);
|
|
1435
1428
|
});
|
|
1436
1429
|
|
|
1437
|
-
it("returns invalid_type and creates nothing when
|
|
1430
|
+
it("returns invalid_type and creates nothing when specialty is unknown", async () => {
|
|
1438
1431
|
const { deps, calls } = makeCreateAgentHarness();
|
|
1439
1432
|
|
|
1440
1433
|
const result = await createAgent(
|
|
1441
1434
|
deps,
|
|
1442
|
-
baseCreateAgentInput({
|
|
1435
|
+
baseCreateAgentInput({ specialty: "nonexistent" }),
|
|
1443
1436
|
);
|
|
1444
1437
|
|
|
1445
1438
|
expect(result).toEqual({ ok: false, errorCode: "invalid_type" });
|
|
1446
1439
|
expect(calls.created).toEqual([]);
|
|
1447
1440
|
});
|
|
1448
1441
|
|
|
1449
|
-
it("returns invalid_type and creates nothing when
|
|
1442
|
+
it("returns invalid_type and creates nothing when specialty is missing", async () => {
|
|
1450
1443
|
const { deps, calls } = makeCreateAgentHarness();
|
|
1451
1444
|
|
|
1452
1445
|
const result = await createAgent(
|
|
1453
1446
|
deps,
|
|
1454
|
-
baseCreateAgentInput({
|
|
1447
|
+
baseCreateAgentInput({ specialty: undefined }),
|
|
1455
1448
|
);
|
|
1456
1449
|
|
|
1457
1450
|
expect(result).toEqual({ ok: false, errorCode: "invalid_type" });
|
package/src/api.smoke.test.ts
CHANGED
|
@@ -39,6 +39,7 @@ interface MockAgent {
|
|
|
39
39
|
restrictSlackToMembers: boolean;
|
|
40
40
|
memberEmails: string[];
|
|
41
41
|
accountStatus?: string | null;
|
|
42
|
+
specialty?: string;
|
|
42
43
|
selfHosted?: boolean;
|
|
43
44
|
// APM-1.5: the 6 agent-policy fields passed through by GET /:id/config.
|
|
44
45
|
autoPostReviews: boolean;
|
|
@@ -94,6 +95,7 @@ function makeMockAgentService(agents: Map<string, MockAgent>): {
|
|
|
94
95
|
restrictSlackToMembers: boolean;
|
|
95
96
|
memberEmails: string[];
|
|
96
97
|
accountStatus?: string | null;
|
|
98
|
+
specialty: string;
|
|
97
99
|
selfHosted?: boolean;
|
|
98
100
|
autoPostReviews: boolean;
|
|
99
101
|
allowSelfReview: boolean;
|
|
@@ -112,6 +114,7 @@ function makeMockAgentService(agents: Map<string, MockAgent>): {
|
|
|
112
114
|
restrictSlackToMembers: boolean;
|
|
113
115
|
memberEmails: string[];
|
|
114
116
|
accountStatus?: string | null;
|
|
117
|
+
specialty: string;
|
|
115
118
|
selfHosted?: boolean;
|
|
116
119
|
autoPostReviews: boolean;
|
|
117
120
|
allowSelfReview: boolean;
|
|
@@ -130,6 +133,7 @@ function makeMockAgentService(agents: Map<string, MockAgent>): {
|
|
|
130
133
|
restrictSlackToMembers: agent.restrictSlackToMembers,
|
|
131
134
|
memberEmails: agent.memberEmails,
|
|
132
135
|
accountStatus: agent.accountStatus,
|
|
136
|
+
specialty: agent.specialty ?? "coding",
|
|
133
137
|
selfHosted: agent.selfHosted,
|
|
134
138
|
autoPostReviews: agent.autoPostReviews,
|
|
135
139
|
allowSelfReview: agent.allowSelfReview,
|
|
@@ -228,6 +232,7 @@ function buildApp(opts?: {
|
|
|
228
232
|
restrictSlackToMembers?: boolean;
|
|
229
233
|
memberEmails?: string[];
|
|
230
234
|
accountStatus?: string | null;
|
|
235
|
+
specialty?: string;
|
|
231
236
|
selfHosted?: boolean;
|
|
232
237
|
anthropicFederationEnabled?: boolean;
|
|
233
238
|
phaseMethodology?: MockPhaseMethodology[];
|
|
@@ -283,6 +288,7 @@ function buildApp(opts?: {
|
|
|
283
288
|
restrictSlackToMembers,
|
|
284
289
|
memberEmails,
|
|
285
290
|
accountStatus: opts?.accountStatus,
|
|
291
|
+
specialty: opts?.specialty,
|
|
286
292
|
selfHosted: opts?.selfHosted,
|
|
287
293
|
autoPostReviews,
|
|
288
294
|
allowSelfReview,
|
|
@@ -332,6 +338,21 @@ describe("GET /:id/config with no phase-methodology service (platform only)", ()
|
|
|
332
338
|
});
|
|
333
339
|
});
|
|
334
340
|
|
|
341
|
+
describe("GET /:id/config deprecated typeName alias (AGR-6.3)", () => {
|
|
342
|
+
// Pre-AGR-6.3 runtime images read only config.typeName to set
|
|
343
|
+
// SHIPWRIGHT_AGENT_TYPE; the alias must mirror specialty until they roll.
|
|
344
|
+
test("typeName equals specialty for a non-coding specialty", async () => {
|
|
345
|
+
const app = buildApp({ specialty: "research" });
|
|
346
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
347
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
348
|
+
});
|
|
349
|
+
expect(res.status).toBe(200);
|
|
350
|
+
const body = await res.json();
|
|
351
|
+
expect(body.specialty).toBe("research");
|
|
352
|
+
expect(body.typeName).toBe("research");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
335
356
|
describe("GET /:id/config features (COS-43.1)", () => {
|
|
336
357
|
const get = (app: ReturnType<typeof buildApp>) =>
|
|
337
358
|
app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
@@ -736,6 +757,7 @@ function buildCombinedApp() {
|
|
|
736
757
|
patchAuthorAllowlist: [],
|
|
737
758
|
restrictSlackToMembers: false,
|
|
738
759
|
memberEmails: [],
|
|
760
|
+
specialty: "coding",
|
|
739
761
|
// APM-1.5: required by AgentConfigResponseSchema.
|
|
740
762
|
autoPostReviews: true,
|
|
741
763
|
allowSelfReview: false,
|
|
@@ -772,7 +794,7 @@ function buildCombinedApp() {
|
|
|
772
794
|
reviewAuthorAllowlist: [],
|
|
773
795
|
patchAuthorAllowlist: [],
|
|
774
796
|
restrictSlackToMembers: false,
|
|
775
|
-
|
|
797
|
+
specialty: "coding",
|
|
776
798
|
createdAt: new Date(),
|
|
777
799
|
updatedAt: new Date(),
|
|
778
800
|
missingRequiredEnv: [],
|
|
@@ -791,7 +813,7 @@ function buildCombinedApp() {
|
|
|
791
813
|
reviewAuthorAllowlist: [],
|
|
792
814
|
patchAuthorAllowlist: [],
|
|
793
815
|
restrictSlackToMembers: false,
|
|
794
|
-
|
|
816
|
+
specialty: "coding",
|
|
795
817
|
createdAt: new Date(),
|
|
796
818
|
updatedAt: new Date(),
|
|
797
819
|
missingRequiredEnv: [],
|
package/src/api.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
|
23
23
|
import type { AgentCronJob } from "./agent-cron-jobs.ts";
|
|
24
24
|
import type { AgentEnvBundle } from "./agent-envs.ts";
|
|
25
25
|
import type { AgentTokenService } from "./agent-tokens.ts";
|
|
26
|
-
import { resolveSpecialty } from "./agent-specialty.ts";
|
|
27
26
|
import { anthropicCredentialKeysIn } from "./anthropic-credential-guard.ts";
|
|
28
27
|
import { type AdminApiKey, createAdminAuthMiddleware } from "./api-auth.ts";
|
|
29
28
|
import {
|
|
@@ -67,13 +66,16 @@ export interface AgentConfigResponse {
|
|
|
67
66
|
*/
|
|
68
67
|
accountStatus?: string | null;
|
|
69
68
|
/**
|
|
70
|
-
* ARD-6.1: the agent's Specialty name (e.g. "coding")
|
|
71
|
-
*
|
|
72
|
-
|
|
69
|
+
* ARD-6.1: the agent's Specialty name (e.g. "coding"), read by the agent
|
|
70
|
+
* runtime to pick its workspace template directory.
|
|
71
|
+
*/
|
|
72
|
+
specialty: string;
|
|
73
|
+
/**
|
|
74
|
+
* @deprecated Back-compat alias of `specialty` for pre-AGR-6.3 runtime
|
|
75
|
+
* images (their entrypoint reads `config.typeName`). Remove once every
|
|
76
|
+
* runtime image reads `specialty`.
|
|
73
77
|
*/
|
|
74
78
|
typeName?: string;
|
|
75
|
-
/** AGR-6.1: specialty (specialty ?? typeName); additive, optional for doubles. */
|
|
76
|
-
specialty?: string;
|
|
77
79
|
/**
|
|
78
80
|
* PMC-1.1: phase -> subagentType (or null) map, covering all six pipeline
|
|
79
81
|
* phases regardless of whether the agent has an explicit override row for
|
|
@@ -136,8 +138,7 @@ interface AgentServiceLike {
|
|
|
136
138
|
trialExpiresAt?: Date | null;
|
|
137
139
|
/** SSP-8.3: owning account status; optional for pre-existing test doubles. */
|
|
138
140
|
accountStatus?: string | null;
|
|
139
|
-
|
|
140
|
-
specialty?: string | null;
|
|
141
|
+
specialty: string;
|
|
141
142
|
/** WIF-2.1: optional so pre-existing test doubles keep compiling; absent is treated as not self-hosted. */
|
|
142
143
|
selfHosted?: boolean;
|
|
143
144
|
/** APM-1.5: the 6 agent-policy fields, direct-passthrough into the response. */
|
|
@@ -349,14 +350,12 @@ export function createAgentRuntimeApp(deps: AgentRuntimeDeps): OpenAPIHono {
|
|
|
349
350
|
? agent.trialExpiresAt.toISOString()
|
|
350
351
|
: null,
|
|
351
352
|
accountStatus: agent.accountStatus ?? null,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
specialty: agent.specialty,
|
|
359
|
-
}),
|
|
353
|
+
specialty: agent.specialty,
|
|
354
|
+
// DEPRECATED (AGR-6.3): back-compat alias for pre-AGR-6.3 runtime images,
|
|
355
|
+
// whose entrypoint only reads `config.typeName` to set
|
|
356
|
+
// SHIPWRIGHT_AGENT_TYPE. Without it those pods silently fall back to
|
|
357
|
+
// coding templates. Remove in a follow-up once all images have rolled.
|
|
358
|
+
typeName: agent.specialty,
|
|
360
359
|
// Omitted entirely when no phase-methodology service was supplied
|
|
361
360
|
// (platform-only admin, no coding extension).
|
|
362
361
|
...(agentPhaseMethodologyService ? { phaseMethodology } : {}),
|
package/src/openapi-schemas.ts
CHANGED
|
@@ -34,7 +34,6 @@ export const AgentSummarySchema = z
|
|
|
34
34
|
id: z.string().openapi({ example: "clx1234567890" }),
|
|
35
35
|
name: z.string().openapi({ example: "Bodhi" }),
|
|
36
36
|
selfHosted: z.boolean().openapi({ example: false }),
|
|
37
|
-
typeName: z.string().openapi({ example: "coding" }),
|
|
38
37
|
specialty: z.string().openapi({ example: "coding" }),
|
|
39
38
|
})
|
|
40
39
|
.openapi("AgentSummary");
|
|
@@ -148,7 +147,7 @@ export const PatchAgentBodySchema = z
|
|
|
148
147
|
export const CreateAgentBodySchema = z
|
|
149
148
|
.object({
|
|
150
149
|
name: z.string().min(1).openapi({ example: "Bodhi" }),
|
|
151
|
-
|
|
150
|
+
specialty: z.string().min(1).openapi({ example: "coding" }),
|
|
152
151
|
/**
|
|
153
152
|
* "in-cluster" means K8s-provisioned; anything else (including absent)
|
|
154
153
|
* means self-hosted.
|
|
@@ -1341,12 +1340,20 @@ export const AgentConfigResponseSchema = z
|
|
|
1341
1340
|
.optional()
|
|
1342
1341
|
.openapi({ example: "active" }),
|
|
1343
1342
|
/**
|
|
1344
|
-
* ARD-6.1: the agent's Specialty name
|
|
1345
|
-
*
|
|
1343
|
+
* ARD-6.1: the agent's Specialty name, used by the runtime to pick its
|
|
1344
|
+
* workspace template directory.
|
|
1346
1345
|
*/
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
|
|
1346
|
+
specialty: z.string().openapi({ example: "coding" }),
|
|
1347
|
+
/**
|
|
1348
|
+
* DEPRECATED back-compat alias of `specialty` (always equal to it). Kept
|
|
1349
|
+
* only for pre-AGR-6.3 runtime images, whose entrypoint reads
|
|
1350
|
+
* `config.typeName` to set SHIPWRIGHT_AGENT_TYPE. Remove in a follow-up
|
|
1351
|
+
* once every runtime image reads `specialty`.
|
|
1352
|
+
*/
|
|
1353
|
+
typeName: z
|
|
1354
|
+
.string()
|
|
1355
|
+
.optional()
|
|
1356
|
+
.openapi({ example: "coding", deprecated: true }),
|
|
1350
1357
|
/**
|
|
1351
1358
|
* PMC-1.1: phase -> subagentType (or null) map, covering all six pipeline
|
|
1352
1359
|
* phases (see AGENT_PHASES) regardless of whether the agent has an
|
|
@@ -140,7 +140,7 @@ function makeService(opts?: {
|
|
|
140
140
|
reviewAuthorAllowlist: [],
|
|
141
141
|
patchAuthorAllowlist: [],
|
|
142
142
|
restrictSlackToMembers: false,
|
|
143
|
-
|
|
143
|
+
specialty: "coding",
|
|
144
144
|
missingRequiredEnv: [],
|
|
145
145
|
})),
|
|
146
146
|
updateFields: async (id: string, input: unknown) => {
|
|
@@ -156,7 +156,7 @@ function makeService(opts?: {
|
|
|
156
156
|
reviewAuthorAllowlist: [],
|
|
157
157
|
patchAuthorAllowlist: [],
|
|
158
158
|
restrictSlackToMembers: false,
|
|
159
|
-
|
|
159
|
+
specialty: "coding",
|
|
160
160
|
missingRequiredEnv: [],
|
|
161
161
|
};
|
|
162
162
|
},
|
|
@@ -242,7 +242,7 @@ function makeMockDeps(
|
|
|
242
242
|
reviewAuthorAllowlist: [],
|
|
243
243
|
patchAuthorAllowlist: [],
|
|
244
244
|
restrictSlackToMembers: false,
|
|
245
|
-
|
|
245
|
+
specialty: "coding",
|
|
246
246
|
missingRequiredEnv: [],
|
|
247
247
|
}),
|
|
248
248
|
updateFields: async (id: string) => ({
|
|
@@ -256,7 +256,7 @@ function makeMockDeps(
|
|
|
256
256
|
reviewAuthorAllowlist: [],
|
|
257
257
|
patchAuthorAllowlist: [],
|
|
258
258
|
restrictSlackToMembers: false,
|
|
259
|
-
|
|
259
|
+
specialty: "coding",
|
|
260
260
|
missingRequiredEnv: [],
|
|
261
261
|
}),
|
|
262
262
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* admin/src/specialty-manifest-loader.ts
|
|
3
3
|
*
|
|
4
|
-
* SpecialtyRegistry — resolves an agent's stored `
|
|
5
|
-
* Specialty manifest by loading `specialties/{
|
|
4
|
+
* SpecialtyRegistry — resolves an agent's stored `specialty` to its parsed
|
|
5
|
+
* Specialty manifest by loading `specialties/{specialty}/manifest.yaml` from
|
|
6
6
|
* disk (relative to the repo root) and validating it via
|
|
7
7
|
* parseSpecialtyManifest().
|
|
8
8
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (and, in time, its default plugins/tools/env contract). It replaces the
|
|
11
11
|
* former hardcoded SYSTEM_CRONS array in admin/src/system-crons.ts.
|
|
12
12
|
*
|
|
13
|
-
* Fallback contract: an unknown `
|
|
13
|
+
* Fallback contract: an unknown `specialty` (a value with no matching
|
|
14
14
|
* directory under specialties/) resolves to the "coding" manifest with a
|
|
15
15
|
* logged warning — never throws. This keeps the boot-path
|
|
16
16
|
* POST /agents/:id/crons/reconcile route from beginning to 5xx for an agent
|
|
@@ -36,13 +36,13 @@ import {
|
|
|
36
36
|
export const DEFAULT_SPECIALTY_NAME = "coding";
|
|
37
37
|
|
|
38
38
|
/** Reads a manifest's raw YAML text for a type; throws if it does not exist. */
|
|
39
|
-
type ManifestReader = (
|
|
39
|
+
type ManifestReader = (specialty: string) => string;
|
|
40
40
|
|
|
41
|
-
/** Sink for the unknown-
|
|
41
|
+
/** Sink for the unknown-specialty warning (defaults to console.warn). */
|
|
42
42
|
type WarnFn = (message: string) => void;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Lists the
|
|
45
|
+
* Lists the name of every discoverable specialty directory (e.g. the
|
|
46
46
|
* names of the subdirectories under specialties/). Used by listTypes() to
|
|
47
47
|
* discover candidates before resolving each one's manifest via `read`.
|
|
48
48
|
*/
|
|
@@ -99,12 +99,12 @@ export function resolveDefaultSpecialtyName(
|
|
|
99
99
|
return env.SHIPWRIGHT_DEFAULT_AGENT_TYPE || DEFAULT_SPECIALTY_NAME;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/** Default reader: loads {typesDir}/{
|
|
102
|
+
/** Default reader: loads {typesDir}/{specialty}/manifest.yaml from disk. */
|
|
103
103
|
export function defaultManifestReader(
|
|
104
|
-
|
|
104
|
+
specialty: string,
|
|
105
105
|
typesDir: string = resolveSpecialtiesDir(),
|
|
106
106
|
): string {
|
|
107
|
-
return readFileSync(join(typesDir,
|
|
107
|
+
return readFileSync(join(typesDir, specialty, "manifest.yaml"), "utf-8");
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/** Default lister: the directory names under the specialties dir on disk. */
|
|
@@ -122,20 +122,20 @@ export function defaultTypeDirLister(
|
|
|
122
122
|
*/
|
|
123
123
|
export interface SpecialtyManifestResolver {
|
|
124
124
|
/**
|
|
125
|
-
* Resolve `
|
|
125
|
+
* Resolve `specialty` to its parsed manifest. Falls back to the "coding"
|
|
126
126
|
* manifest (with a warning) when the type is unknown. Never throws for an
|
|
127
127
|
* unknown type — only if the "coding" fallback itself fails to load.
|
|
128
128
|
*/
|
|
129
|
-
getManifest(
|
|
129
|
+
getManifest(specialty: string): SpecialtyManifest;
|
|
130
130
|
/**
|
|
131
|
-
* Resolve `
|
|
131
|
+
* Resolve `specialty` to its parsed manifest WITHOUT falling back to
|
|
132
132
|
* "coding" — returns undefined for an unknown type instead. No warning is
|
|
133
133
|
* logged (an unknown type here is an expected, callers-handle-it outcome,
|
|
134
134
|
* not a degraded-boot condition). Used by POST /admin/agents to reject an
|
|
135
135
|
* unknown requested `type` with 400 rather than silently seeding a
|
|
136
136
|
* "coding" agent under a different label.
|
|
137
137
|
*/
|
|
138
|
-
tryGetManifest(
|
|
138
|
+
tryGetManifest(specialty: string): SpecialtyManifest | undefined;
|
|
139
139
|
/**
|
|
140
140
|
* Discover every specialty directory (via the injected `list` fn) and
|
|
141
141
|
* resolve each to its {name, displayName} for populating the new-agent
|
|
@@ -178,18 +178,18 @@ export class SpecialtyRegistry implements SpecialtyManifestResolver {
|
|
|
178
178
|
opts.defaultTypeName ?? resolveDefaultSpecialtyName();
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
getManifest(
|
|
182
|
-
const resolved = this.tryLoad(
|
|
181
|
+
getManifest(specialty: string): SpecialtyManifest {
|
|
182
|
+
const resolved = this.tryLoad(specialty);
|
|
183
183
|
if (resolved) return resolved;
|
|
184
184
|
|
|
185
185
|
// Unknown type — warn once and fall back to "coding". The fallback load is
|
|
186
186
|
// deliberately NOT wrapped in tryLoad: if "coding" can't load, that's a
|
|
187
187
|
// hard startup bug and must surface, not silently become a no-op.
|
|
188
188
|
this.warn(
|
|
189
|
-
`SpecialtyRegistry: unknown agent
|
|
189
|
+
`SpecialtyRegistry: unknown agent specialty "${specialty}" — ` +
|
|
190
190
|
`falling back to "${this.defaultTypeName}" manifest`,
|
|
191
191
|
);
|
|
192
|
-
if (
|
|
192
|
+
if (specialty === this.defaultTypeName) {
|
|
193
193
|
// Guard against infinite intent: the default itself failed to load.
|
|
194
194
|
throw missingDefaultSpecialtyError(this.defaultTypeName, this.typesDir);
|
|
195
195
|
}
|
|
@@ -205,19 +205,19 @@ export class SpecialtyRegistry implements SpecialtyManifestResolver {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* Resolve `
|
|
208
|
+
* Resolve `specialty` without falling back to "coding" — see the interface
|
|
209
209
|
* doc comment. Thin public wrapper around the existing tryLoad() used
|
|
210
210
|
* internally by getManifest().
|
|
211
211
|
*/
|
|
212
|
-
tryGetManifest(
|
|
213
|
-
return this.tryLoad(
|
|
212
|
+
tryGetManifest(specialty: string): SpecialtyManifest | undefined {
|
|
213
|
+
return this.tryLoad(specialty);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
listTypes(): SpecialtyOption[] {
|
|
217
|
-
const
|
|
217
|
+
const specialties = this.list();
|
|
218
218
|
const options: SpecialtyOption[] = [];
|
|
219
|
-
for (const
|
|
220
|
-
const manifest = this.tryLoad(
|
|
219
|
+
for (const specialty of specialties) {
|
|
220
|
+
const manifest = this.tryLoad(specialty);
|
|
221
221
|
if (!manifest) continue;
|
|
222
222
|
options.push({
|
|
223
223
|
name: manifest.metadata.name,
|
|
@@ -228,28 +228,28 @@ export class SpecialtyRegistry implements SpecialtyManifestResolver {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/** Load + cache a manifest, returning undefined only if the file is missing. */
|
|
231
|
-
private tryLoad(
|
|
232
|
-
const cached = this.cache.get(
|
|
231
|
+
private tryLoad(specialty: string): SpecialtyManifest | undefined {
|
|
232
|
+
const cached = this.cache.get(specialty);
|
|
233
233
|
if (cached) return cached;
|
|
234
234
|
let raw: string;
|
|
235
235
|
try {
|
|
236
|
-
raw = this.read(
|
|
236
|
+
raw = this.read(specialty);
|
|
237
237
|
} catch {
|
|
238
238
|
return undefined;
|
|
239
239
|
}
|
|
240
|
-
return this.parseAndCache(
|
|
240
|
+
return this.parseAndCache(specialty, raw);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
/** Load + cache a manifest, propagating any read/parse error. */
|
|
244
|
-
private loadOrThrow(
|
|
245
|
-
const cached = this.cache.get(
|
|
244
|
+
private loadOrThrow(specialty: string): SpecialtyManifest {
|
|
245
|
+
const cached = this.cache.get(specialty);
|
|
246
246
|
if (cached) return cached;
|
|
247
|
-
return this.parseAndCache(
|
|
247
|
+
return this.parseAndCache(specialty, this.read(specialty));
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
private parseAndCache(
|
|
250
|
+
private parseAndCache(specialty: string, raw: string): SpecialtyManifest {
|
|
251
251
|
const manifest = parseSpecialtyManifest(raw);
|
|
252
|
-
this.cache.set(
|
|
252
|
+
this.cache.set(specialty, manifest);
|
|
253
253
|
return manifest;
|
|
254
254
|
}
|
|
255
255
|
}
|