@app-vitals/agent-admin 0.16.2 → 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/e2e/agents-page.e2e.ts +9 -9
- package/e2e/test-server.ts +6 -6
- package/openapi.json +3 -3
- package/openapi.platform.json +3 -3
- package/package.json +3 -5
- package/src/account-agent-create.ts +1 -1
- package/src/admin-boot.ts +6 -6
- package/src/admin-extension.ts +16 -15
- package/src/admin-extension.unit.test.ts +2 -2
- package/src/admin-local-agent.smoke.test.ts +8 -5
- package/src/admin-ui-pages.ts +4 -4
- package/src/admin-ui.smoke.test.ts +27 -39
- package/src/admin-ui.ts +12 -12
- package/src/agent-chat-tokens.smoke.test.ts +2 -2
- package/src/agent-cron-jobs.integration.test.ts +23 -21
- package/src/agent-cron-jobs.ts +30 -40
- package/src/agent-cron-jobs.unit.test.ts +51 -39
- package/src/agent-cron-run-stats.smoke.test.ts +2 -2
- package/src/agent-manifest.ts +2 -6
- package/src/agent-patch-author-allowlist.integration.test.ts +16 -27
- package/src/agents-api.integration.test.ts +20 -31
- package/src/agents-api.sentry.smoke.test.ts +2 -2
- package/src/agents-api.smoke.test.ts +17 -30
- package/src/agents-api.ts +13 -13
- package/src/agents.integration.test.ts +21 -31
- package/src/agents.ts +27 -34
- package/src/agents.unit.test.ts +35 -55
- package/src/api.smoke.test.ts +2 -2
- package/src/api.ts +1 -1
- package/src/chat-service-provisioning-client.integration.test.ts +1 -1
- package/src/server.smoke.test.ts +2 -2
- package/src/specialty-defaults.ts +4 -4
- package/src/specialty-types.ts +92 -0
- package/src/specialty-types.unit.test.ts +67 -0
- package/e2e/fixtures/specialties/coding/manifest.yaml +0 -33
- package/src/coding-task-store-provisioning-client.integration.test.ts +0 -372
- package/src/coding-task-store-provisioning-client.ts +0 -156
- package/src/fixtures/task-store-provisioning-cassette.json +0 -85
- package/src/specialty-manifest-loader.fixture.integration.test.ts +0 -67
- package/src/specialty-manifest-loader.ts +0 -285
- package/src/specialty-manifest-loader.unit.test.ts +0 -364
- package/src/specialty-registry.schema.unit.test.ts +0 -35
- package/src/specialty-registry.ts +0 -236
- package/src/specialty-registry.unit.test.ts +0 -272
- /package/e2e/fixtures/specialties/{coding → specialist}/templates/CLAUDE.md.template +0 -0
- /package/e2e/fixtures/specialties/{coding → specialist}/templates/IDENTITY.md.template +0 -0
- /package/e2e/fixtures/specialties/{coding → specialist}/templates/SOUL.md.template +0 -0
|
@@ -247,10 +247,10 @@ function makeBaseDeps(
|
|
|
247
247
|
listByAgentId: async () => [],
|
|
248
248
|
},
|
|
249
249
|
specialtyRegistry: {
|
|
250
|
-
|
|
250
|
+
getType: () => {
|
|
251
251
|
throw new Error("not implemented");
|
|
252
252
|
},
|
|
253
|
-
|
|
253
|
+
tryGetType: () => undefined,
|
|
254
254
|
listTypes: () => [],
|
|
255
255
|
},
|
|
256
256
|
agentChatTokenService: {
|
|
@@ -22,28 +22,19 @@ import type { AgentTokenService } from "./agent-tokens.ts";
|
|
|
22
22
|
import type { AdminDeps } from "./agents-api.ts";
|
|
23
23
|
import { createAdminApp, parseAdminApiKeys } from "./agents-api.ts";
|
|
24
24
|
import { PLATFORM_OPS_ROUTES } from "./api-auth.ts";
|
|
25
|
-
import type {
|
|
25
|
+
import type { SpecialtyType } from "./specialty-types.ts";
|
|
26
26
|
import { identityCrypto } from "./token-crypto.ts";
|
|
27
27
|
|
|
28
|
-
// ─── Fake
|
|
28
|
+
// ─── Fake SpecialtyTypeResolver ────────────────────────────────────────
|
|
29
29
|
//
|
|
30
30
|
// Mirrors the shape used by agent-cron-jobs.unit.test.ts's fakeRegistry —
|
|
31
|
-
// resolves a fixed set of manifests by specialty, with
|
|
31
|
+
// resolves a fixed set of manifests by specialty, with tryGetType()
|
|
32
32
|
// returning undefined (no fallback) for anything not in the map.
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
name: "coding",
|
|
39
|
-
displayName: "Coding Agent",
|
|
40
|
-
description: "test manifest",
|
|
41
|
-
version: "1.0.0",
|
|
42
|
-
skills: [],
|
|
43
|
-
},
|
|
44
|
-
identity: { templatesDir: "workspace/" },
|
|
45
|
-
crons: [],
|
|
46
|
-
plugins: ["shipwright"],
|
|
34
|
+
const CODING_TYPE: SpecialtyType = {
|
|
35
|
+
name: "coding",
|
|
36
|
+
displayName: "Coding Agent",
|
|
37
|
+
templatesDir: "workspace/",
|
|
47
38
|
tools: [
|
|
48
39
|
"Read",
|
|
49
40
|
"Write",
|
|
@@ -56,32 +47,28 @@ const CODING_MANIFEST: SpecialtyManifest = {
|
|
|
56
47
|
"Skill",
|
|
57
48
|
"Agent",
|
|
58
49
|
],
|
|
59
|
-
|
|
60
|
-
members: [],
|
|
61
|
-
repos: [],
|
|
62
|
-
chat: true,
|
|
63
|
-
voice: true,
|
|
50
|
+
requiredEnv: [],
|
|
64
51
|
};
|
|
65
52
|
|
|
66
|
-
function
|
|
53
|
+
function fakeSpecialtyTypes(
|
|
67
54
|
// The self-serve specialty is registered too: account users are forced to it
|
|
68
55
|
// (the platform test run points it at the fixture specialty, not "coding").
|
|
69
|
-
byType: Record<string,
|
|
70
|
-
coding:
|
|
71
|
-
[getSelfServeSpecialty()]:
|
|
56
|
+
byType: Record<string, SpecialtyType> = {
|
|
57
|
+
coding: CODING_TYPE,
|
|
58
|
+
[getSelfServeSpecialty()]: CODING_TYPE,
|
|
72
59
|
},
|
|
73
60
|
): AdminDeps["specialtyRegistry"] {
|
|
74
61
|
return {
|
|
75
|
-
|
|
76
|
-
return byType[specialty] ?? (byType.coding as
|
|
62
|
+
getType(specialty: string) {
|
|
63
|
+
return byType[specialty] ?? (byType.coding as SpecialtyType);
|
|
77
64
|
},
|
|
78
|
-
|
|
65
|
+
tryGetType(specialty: string) {
|
|
79
66
|
return byType[specialty];
|
|
80
67
|
},
|
|
81
68
|
listTypes() {
|
|
82
69
|
return Object.entries(byType).map(([name, manifest]) => ({
|
|
83
70
|
name,
|
|
84
|
-
displayName: manifest.
|
|
71
|
+
displayName: manifest.displayName,
|
|
85
72
|
}));
|
|
86
73
|
},
|
|
87
74
|
};
|
|
@@ -578,7 +565,7 @@ function makeMockDeps(): AdminDeps {
|
|
|
578
565
|
},
|
|
579
566
|
],
|
|
580
567
|
},
|
|
581
|
-
specialtyRegistry:
|
|
568
|
+
specialtyRegistry: fakeSpecialtyTypes(),
|
|
582
569
|
agentChatTokenService: {
|
|
583
570
|
upsertDailyByModel: async (
|
|
584
571
|
_agentId: string,
|
package/src/agents-api.ts
CHANGED
|
@@ -49,10 +49,6 @@ import {
|
|
|
49
49
|
registerAgentAskRoutes,
|
|
50
50
|
} from "./agent-ask.ts";
|
|
51
51
|
import type { AgentChatTokenService } from "./agent-chat-tokens.ts";
|
|
52
|
-
import {
|
|
53
|
-
type CronRunNowDeps,
|
|
54
|
-
registerCronRunNowRoutes,
|
|
55
|
-
} from "./cron-run-now.ts";
|
|
56
52
|
import type {
|
|
57
53
|
AgentCronJobService,
|
|
58
54
|
AgentCronJobWithRunSummary,
|
|
@@ -93,6 +89,10 @@ import {
|
|
|
93
89
|
memberOnlyCallerScopeResolver,
|
|
94
90
|
} from "./caller-scope.ts";
|
|
95
91
|
import type { ChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
92
|
+
import {
|
|
93
|
+
type CronRunNowDeps,
|
|
94
|
+
registerCronRunNowRoutes,
|
|
95
|
+
} from "./cron-run-now.ts";
|
|
96
96
|
import {
|
|
97
97
|
type CronRunTranscriptDeps,
|
|
98
98
|
registerCronRunTranscriptRoutes,
|
|
@@ -157,7 +157,7 @@ import {
|
|
|
157
157
|
} from "./openapi-schemas.ts";
|
|
158
158
|
import type { SelfServeConfig } from "./self-serve-config.ts";
|
|
159
159
|
import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
|
|
160
|
-
import type {
|
|
160
|
+
import type { SpecialtyTypeResolver } from "./specialty-types.ts";
|
|
161
161
|
import { managedReconcileTargets } from "./startup-policy-reconcile.ts";
|
|
162
162
|
|
|
163
163
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -218,15 +218,15 @@ export interface AdminDeps {
|
|
|
218
218
|
/** Account-aware caller-scope resolver (SSP-2.1); defaults to AgentMember-only. */
|
|
219
219
|
callerScopeResolver?: CallerScopeResolver;
|
|
220
220
|
/**
|
|
221
|
-
* Resolves a specialty name to its
|
|
221
|
+
* Resolves a specialty name to its type record. Consumed
|
|
222
222
|
* by POST /agents (createAgentRoute, APA-2.1) to seed AgentTool/AgentPlugin
|
|
223
|
-
* rows from the resolved
|
|
223
|
+
* rows from the resolved type during agent creation — the same
|
|
224
224
|
* createAgent() call admin-ui.ts's POST /admin/agents form handler makes
|
|
225
225
|
* with its own independently-constructed specialtyRegistry instance.
|
|
226
226
|
* Required (not optional) because the route needs it unconditionally.
|
|
227
227
|
*/
|
|
228
|
-
specialtyRegistry:
|
|
229
|
-
/** Extension-supplied specialty defaults, consulted before the
|
|
228
|
+
specialtyRegistry: SpecialtyTypeResolver;
|
|
229
|
+
/** Extension-supplied specialty defaults, consulted before the built-in type. */
|
|
230
230
|
specialtyDefaults?: SpecialtyDefaultsResolver;
|
|
231
231
|
agentChatTokenService: Pick<
|
|
232
232
|
AgentChatTokenService,
|
|
@@ -421,7 +421,7 @@ const GetAgentResultSchema = z
|
|
|
421
421
|
createdAt: z.string().datetime(),
|
|
422
422
|
updatedAt: z.string().datetime(),
|
|
423
423
|
/**
|
|
424
|
-
* Required env keys declared by the agent's type
|
|
424
|
+
* Required env keys declared by the agent's type that have no
|
|
425
425
|
* corresponding AgentEnv row yet — key names only, never values
|
|
426
426
|
* (secrets_in_logs). Always present; informational only (ATS-4.2).
|
|
427
427
|
*/
|
|
@@ -585,7 +585,7 @@ const createAgentRoute = createRoute({
|
|
|
585
585
|
path: "/agents",
|
|
586
586
|
summary: "Create an agent",
|
|
587
587
|
description:
|
|
588
|
-
'Admin-only. Validates name/specialty, creates the Agent row, seeds AgentTool/AgentPlugin rows from the resolved type
|
|
588
|
+
'Admin-only. Validates name/specialty, creates the Agent row, seeds AgentTool/AgentPlugin rows from the resolved type, 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
589
|
request: {
|
|
590
590
|
body: {
|
|
591
591
|
content: { "application/json": { schema: CreateAgentBodySchema } },
|
|
@@ -606,7 +606,7 @@ const getAgentRoute = createRoute({
|
|
|
606
606
|
path: "/agents/{id}",
|
|
607
607
|
summary: "Get an agent",
|
|
608
608
|
description:
|
|
609
|
-
"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
|
|
609
|
+
"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 with no corresponding AgentEnv row yet (informational only).",
|
|
610
610
|
request: { params: AgentIdParamSchema },
|
|
611
611
|
responses: {
|
|
612
612
|
200: {
|
|
@@ -809,7 +809,7 @@ const reconcileCronsRoute = createRoute({
|
|
|
809
809
|
path: "/agents/{id}/crons/reconcile",
|
|
810
810
|
summary: "Reconcile system crons",
|
|
811
811
|
description:
|
|
812
|
-
"Reconciles the agent's system crons against the cron list
|
|
812
|
+
"Reconciles the agent's system crons against the cron list supplied by the AdminExtension for its specialty. Runs in three passes within a single transaction: create-or-update matched-by-name system crons (preserving IDs so run history survives), link/unlink parent-child cron relationships, and delete orphaned system crons no longer in the defaults. Called automatically on agent startup; returns created/updated/deleted counts.",
|
|
813
813
|
request: { params: AgentIdParamSchema },
|
|
814
814
|
responses: {
|
|
815
815
|
200: {
|
|
@@ -26,11 +26,13 @@ import { AgentMemberService } from "./agent-members.ts";
|
|
|
26
26
|
import { AgentPluginService } from "./agent-plugins.ts";
|
|
27
27
|
import type { AgentProvisioner, ProvisionResult } from "./agent-provisioner.ts";
|
|
28
28
|
import { AgentToolService } from "./agent-tools.ts";
|
|
29
|
-
import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
|
|
30
|
-
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
31
29
|
import type { CreateAgentDeps } from "./agents.ts";
|
|
32
30
|
import { AgentService, createAgent } from "./agents.ts";
|
|
33
31
|
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
32
|
+
import type {
|
|
33
|
+
SpecialtyType,
|
|
34
|
+
SpecialtyTypeResolver,
|
|
35
|
+
} from "./specialty-types.ts";
|
|
34
36
|
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
35
37
|
|
|
36
38
|
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
@@ -42,35 +44,22 @@ function makePrisma(): PrismaClient {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** A minimal valid "coding" manifest, matching agents-api.integration.test.ts's fixture. */
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
name: "coding",
|
|
50
|
-
displayName: "Coding Agent",
|
|
51
|
-
description: "test manifest",
|
|
52
|
-
version: "1.0.0",
|
|
53
|
-
skills: [],
|
|
54
|
-
},
|
|
55
|
-
identity: { templatesDir: "workspace/" },
|
|
56
|
-
crons: [],
|
|
57
|
-
plugins: ["shipwright"],
|
|
47
|
+
const CODING_TYPE: SpecialtyType = {
|
|
48
|
+
name: "coding",
|
|
49
|
+
displayName: "Coding Agent",
|
|
50
|
+
templatesDir: "workspace/",
|
|
58
51
|
tools: ["Read", "Write", "Edit", "Bash"],
|
|
59
|
-
|
|
60
|
-
members: [],
|
|
61
|
-
repos: [],
|
|
62
|
-
chat: true,
|
|
63
|
-
voice: true,
|
|
52
|
+
requiredEnv: [],
|
|
64
53
|
};
|
|
65
54
|
|
|
66
|
-
function
|
|
67
|
-
byType: Record<string,
|
|
68
|
-
):
|
|
55
|
+
function fakeSpecialtyTypes(
|
|
56
|
+
byType: Record<string, SpecialtyType> = { coding: CODING_TYPE },
|
|
57
|
+
): SpecialtyTypeResolver {
|
|
69
58
|
return {
|
|
70
|
-
|
|
71
|
-
return byType[specialty] ?? (byType.coding as
|
|
59
|
+
getType(specialty: string): SpecialtyType {
|
|
60
|
+
return byType[specialty] ?? (byType.coding as SpecialtyType);
|
|
72
61
|
},
|
|
73
|
-
|
|
62
|
+
tryGetType(specialty: string): SpecialtyType | undefined {
|
|
74
63
|
return byType[specialty];
|
|
75
64
|
},
|
|
76
65
|
listTypes() {
|
|
@@ -130,7 +119,7 @@ describeOrSkip("createAgent() — integration (real Postgres)", () => {
|
|
|
130
119
|
|
|
131
120
|
beforeEach(async () => {
|
|
132
121
|
prisma = makePrisma();
|
|
133
|
-
agentService = new AgentService(prisma,
|
|
122
|
+
agentService = new AgentService(prisma, fakeSpecialtyTypes());
|
|
134
123
|
agentToolService = new AgentToolService(prisma);
|
|
135
124
|
agentPluginService = new AgentPluginService(prisma);
|
|
136
125
|
agentMemberService = new AgentMemberService(prisma);
|
|
@@ -141,7 +130,7 @@ describeOrSkip("createAgent() — integration (real Postgres)", () => {
|
|
|
141
130
|
agentCronJobService = new AgentCronJobService(
|
|
142
131
|
prisma,
|
|
143
132
|
undefined,
|
|
144
|
-
|
|
133
|
+
fakeSpecialtyTypes(),
|
|
145
134
|
);
|
|
146
135
|
|
|
147
136
|
// Truncate between tests — this suite shares a DB with every other
|
|
@@ -171,7 +160,8 @@ describeOrSkip("createAgent() — integration (real Postgres)", () => {
|
|
|
171
160
|
agentEnvService,
|
|
172
161
|
agentCronJobService,
|
|
173
162
|
provisioner: makeProvisioner(),
|
|
174
|
-
specialtyRegistry:
|
|
163
|
+
specialtyRegistry: fakeSpecialtyTypes(),
|
|
164
|
+
specialtyDefaults: () => ({ plugins: ["shipwright"] }),
|
|
175
165
|
...overrides,
|
|
176
166
|
};
|
|
177
167
|
}
|
|
@@ -205,7 +195,7 @@ describeOrSkip("createAgent() — integration (real Postgres)", () => {
|
|
|
205
195
|
where: { agentId: result.agent.id },
|
|
206
196
|
});
|
|
207
197
|
expect(tools.map((t) => t.pattern).sort()).toEqual(
|
|
208
|
-
[...
|
|
198
|
+
[...CODING_TYPE.tools].sort(),
|
|
209
199
|
);
|
|
210
200
|
|
|
211
201
|
const plugins = await prisma.agentPlugin.findMany({
|
|
@@ -518,7 +508,7 @@ describeOrSkip("Agent.specialty — final shape (AGR-6.3)", () => {
|
|
|
518
508
|
|
|
519
509
|
beforeEach(() => {
|
|
520
510
|
prisma = makePrisma();
|
|
521
|
-
agentService = new AgentService(prisma,
|
|
511
|
+
agentService = new AgentService(prisma, fakeSpecialtyTypes());
|
|
522
512
|
});
|
|
523
513
|
|
|
524
514
|
afterEach(async () => {
|
package/src/agents.ts
CHANGED
|
@@ -19,13 +19,13 @@ import { AgentMemberService } from "./agent-members.ts";
|
|
|
19
19
|
import type { AgentPluginService } from "./agent-plugins.ts";
|
|
20
20
|
import type { AgentProvisioner } from "./agent-provisioner.ts";
|
|
21
21
|
import type { AgentToolService } from "./agent-tools.ts";
|
|
22
|
-
import {
|
|
23
|
-
type SpecialtyManifestResolver,
|
|
24
|
-
SpecialtyRegistry,
|
|
25
|
-
} from "./specialty-manifest-loader.ts";
|
|
26
|
-
import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
|
|
27
22
|
import { ANTHROPIC_CREDENTIALS_BLOCKED_CODE } from "./anthropic-credential-guard.ts";
|
|
28
23
|
import type { PrismaTransactionClient } from "./prisma-tx.ts";
|
|
24
|
+
import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
|
|
25
|
+
import {
|
|
26
|
+
type SpecialtyTypeResolver,
|
|
27
|
+
SpecialtyTypes,
|
|
28
|
+
} from "./specialty-types.ts";
|
|
29
29
|
|
|
30
30
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@ interface CreateAgentInput {
|
|
|
35
35
|
selfHosted?: boolean;
|
|
36
36
|
/** Specialty name. Omitted means Prisma's column default ("specialist") applies. */
|
|
37
37
|
specialty?: string;
|
|
38
|
-
/** Initial repos[]
|
|
38
|
+
/** Initial repos[] from the request. */
|
|
39
39
|
repos?: string[];
|
|
40
40
|
/** Initial reviewAuthorAllowlist[]. */
|
|
41
41
|
reviewAuthorAllowlist?: string[];
|
|
@@ -108,7 +108,7 @@ export interface AgentDetail extends Partial<AgentAnthropicIdentity> {
|
|
|
108
108
|
createdAt: Date;
|
|
109
109
|
updatedAt: Date;
|
|
110
110
|
/**
|
|
111
|
-
* Required env keys declared by the agent's type
|
|
111
|
+
* Required env keys declared by the agent's type that have no
|
|
112
112
|
* corresponding AgentEnv row yet — key names only, never values
|
|
113
113
|
* (secrets_in_logs). Always present (empty array when the type's required
|
|
114
114
|
* contract is empty or every required key is set). Informational only —
|
|
@@ -302,7 +302,7 @@ const DETAIL_SELECT = {
|
|
|
302
302
|
export class AgentService {
|
|
303
303
|
constructor(
|
|
304
304
|
private prisma: PrismaClient,
|
|
305
|
-
private specialtyRegistry:
|
|
305
|
+
private specialtyRegistry: SpecialtyTypeResolver = new SpecialtyTypes(),
|
|
306
306
|
private agentMemberService: Pick<
|
|
307
307
|
AgentMemberService,
|
|
308
308
|
"listByAgentId"
|
|
@@ -407,11 +407,11 @@ export class AgentService {
|
|
|
407
407
|
}
|
|
408
408
|
|
|
409
409
|
/**
|
|
410
|
-
* The env-contract gap: the type
|
|
410
|
+
* The env-contract gap: the type's required env keys minus the
|
|
411
411
|
* keys already present in this agent's AgentEnv rows (key names only,
|
|
412
412
|
* never values — the AgentEnv query below selects only `key`, never
|
|
413
413
|
* `value`, so decrypted secrets can never reach this computation).
|
|
414
|
-
* Skips the AgentEnv query entirely when the
|
|
414
|
+
* Skips the AgentEnv query entirely when the type declares no required
|
|
415
415
|
* keys. Purely informational: never blocks create or gates provisioning
|
|
416
416
|
* (ATS-4.2).
|
|
417
417
|
*/
|
|
@@ -420,9 +420,7 @@ export class AgentService {
|
|
|
420
420
|
specialty: string,
|
|
421
421
|
client: PrismaTransactionClient = this.prisma,
|
|
422
422
|
): Promise<string[]> {
|
|
423
|
-
const requiredKeys = this.specialtyRegistry
|
|
424
|
-
.getManifest(specialty)
|
|
425
|
-
.env.required.map((entry) => entry.key);
|
|
423
|
+
const requiredKeys = this.specialtyRegistry.getType(specialty).requiredEnv;
|
|
426
424
|
|
|
427
425
|
if (requiredKeys.length === 0) return [];
|
|
428
426
|
|
|
@@ -785,10 +783,10 @@ export interface CreateAgentDeps {
|
|
|
785
783
|
agentCronJobService: Pick<AgentCronJobService, "reconcileSystemCrons">;
|
|
786
784
|
provisioner: Pick<AgentProvisioner, "canProvision" | "provision">;
|
|
787
785
|
specialtyRegistry: Pick<
|
|
788
|
-
|
|
789
|
-
"
|
|
786
|
+
SpecialtyTypeResolver,
|
|
787
|
+
"tryGetType" | "hasExtensionType"
|
|
790
788
|
>;
|
|
791
|
-
/** Extension-supplied tools/plugins, consulted before the
|
|
789
|
+
/** Extension-supplied tools/plugins, consulted before the type's tools floor. */
|
|
792
790
|
specialtyDefaults?: SpecialtyDefaultsResolver;
|
|
793
791
|
/** WIF-2.1: Anthropic federation is enabled; defaults to false. */
|
|
794
792
|
anthropicFederationEnabled?: boolean;
|
|
@@ -839,7 +837,7 @@ type CreateAgentTxOutcome =
|
|
|
839
837
|
|
|
840
838
|
/**
|
|
841
839
|
* Atomically create an agent: validate name/specialty, create the Agent row,
|
|
842
|
-
* seed AgentTool/AgentPlugin from the resolved type
|
|
840
|
+
* seed AgentTool/AgentPlugin from the resolved type, attach
|
|
843
841
|
* repos/allowlists/members, patch Claude credentials, provision Kubernetes
|
|
844
842
|
* resources when requested, and best-effort reconcile system crons.
|
|
845
843
|
*
|
|
@@ -878,18 +876,18 @@ export async function createAgent(
|
|
|
878
876
|
}
|
|
879
877
|
|
|
880
878
|
// Resolve the requested type BEFORE creating any row — an unknown/missing
|
|
881
|
-
// type must fail with zero rows created. The resolved
|
|
882
|
-
// captured (not discarded) so its tools
|
|
879
|
+
// type must fail with zero rows created. The resolved type is
|
|
880
|
+
// captured (not discarded) so its tools can be seeded below.
|
|
883
881
|
const specialty = input.specialty;
|
|
884
|
-
const
|
|
885
|
-
? deps.specialtyRegistry.
|
|
882
|
+
const type = specialty
|
|
883
|
+
? deps.specialtyRegistry.tryGetType(specialty)
|
|
886
884
|
: undefined;
|
|
887
885
|
const extensionDefaults = specialty
|
|
888
886
|
? deps.specialtyDefaults?.(specialty)
|
|
889
887
|
: undefined;
|
|
890
888
|
if (
|
|
891
889
|
!specialty ||
|
|
892
|
-
(!
|
|
890
|
+
(!type &&
|
|
893
891
|
!extensionDefaults &&
|
|
894
892
|
!deps.specialtyRegistry.hasExtensionType?.(specialty))
|
|
895
893
|
) {
|
|
@@ -947,21 +945,16 @@ export async function createAgent(
|
|
|
947
945
|
tx,
|
|
948
946
|
);
|
|
949
947
|
|
|
950
|
-
// Seed AgentTool/AgentPlugin rows from the
|
|
951
|
-
// agent row back on any seeding
|
|
952
|
-
// doesn't collide with a
|
|
953
|
-
// seeded
|
|
954
|
-
//
|
|
955
|
-
// for every specialty that exists today.
|
|
948
|
+
// Seed AgentTool/AgentPlugin rows from the extension's defaults, else
|
|
949
|
+
// the type's tools floor. Roll the agent row back on any seeding
|
|
950
|
+
// failure so a retry with the same name doesn't collide with a
|
|
951
|
+
// half-seeded agent. Members/repos have their own dedicated handling
|
|
952
|
+
// below.
|
|
956
953
|
try {
|
|
957
|
-
for (const pattern of extensionDefaults?.tools ??
|
|
958
|
-
manifest?.tools ??
|
|
959
|
-
[]) {
|
|
954
|
+
for (const pattern of extensionDefaults?.tools ?? type?.tools ?? []) {
|
|
960
955
|
await deps.agentToolService.add(agent.id, pattern, tx);
|
|
961
956
|
}
|
|
962
|
-
for (const pluginName of extensionDefaults?.plugins ??
|
|
963
|
-
manifest?.plugins ??
|
|
964
|
-
[]) {
|
|
957
|
+
for (const pluginName of extensionDefaults?.plugins ?? []) {
|
|
965
958
|
await deps.agentPluginService.add(
|
|
966
959
|
agent.id,
|
|
967
960
|
pluginName,
|
package/src/agents.unit.test.ts
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import { describe, expect, it } from "bun:test";
|
|
9
9
|
import { DEFAULT_AGENT_ENV } from "@app-vitals/agent-lib/default-agent-env";
|
|
10
|
-
import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
|
|
11
|
-
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
12
10
|
import type {
|
|
13
11
|
AgentDetail,
|
|
14
12
|
CreateAgentDeps,
|
|
@@ -17,6 +15,11 @@ import type {
|
|
|
17
15
|
} from "./agents.ts";
|
|
18
16
|
import { AgentService, createAgent, resolveOwnerEmail } from "./agents.ts";
|
|
19
17
|
import type { PrismaTransactionClient } from "./prisma-tx.ts";
|
|
18
|
+
import {
|
|
19
|
+
SPECIALIST_TYPE,
|
|
20
|
+
type SpecialtyType,
|
|
21
|
+
type SpecialtyTypeResolver,
|
|
22
|
+
} from "./specialty-types.ts";
|
|
20
23
|
|
|
21
24
|
// ─── In-memory prisma.agent test double ────────────────────────────────────
|
|
22
25
|
|
|
@@ -255,24 +258,15 @@ type FakePrisma = ReturnType<typeof makeFakePrisma>;
|
|
|
255
258
|
* fakeRegistry() pattern in agent-cron-jobs.unit.test.ts. */
|
|
256
259
|
function fakeRegistry(
|
|
257
260
|
requiredEnvByType: Record<string, string[]>,
|
|
258
|
-
):
|
|
261
|
+
): SpecialtyTypeResolver {
|
|
259
262
|
return {
|
|
260
|
-
|
|
263
|
+
getType(specialty: string): SpecialtyType {
|
|
261
264
|
const required = requiredEnvByType[specialty] ?? [];
|
|
262
|
-
return {
|
|
263
|
-
env: {
|
|
264
|
-
required: required.map((key) => ({
|
|
265
|
-
key,
|
|
266
|
-
description: `${key} description`,
|
|
267
|
-
secret: true,
|
|
268
|
-
})),
|
|
269
|
-
optional: [],
|
|
270
|
-
},
|
|
271
|
-
} as unknown as SpecialtyManifest;
|
|
265
|
+
return { ...SPECIALIST_TYPE, requiredEnv: required };
|
|
272
266
|
},
|
|
273
|
-
|
|
267
|
+
tryGetType(specialty: string): SpecialtyType | undefined {
|
|
274
268
|
if (!(specialty in requiredEnvByType)) return undefined;
|
|
275
|
-
return this.
|
|
269
|
+
return this.getType(specialty);
|
|
276
270
|
},
|
|
277
271
|
listTypes() {
|
|
278
272
|
return Object.keys(requiredEnvByType).map((name) => ({
|
|
@@ -1119,32 +1113,19 @@ describe("AgentService.updateFields", () => {
|
|
|
1119
1113
|
// criterion this whole task is about) is covered separately by
|
|
1120
1114
|
// agents.integration.test.ts against a real DB.
|
|
1121
1115
|
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
name: "coding",
|
|
1127
|
-
displayName: "Coding Agent",
|
|
1128
|
-
description: "test manifest",
|
|
1129
|
-
version: "1.0.0",
|
|
1130
|
-
skills: [],
|
|
1131
|
-
},
|
|
1132
|
-
identity: { templatesDir: "workspace/" },
|
|
1133
|
-
crons: [],
|
|
1134
|
-
plugins: ["shipwright"],
|
|
1116
|
+
const CODING_TYPE: SpecialtyType = {
|
|
1117
|
+
name: "coding",
|
|
1118
|
+
displayName: "Coding Agent",
|
|
1119
|
+
templatesDir: "workspace/",
|
|
1135
1120
|
tools: ["Read", "Write", "Bash"],
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
function fakeCreateSpecialtyRegistry(
|
|
1144
|
-
byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST },
|
|
1145
|
-
): Pick<SpecialtyManifestResolver, "tryGetManifest"> {
|
|
1121
|
+
requiredEnv: [],
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
function fakeCreateSpecialtyTypes(
|
|
1125
|
+
byType: Record<string, SpecialtyType> = { coding: CODING_TYPE },
|
|
1126
|
+
): Pick<SpecialtyTypeResolver, "tryGetType"> {
|
|
1146
1127
|
return {
|
|
1147
|
-
|
|
1128
|
+
tryGetType: (specialty: string) => byType[specialty],
|
|
1148
1129
|
};
|
|
1149
1130
|
}
|
|
1150
1131
|
|
|
@@ -1166,7 +1147,7 @@ interface CreateAgentHarnessCalls {
|
|
|
1166
1147
|
|
|
1167
1148
|
function makeCreateAgentHarness(
|
|
1168
1149
|
opts: {
|
|
1169
|
-
manifests?: Record<string,
|
|
1150
|
+
manifests?: Record<string, SpecialtyType>;
|
|
1170
1151
|
canProvision?: boolean;
|
|
1171
1152
|
provisionImpl?: (agentId: string) => Promise<unknown>;
|
|
1172
1153
|
toolAddImpl?: (agentId: string, pattern: string) => Promise<unknown>;
|
|
@@ -1283,7 +1264,7 @@ function makeCreateAgentHarness(
|
|
|
1283
1264
|
},
|
|
1284
1265
|
};
|
|
1285
1266
|
|
|
1286
|
-
const specialtyRegistry =
|
|
1267
|
+
const specialtyRegistry = fakeCreateSpecialtyTypes(opts.manifests);
|
|
1287
1268
|
|
|
1288
1269
|
return {
|
|
1289
1270
|
deps: {
|
|
@@ -1320,7 +1301,7 @@ function baseCreateAgentInput(
|
|
|
1320
1301
|
}
|
|
1321
1302
|
|
|
1322
1303
|
describe("createAgent() specialtyDefaults hook", () => {
|
|
1323
|
-
it("seeds tools and plugins from the hook instead of the
|
|
1304
|
+
it("seeds tools and plugins from the hook instead of the type", async () => {
|
|
1324
1305
|
const { deps, calls } = makeCreateAgentHarness();
|
|
1325
1306
|
deps.specialtyDefaults = () => ({
|
|
1326
1307
|
tools: ["Grep"],
|
|
@@ -1334,18 +1315,16 @@ describe("createAgent() specialtyDefaults hook", () => {
|
|
|
1334
1315
|
expect(calls.pluginsAdded.map((p) => p.name)).toEqual(["ext-plugin"]);
|
|
1335
1316
|
});
|
|
1336
1317
|
|
|
1337
|
-
it("falls back to the
|
|
1318
|
+
it("falls back to the type's tools floor when the hook returns nothing", async () => {
|
|
1338
1319
|
const { deps, calls } = makeCreateAgentHarness();
|
|
1339
1320
|
deps.specialtyDefaults = () => undefined;
|
|
1340
1321
|
|
|
1341
1322
|
await createAgent(deps, baseCreateAgentInput());
|
|
1342
1323
|
|
|
1343
|
-
expect(calls.toolsAdded.map((t) => t.pattern)).toEqual(
|
|
1344
|
-
|
|
1345
|
-
);
|
|
1346
|
-
expect(calls.pluginsAdded
|
|
1347
|
-
CODING_MANIFEST.plugins,
|
|
1348
|
-
);
|
|
1324
|
+
expect(calls.toolsAdded.map((t) => t.pattern)).toEqual([
|
|
1325
|
+
...CODING_TYPE.tools,
|
|
1326
|
+
]);
|
|
1327
|
+
expect(calls.pluginsAdded).toEqual([]);
|
|
1349
1328
|
});
|
|
1350
1329
|
|
|
1351
1330
|
it("falls back per field when the hook omits tools or plugins", async () => {
|
|
@@ -1354,9 +1333,9 @@ describe("createAgent() specialtyDefaults hook", () => {
|
|
|
1354
1333
|
|
|
1355
1334
|
await createAgent(deps, baseCreateAgentInput());
|
|
1356
1335
|
|
|
1357
|
-
expect(calls.toolsAdded.map((t) => t.pattern)).toEqual(
|
|
1358
|
-
|
|
1359
|
-
);
|
|
1336
|
+
expect(calls.toolsAdded.map((t) => t.pattern)).toEqual([
|
|
1337
|
+
...CODING_TYPE.tools,
|
|
1338
|
+
]);
|
|
1360
1339
|
expect(calls.pluginsAdded.map((p) => p.name)).toEqual(["ext-plugin"]);
|
|
1361
1340
|
});
|
|
1362
1341
|
|
|
@@ -1533,7 +1512,7 @@ describe("createAgent()", () => {
|
|
|
1533
1512
|
expect(calls.created).toEqual([]);
|
|
1534
1513
|
});
|
|
1535
1514
|
|
|
1536
|
-
it("happy path: creates the agent, seeds
|
|
1515
|
+
it("happy path: creates the agent, seeds the type's tools, attaches repos/allowlists/members, and reconciles crons", async () => {
|
|
1537
1516
|
const { deps, calls } = makeCreateAgentHarness({ canProvision: false });
|
|
1538
1517
|
|
|
1539
1518
|
const result = await createAgent(
|
|
@@ -1557,7 +1536,7 @@ describe("createAgent()", () => {
|
|
|
1557
1536
|
"Write",
|
|
1558
1537
|
"Bash",
|
|
1559
1538
|
]);
|
|
1560
|
-
expect(calls.pluginsAdded
|
|
1539
|
+
expect(calls.pluginsAdded).toEqual([]);
|
|
1561
1540
|
expect(calls.updateFieldsCalls).toEqual([
|
|
1562
1541
|
{ id: result.agent.id, fields: { repos: ["org/repo1", "org/repo2"] } },
|
|
1563
1542
|
{
|
|
@@ -1615,6 +1594,7 @@ describe("createAgent()", () => {
|
|
|
1615
1594
|
throw new Error("boom: plugin seeding failed");
|
|
1616
1595
|
},
|
|
1617
1596
|
});
|
|
1597
|
+
deps.specialtyDefaults = () => ({ plugins: ["ext-plugin"] });
|
|
1618
1598
|
|
|
1619
1599
|
const result = await createAgent(deps, baseCreateAgentInput());
|
|
1620
1600
|
|
package/src/api.smoke.test.ts
CHANGED
|
@@ -824,10 +824,10 @@ function buildCombinedApp() {
|
|
|
824
824
|
runTransaction: async (fn) => fn(undefined as never),
|
|
825
825
|
},
|
|
826
826
|
specialtyRegistry: {
|
|
827
|
-
|
|
827
|
+
getType: () => {
|
|
828
828
|
throw new Error("not implemented");
|
|
829
829
|
},
|
|
830
|
-
|
|
830
|
+
tryGetType: () => undefined,
|
|
831
831
|
listTypes: () => [],
|
|
832
832
|
},
|
|
833
833
|
agentEnvService: {
|
package/src/api.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface AgentConfigResponse {
|
|
|
66
66
|
*/
|
|
67
67
|
accountStatus?: string | null;
|
|
68
68
|
/**
|
|
69
|
-
* ARD-6.1: the agent's Specialty name (e.g. "
|
|
69
|
+
* ARD-6.1: the agent's Specialty name (e.g. "specialist"), read by the agent
|
|
70
70
|
* runtime to pick its workspace template directory.
|
|
71
71
|
*/
|
|
72
72
|
specialty: string;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Drives the client through an INJECTED fetchFn that replays canned Responses
|
|
7
7
|
* from a cassette keyed by scenario — no live API server, no global.fetch
|
|
8
8
|
* override, no mock.module(). Mirrors the pattern in
|
|
9
|
-
*
|
|
9
|
+
* the other provisioning-client integration tests.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, expect, it } from "bun:test";
|