@app-vitals/agent-admin 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +126 -0
- 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-api.ts
CHANGED
|
@@ -65,7 +65,6 @@ import type { AgentEnvService } from "./agent-envs.ts";
|
|
|
65
65
|
import type { AgentGitHubInstallationsService } from "./agent-github-installations.ts";
|
|
66
66
|
import type { AgentMemberService } from "./agent-members.ts";
|
|
67
67
|
import type { AgentPluginService } from "./agent-plugins.ts";
|
|
68
|
-
import { resolveSpecialty } from "./agent-specialty.ts";
|
|
69
68
|
import {
|
|
70
69
|
type AgentProvisioner,
|
|
71
70
|
hasPerAgentIdentity,
|
|
@@ -411,8 +410,6 @@ const GetAgentResultSchema = z
|
|
|
411
410
|
reviewAuthorAllowlist: z.array(z.string()),
|
|
412
411
|
patchAuthorAllowlist: z.array(z.string()),
|
|
413
412
|
restrictSlackToMembers: z.boolean(),
|
|
414
|
-
typeName: z.string(),
|
|
415
|
-
/** AGR-6.1: specialty (specialty ?? typeName); additive next to typeName. */
|
|
416
413
|
specialty: z.string(),
|
|
417
414
|
/** COS-49.1: the pod ServiceAccount; null when unset. */
|
|
418
415
|
podServiceAccountName: z.string().nullable(),
|
|
@@ -570,7 +567,7 @@ const listAgentsRoute = createRoute({
|
|
|
570
567
|
path: "/agents",
|
|
571
568
|
summary: "List all agents",
|
|
572
569
|
description:
|
|
573
|
-
"Admin-only. Returns every agent's `id`, `name`, `selfHosted`, and `
|
|
570
|
+
"Admin-only. Returns every agent's `id`, `name`, `selfHosted`, and `specialty` — used for metrics name resolution.",
|
|
574
571
|
responses: {
|
|
575
572
|
200: {
|
|
576
573
|
description: "List of agents",
|
|
@@ -585,7 +582,7 @@ const createAgentRoute = createRoute({
|
|
|
585
582
|
path: "/agents",
|
|
586
583
|
summary: "Create an agent",
|
|
587
584
|
description:
|
|
588
|
-
'Admin-only. Validates name/
|
|
585
|
+
'Admin-only. Validates name/specialty, creates the Agent row, seeds AgentTool/AgentPlugin rows from the resolved type manifest, attaches repos/allowlists/members, patches Claude credentials, and provisions Kubernetes when `runtime` is "in-cluster" — the same createAgent() implementation (APA-1.1) the web UI form at /admin/agents/new uses. Transactional: on any failure zero rows persist, and this route never returns a partial-success 200. Returns the created agent in the same shape as GET /agents/:id.',
|
|
589
586
|
request: {
|
|
590
587
|
body: {
|
|
591
588
|
content: { "application/json": { schema: CreateAgentBodySchema } },
|
|
@@ -606,7 +603,7 @@ const getAgentRoute = createRoute({
|
|
|
606
603
|
path: "/agents/{id}",
|
|
607
604
|
summary: "Get an agent",
|
|
608
605
|
description:
|
|
609
|
-
"Admin-only. Returns the full agent record including `selfHosted`, `repos`, `reviewAuthorAllowlist`, `patchAuthorAllowlist`, `restrictSlackToMembers`, `
|
|
606
|
+
"Admin-only. Returns the full agent record including `selfHosted`, `repos`, `reviewAuthorAllowlist`, `patchAuthorAllowlist`, `restrictSlackToMembers`, `specialty`, `accountId` (null for unassigned agents), and `missingRequiredEnv` — required env keys declared by the agent's type manifest with no corresponding AgentEnv row yet (informational only).",
|
|
610
607
|
request: { params: AgentIdParamSchema },
|
|
611
608
|
responses: {
|
|
612
609
|
200: {
|
|
@@ -623,7 +620,7 @@ const patchAgentRoute = createRoute({
|
|
|
623
620
|
path: "/agents/{id}",
|
|
624
621
|
summary: "Update an agent",
|
|
625
622
|
description:
|
|
626
|
-
"Admin-only. Updates `selfHosted`, `repos`, `reviewAuthorAllowlist`, `patchAuthorAllowlist`, `restrictSlackToMembers`, `slackId`, `trialExpiresAt`, `accountId` (platform admin only — 403 for session members; `null` clears it; reconciles the workload of in-cluster agents; does not move historical task-store rows), and/or `podServiceAccountName` (platform admin only — 403 otherwise; `null` clears it; 409 unless SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED is on; 422 for agents with a per-agent federated identity; reconciles the workload so the pod is recreated under the new ServiceAccount). `
|
|
623
|
+
"Admin-only. Updates `selfHosted`, `repos`, `reviewAuthorAllowlist`, `patchAuthorAllowlist`, `restrictSlackToMembers`, `slackId`, `trialExpiresAt`, `accountId` (platform admin only — 403 for session members; `null` clears it; reconciles the workload of in-cluster agents; does not move historical task-store rows), and/or `podServiceAccountName` (platform admin only — 403 otherwise; `null` clears it; 409 unless SHIPWRIGHT_K8S_AGENT_POD_SERVICE_ACCOUNT_ENABLED is on; 422 for agents with a per-agent federated identity; reconciles the workload so the pod is recreated under the new ServiceAccount). `specialty` and `trialExpiryWarnedAt` are not updatable via this route. `repos` here REPLACES the whole list; for incremental changes use `POST /agents/{id}/repos` (add) and `DELETE /agents/{id}/repos/{owner}/{name}` (remove). Returns the updated agent, including a `warning` field when `restrictSlackToMembers` is set true on an agent with zero members.",
|
|
627
624
|
request: {
|
|
628
625
|
params: AgentIdParamSchema,
|
|
629
626
|
body: {
|
|
@@ -1908,7 +1905,7 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
1908
1905
|
anthropicFederationRuleId: _r,
|
|
1909
1906
|
k8sServiceAccountName: _k,
|
|
1910
1907
|
...summary
|
|
1911
|
-
}) =>
|
|
1908
|
+
}) => summary,
|
|
1912
1909
|
);
|
|
1913
1910
|
if (c.get("isAdmin") === true || email === undefined) {
|
|
1914
1911
|
return c.json(agents, 200);
|
|
@@ -1971,8 +1968,8 @@ export function createAdminApp(deps: AdminDeps): OpenAPIHono<AdminAuthEnv> {
|
|
|
1971
1968
|
},
|
|
1972
1969
|
{
|
|
1973
1970
|
name: body.name,
|
|
1974
|
-
|
|
1975
|
-
accountId !== undefined ? getSelfServeSpecialty() : body.
|
|
1971
|
+
specialty:
|
|
1972
|
+
accountId !== undefined ? getSelfServeSpecialty() : body.specialty,
|
|
1976
1973
|
...(accountId !== undefined ? { accountId } : {}),
|
|
1977
1974
|
// PAI-1.2: owner comes from the authenticated caller only; the body
|
|
1978
1975
|
// schema has no owner field, so a client-supplied one is stripped.
|
|
@@ -2818,8 +2815,7 @@ function serializeAgent(
|
|
|
2818
2815
|
reviewAuthorAllowlist?: string[];
|
|
2819
2816
|
patchAuthorAllowlist?: string[];
|
|
2820
2817
|
restrictSlackToMembers?: boolean;
|
|
2821
|
-
|
|
2822
|
-
specialty?: string | null;
|
|
2818
|
+
specialty: string;
|
|
2823
2819
|
accountId?: string | null;
|
|
2824
2820
|
podServiceAccountName?: string | null;
|
|
2825
2821
|
createdAt: Date;
|
|
@@ -2849,8 +2845,7 @@ function serializeAgent(
|
|
|
2849
2845
|
reviewAuthorAllowlist: agent.reviewAuthorAllowlist ?? [],
|
|
2850
2846
|
patchAuthorAllowlist: agent.patchAuthorAllowlist ?? [],
|
|
2851
2847
|
restrictSlackToMembers: agent.restrictSlackToMembers ?? false,
|
|
2852
|
-
|
|
2853
|
-
specialty: resolveSpecialty(agent),
|
|
2848
|
+
specialty: agent.specialty,
|
|
2854
2849
|
accountId: agent.accountId ?? null,
|
|
2855
2850
|
podServiceAccountName: agent.podServiceAccountName ?? null,
|
|
2856
2851
|
createdAt: agent.createdAt.toISOString(),
|
|
@@ -67,11 +67,11 @@ function fakeSpecialtyRegistry(
|
|
|
67
67
|
byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST },
|
|
68
68
|
): SpecialtyManifestResolver {
|
|
69
69
|
return {
|
|
70
|
-
getManifest(
|
|
71
|
-
return byType[
|
|
70
|
+
getManifest(specialty: string): SpecialtyManifest {
|
|
71
|
+
return byType[specialty] ?? (byType.coding as SpecialtyManifest);
|
|
72
72
|
},
|
|
73
|
-
tryGetManifest(
|
|
74
|
-
return byType[
|
|
73
|
+
tryGetManifest(specialty: string): SpecialtyManifest | undefined {
|
|
74
|
+
return byType[specialty];
|
|
75
75
|
},
|
|
76
76
|
listTypes() {
|
|
77
77
|
return Object.keys(byType).map((name) => ({ name, displayName: name }));
|
|
@@ -106,7 +106,7 @@ function makeProvisioner(
|
|
|
106
106
|
function baseInput(overrides: Partial<Parameters<typeof createAgent>[1]> = {}) {
|
|
107
107
|
return {
|
|
108
108
|
name: "Test Agent",
|
|
109
|
-
|
|
109
|
+
specialty: "coding",
|
|
110
110
|
runtime: undefined,
|
|
111
111
|
reposRaw: undefined,
|
|
112
112
|
authorAllowlistRaw: undefined,
|
|
@@ -511,7 +511,7 @@ describeOrSkip("createAgent() — integration (real Postgres)", () => {
|
|
|
511
511
|
});
|
|
512
512
|
});
|
|
513
513
|
|
|
514
|
-
describeOrSkip("Agent.specialty —
|
|
514
|
+
describeOrSkip("Agent.specialty — final shape (AGR-6.3)", () => {
|
|
515
515
|
let prisma: PrismaClient;
|
|
516
516
|
let agentService: AgentService;
|
|
517
517
|
const createdIds: string[] = [];
|
|
@@ -527,76 +527,63 @@ describeOrSkip("Agent.specialty — dual-write + fallback read (AGR-6.1)", () =>
|
|
|
527
527
|
await prisma.$disconnect();
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
-
it("create()
|
|
530
|
+
it("create() persists specialty and exposes no typeName anywhere", async () => {
|
|
531
531
|
const created = await agentService.create({
|
|
532
|
-
name: "
|
|
533
|
-
|
|
532
|
+
name: "Final Shape",
|
|
533
|
+
specialty: "research",
|
|
534
534
|
});
|
|
535
535
|
createdIds.push(created.id);
|
|
536
536
|
|
|
537
537
|
const row = await prisma.agent.findUniqueOrThrow({
|
|
538
538
|
where: { id: created.id },
|
|
539
539
|
});
|
|
540
|
-
expect(row.
|
|
541
|
-
expect(row
|
|
542
|
-
expect(created.
|
|
543
|
-
expect(created
|
|
540
|
+
expect(row.specialty).toBe("research");
|
|
541
|
+
expect("typeName" in row).toBe(false);
|
|
542
|
+
expect(created.specialty).toBe("research");
|
|
543
|
+
expect("typeName" in created).toBe(false);
|
|
544
|
+
|
|
545
|
+
const columns = await prisma.$queryRaw<{ column_name: string }[]>`
|
|
546
|
+
SELECT column_name FROM information_schema.columns
|
|
547
|
+
WHERE table_name = 'Agent' AND table_schema = current_schema()`;
|
|
548
|
+
const names = columns.map((c) => c.column_name);
|
|
549
|
+
expect(names).toContain("specialty");
|
|
550
|
+
expect(names).not.toContain("typeName");
|
|
544
551
|
});
|
|
545
552
|
|
|
546
|
-
it("
|
|
553
|
+
it("every read path returns the stored specialty", async () => {
|
|
547
554
|
const created = await agentService.create({
|
|
548
|
-
name: "
|
|
549
|
-
|
|
555
|
+
name: "Reads Specialty",
|
|
556
|
+
specialty: "research",
|
|
550
557
|
});
|
|
551
558
|
createdIds.push(created.id);
|
|
552
|
-
await prisma.agent.update({
|
|
553
|
-
where: { id: created.id },
|
|
554
|
-
data: { specialty: "research" },
|
|
555
|
-
});
|
|
556
559
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
+
expect((await agentService.getDetail(created.id))?.specialty).toBe(
|
|
561
|
+
"research",
|
|
562
|
+
);
|
|
560
563
|
expect((await agentService.getSummary(created.id))?.specialty).toBe(
|
|
561
564
|
"research",
|
|
562
565
|
);
|
|
566
|
+
expect((await agentService.getById(created.id))?.specialty).toBe(
|
|
567
|
+
"research",
|
|
568
|
+
);
|
|
569
|
+
expect((await agentService.listByIds([created.id]))[0]?.specialty).toBe(
|
|
570
|
+
"research",
|
|
571
|
+
);
|
|
563
572
|
expect(
|
|
564
573
|
(await agentService.list()).find((a) => a.id === created.id)?.specialty,
|
|
565
574
|
).toBe("research");
|
|
566
575
|
});
|
|
567
576
|
|
|
568
|
-
it("
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
where: { id: created.id },
|
|
576
|
-
data: { specialty: null },
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
const detail = await agentService.getDetail(created.id);
|
|
580
|
-
expect(detail?.specialty).toBe("coding");
|
|
581
|
-
expect((await agentService.getSummary(created.id))?.specialty).toBe(
|
|
582
|
-
"coding",
|
|
583
|
-
);
|
|
584
|
-
expect((await agentService.getById(created.id))?.specialty).toBe("coding");
|
|
585
|
-
expect((await agentService.listByIds([created.id]))[0]?.specialty).toBe(
|
|
586
|
-
"coding",
|
|
587
|
-
);
|
|
588
|
-
});
|
|
577
|
+
it("specialty is required: NULL is rejected and omitting it applies the 'coding' default", async () => {
|
|
578
|
+
await expect(
|
|
579
|
+
(async () =>
|
|
580
|
+
prisma.agent.create({
|
|
581
|
+
data: { name: "Null Specialty", specialty: null as never },
|
|
582
|
+
}))(),
|
|
583
|
+
).rejects.toThrow();
|
|
589
584
|
|
|
590
|
-
|
|
591
|
-
const created = await agentService.create({ name: "No Type" });
|
|
585
|
+
const created = await agentService.create({ name: "No Specialty" });
|
|
592
586
|
createdIds.push(created.id);
|
|
593
|
-
const row = await prisma.agent.findUniqueOrThrow({
|
|
594
|
-
where: { id: created.id },
|
|
595
|
-
});
|
|
596
|
-
// typeName omitted: column default applies and specialty stays NULL,
|
|
597
|
-
// so the fallback read still yields the default.
|
|
598
|
-
expect(row.typeName).toBe("coding");
|
|
599
|
-
expect(row.specialty).toBeNull();
|
|
600
587
|
expect(created.specialty).toBe("coding");
|
|
601
588
|
});
|
|
602
589
|
});
|
package/src/agents.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
SpecialtyRegistry,
|
|
25
25
|
} from "./specialty-manifest-loader.ts";
|
|
26
26
|
import { ANTHROPIC_CREDENTIALS_BLOCKED_CODE } from "./anthropic-credential-guard.ts";
|
|
27
|
-
import { resolveSpecialty, withResolvedSpecialty } from "./agent-specialty.ts";
|
|
28
27
|
import type { PrismaTransactionClient } from "./prisma-tx.ts";
|
|
29
28
|
|
|
30
29
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -34,7 +33,7 @@ interface CreateAgentInput {
|
|
|
34
33
|
slackId?: string | null;
|
|
35
34
|
selfHosted?: boolean;
|
|
36
35
|
/** Specialty name. Omitted means Prisma's column default ("coding") applies. */
|
|
37
|
-
|
|
36
|
+
specialty?: string;
|
|
38
37
|
/** Initial repos[] — the manifest repos merged with any request-supplied repos. */
|
|
39
38
|
repos?: string[];
|
|
40
39
|
/** Initial reviewAuthorAllowlist[]. */
|
|
@@ -54,9 +53,7 @@ export interface AgentRecord {
|
|
|
54
53
|
name: string;
|
|
55
54
|
slackId: string | null;
|
|
56
55
|
selfHosted: boolean;
|
|
57
|
-
|
|
58
|
-
/** AGR-6.1: resolved specialty (specialty ?? typeName). */
|
|
59
|
-
specialty?: string;
|
|
56
|
+
specialty: string;
|
|
60
57
|
createdAt: Date;
|
|
61
58
|
updatedAt: Date;
|
|
62
59
|
/** Owning account (SSP-5.2 admin Account column); null for legacy agents. */
|
|
@@ -84,9 +81,7 @@ export interface AgentSummary extends Partial<AgentAnthropicIdentity> {
|
|
|
84
81
|
id: string;
|
|
85
82
|
name: string;
|
|
86
83
|
selfHosted: boolean;
|
|
87
|
-
|
|
88
|
-
/** AGR-6.1: resolved specialty (specialty ?? typeName). */
|
|
89
|
-
specialty?: string;
|
|
84
|
+
specialty: string;
|
|
90
85
|
/** Owning account; null/absent for platform agents. */
|
|
91
86
|
accountId?: string | null;
|
|
92
87
|
}
|
|
@@ -102,9 +97,7 @@ export interface AgentDetail extends Partial<AgentAnthropicIdentity> {
|
|
|
102
97
|
reviewAuthorAllowlist: string[];
|
|
103
98
|
patchAuthorAllowlist: string[];
|
|
104
99
|
restrictSlackToMembers: boolean;
|
|
105
|
-
|
|
106
|
-
/** AGR-6.1: resolved specialty (specialty ?? typeName). */
|
|
107
|
-
specialty?: string;
|
|
100
|
+
specialty: string;
|
|
108
101
|
/**
|
|
109
102
|
* Owning account, null for unassigned (platform) agents. Optional here so
|
|
110
103
|
* existing test doubles constructing a narrower AgentDetail literal don't
|
|
@@ -209,11 +202,8 @@ interface AgentIdAndRepos {
|
|
|
209
202
|
/**
|
|
210
203
|
* ARD-6.1: the agent's Specialty name, surfaced on the runtime config
|
|
211
204
|
* response so the agent process can pick its workspace template directory.
|
|
212
|
-
* Optional for the same fixture-compat reason as trialExpiresAt.
|
|
213
205
|
*/
|
|
214
|
-
|
|
215
|
-
/** AGR-6.1: resolved specialty (specialty ?? typeName). */
|
|
216
|
-
specialty?: string;
|
|
206
|
+
specialty: string;
|
|
217
207
|
/**
|
|
218
208
|
* APM-1.5: the 6 agent-policy fields, read by the runtime GET /:id/config
|
|
219
209
|
* route for direct passthrough. Required (not optional) since the Prisma
|
|
@@ -263,7 +253,6 @@ const SUMMARY_SELECT = {
|
|
|
263
253
|
id: true,
|
|
264
254
|
name: true,
|
|
265
255
|
selfHosted: true,
|
|
266
|
-
typeName: true,
|
|
267
256
|
specialty: true,
|
|
268
257
|
accountId: true,
|
|
269
258
|
ownerEmail: true,
|
|
@@ -283,7 +272,6 @@ const DETAIL_SELECT = {
|
|
|
283
272
|
reviewAuthorAllowlist: true,
|
|
284
273
|
patchAuthorAllowlist: true,
|
|
285
274
|
restrictSlackToMembers: true,
|
|
286
|
-
typeName: true,
|
|
287
275
|
specialty: true,
|
|
288
276
|
accountId: true,
|
|
289
277
|
createdAt: true,
|
|
@@ -338,8 +326,8 @@ export class AgentService {
|
|
|
338
326
|
name: input.name,
|
|
339
327
|
slackId: input.slackId ?? null,
|
|
340
328
|
selfHosted: input.selfHosted ?? false,
|
|
341
|
-
...(input.
|
|
342
|
-
? {
|
|
329
|
+
...(input.specialty !== undefined
|
|
330
|
+
? { specialty: input.specialty }
|
|
343
331
|
: {}),
|
|
344
332
|
...(input.repos !== undefined ? { repos: input.repos } : {}),
|
|
345
333
|
...(input.reviewAuthorAllowlist !== undefined
|
|
@@ -361,10 +349,10 @@ export class AgentService {
|
|
|
361
349
|
});
|
|
362
350
|
const missingRequiredEnv = await this.computeMissingRequiredEnv(
|
|
363
351
|
row.id,
|
|
364
|
-
|
|
352
|
+
row.specialty,
|
|
365
353
|
client,
|
|
366
354
|
);
|
|
367
|
-
return { ...
|
|
355
|
+
return { ...row, missingRequiredEnv };
|
|
368
356
|
}
|
|
369
357
|
|
|
370
358
|
/**
|
|
@@ -395,18 +383,18 @@ export class AgentService {
|
|
|
395
383
|
}
|
|
396
384
|
|
|
397
385
|
/**
|
|
398
|
-
* List all agents (id + name + selfHosted +
|
|
386
|
+
* List all agents (id + name + selfHosted + specialty), ordered by name asc.
|
|
399
387
|
*/
|
|
400
388
|
async list(): Promise<AgentSummary[]> {
|
|
401
389
|
const rows = await this.prisma.agent.findMany({
|
|
402
390
|
select: SUMMARY_SELECT,
|
|
403
391
|
orderBy: { name: "asc" },
|
|
404
392
|
});
|
|
405
|
-
return rows
|
|
393
|
+
return rows;
|
|
406
394
|
}
|
|
407
395
|
|
|
408
396
|
/**
|
|
409
|
-
* Get {id, name, selfHosted,
|
|
397
|
+
* Get {id, name, selfHosted, specialty} for a single agent. Returns null if
|
|
410
398
|
* not found.
|
|
411
399
|
*/
|
|
412
400
|
async getSummary(id: string): Promise<AgentSummary | null> {
|
|
@@ -414,7 +402,7 @@ export class AgentService {
|
|
|
414
402
|
where: { id },
|
|
415
403
|
select: SUMMARY_SELECT,
|
|
416
404
|
});
|
|
417
|
-
return row
|
|
405
|
+
return row;
|
|
418
406
|
}
|
|
419
407
|
|
|
420
408
|
/**
|
|
@@ -428,11 +416,11 @@ export class AgentService {
|
|
|
428
416
|
*/
|
|
429
417
|
private async computeMissingRequiredEnv(
|
|
430
418
|
agentId: string,
|
|
431
|
-
|
|
419
|
+
specialty: string,
|
|
432
420
|
client: PrismaTransactionClient = this.prisma,
|
|
433
421
|
): Promise<string[]> {
|
|
434
422
|
const requiredKeys = this.specialtyRegistry
|
|
435
|
-
.getManifest(
|
|
423
|
+
.getManifest(specialty)
|
|
436
424
|
.env.required.map((entry) => entry.key);
|
|
437
425
|
|
|
438
426
|
if (requiredKeys.length === 0) return [];
|
|
@@ -447,7 +435,7 @@ export class AgentService {
|
|
|
447
435
|
}
|
|
448
436
|
|
|
449
437
|
/**
|
|
450
|
-
* Get the full agent record (incl. repos/
|
|
438
|
+
* Get the full agent record (incl. repos/specialty/timestamps), plus
|
|
451
439
|
* `missingRequiredEnv` (see computeMissingRequiredEnv). Returns null if not
|
|
452
440
|
* found.
|
|
453
441
|
*/
|
|
@@ -460,9 +448,9 @@ export class AgentService {
|
|
|
460
448
|
|
|
461
449
|
const missingRequiredEnv = await this.computeMissingRequiredEnv(
|
|
462
450
|
id,
|
|
463
|
-
|
|
451
|
+
row.specialty,
|
|
464
452
|
);
|
|
465
|
-
return { ...
|
|
453
|
+
return { ...row, missingRequiredEnv };
|
|
466
454
|
}
|
|
467
455
|
|
|
468
456
|
/**
|
|
@@ -531,9 +519,9 @@ export class AgentService {
|
|
|
531
519
|
});
|
|
532
520
|
const missingRequiredEnv = await this.computeMissingRequiredEnv(
|
|
533
521
|
id,
|
|
534
|
-
|
|
522
|
+
row.specialty,
|
|
535
523
|
);
|
|
536
|
-
return { ...
|
|
524
|
+
return { ...row, missingRequiredEnv };
|
|
537
525
|
}
|
|
538
526
|
|
|
539
527
|
/**
|
|
@@ -553,7 +541,6 @@ export class AgentService {
|
|
|
553
541
|
restrictSlackToMembers: true,
|
|
554
542
|
trialExpiresAt: true,
|
|
555
543
|
selfHosted: true,
|
|
556
|
-
typeName: true,
|
|
557
544
|
specialty: true,
|
|
558
545
|
account: { select: { status: true } },
|
|
559
546
|
autoPostReviews: true,
|
|
@@ -570,7 +557,7 @@ export class AgentService {
|
|
|
570
557
|
const memberEmails = members.map((m) => m.email);
|
|
571
558
|
const { account, ...rest } = row;
|
|
572
559
|
return {
|
|
573
|
-
...
|
|
560
|
+
...rest,
|
|
574
561
|
memberEmails,
|
|
575
562
|
accountStatus: account?.status ?? null,
|
|
576
563
|
};
|
|
@@ -583,7 +570,7 @@ export class AgentService {
|
|
|
583
570
|
* order Prisma returns.
|
|
584
571
|
*/
|
|
585
572
|
async listAll(): Promise<AgentRecord[]> {
|
|
586
|
-
return
|
|
573
|
+
return await this.prisma.agent.findMany();
|
|
587
574
|
}
|
|
588
575
|
|
|
589
576
|
/**
|
|
@@ -595,7 +582,7 @@ export class AgentService {
|
|
|
595
582
|
const rows = await this.prisma.agent.findMany({
|
|
596
583
|
where: { id: { in: ids } },
|
|
597
584
|
});
|
|
598
|
-
return rows
|
|
585
|
+
return rows;
|
|
599
586
|
}
|
|
600
587
|
|
|
601
588
|
/**
|
|
@@ -606,7 +593,7 @@ export class AgentService {
|
|
|
606
593
|
const rows = await this.prisma.agent.findMany({
|
|
607
594
|
where: { name: { contains: query, mode: "insensitive" } },
|
|
608
595
|
});
|
|
609
|
-
return rows
|
|
596
|
+
return rows;
|
|
610
597
|
}
|
|
611
598
|
|
|
612
599
|
/**
|
|
@@ -677,10 +664,10 @@ export class AgentService {
|
|
|
677
664
|
});
|
|
678
665
|
const missingRequiredEnv = await this.computeMissingRequiredEnv(
|
|
679
666
|
id,
|
|
680
|
-
|
|
667
|
+
row.specialty,
|
|
681
668
|
client,
|
|
682
669
|
);
|
|
683
|
-
return { ...
|
|
670
|
+
return { ...row, missingRequiredEnv };
|
|
684
671
|
}
|
|
685
672
|
}
|
|
686
673
|
|
|
@@ -696,7 +683,7 @@ export class AgentService {
|
|
|
696
683
|
*/
|
|
697
684
|
export interface CreateAgentFormInput {
|
|
698
685
|
name: string | undefined;
|
|
699
|
-
|
|
686
|
+
specialty: string | undefined;
|
|
700
687
|
/**
|
|
701
688
|
* "in-cluster" means K8s-provisioned; anything else (including absent)
|
|
702
689
|
* means self-hosted — matches the pre-APA-1.1 handler's fallback rule.
|
|
@@ -845,7 +832,7 @@ type CreateAgentTxOutcome =
|
|
|
845
832
|
| { ok: false; errorCode: CreateAgentErrorCode; errorMessage?: string };
|
|
846
833
|
|
|
847
834
|
/**
|
|
848
|
-
* Atomically create an agent: validate name/
|
|
835
|
+
* Atomically create an agent: validate name/specialty, create the Agent row,
|
|
849
836
|
* seed AgentTool/AgentPlugin from the resolved type manifest, attach
|
|
850
837
|
* repos/allowlists/members, patch Claude credentials, provision Kubernetes
|
|
851
838
|
* resources when requested, and best-effort reconcile system crons.
|
|
@@ -887,11 +874,11 @@ export async function createAgent(
|
|
|
887
874
|
// Resolve the requested type BEFORE creating any row — an unknown/missing
|
|
888
875
|
// type must fail with zero rows created. The resolved manifest is
|
|
889
876
|
// captured (not discarded) so its tools/plugins can be seeded below.
|
|
890
|
-
const
|
|
891
|
-
const manifest =
|
|
892
|
-
? deps.specialtyRegistry.tryGetManifest(
|
|
877
|
+
const specialty = input.specialty;
|
|
878
|
+
const manifest = specialty
|
|
879
|
+
? deps.specialtyRegistry.tryGetManifest(specialty)
|
|
893
880
|
: undefined;
|
|
894
|
-
if (!
|
|
881
|
+
if (!specialty || !manifest) {
|
|
895
882
|
return { ok: false, errorCode: "invalid_type" };
|
|
896
883
|
}
|
|
897
884
|
|
|
@@ -936,7 +923,7 @@ export async function createAgent(
|
|
|
936
923
|
{
|
|
937
924
|
name,
|
|
938
925
|
selfHosted: !inCluster,
|
|
939
|
-
|
|
926
|
+
specialty,
|
|
940
927
|
restrictSlackToMembers,
|
|
941
928
|
...(input.accountId !== undefined
|
|
942
929
|
? { accountId: input.accountId }
|