@app-vitals/agent-admin 0.16.2 → 1.0.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.
Files changed (42) hide show
  1. package/e2e/agents-page.e2e.ts +2 -2
  2. package/openapi.json +3 -3
  3. package/openapi.platform.json +3 -3
  4. package/package.json +3 -5
  5. package/src/account-agent-create.ts +1 -1
  6. package/src/admin-boot.ts +6 -6
  7. package/src/admin-extension.ts +16 -15
  8. package/src/admin-extension.unit.test.ts +2 -2
  9. package/src/admin-local-agent.smoke.test.ts +8 -5
  10. package/src/admin-ui-pages.ts +4 -4
  11. package/src/admin-ui.smoke.test.ts +27 -39
  12. package/src/admin-ui.ts +12 -12
  13. package/src/agent-chat-tokens.smoke.test.ts +2 -2
  14. package/src/agent-cron-jobs.integration.test.ts +23 -21
  15. package/src/agent-cron-jobs.ts +30 -40
  16. package/src/agent-cron-jobs.unit.test.ts +51 -39
  17. package/src/agent-cron-run-stats.smoke.test.ts +2 -2
  18. package/src/agent-manifest.ts +2 -6
  19. package/src/agent-patch-author-allowlist.integration.test.ts +16 -27
  20. package/src/agents-api.integration.test.ts +20 -31
  21. package/src/agents-api.sentry.smoke.test.ts +2 -2
  22. package/src/agents-api.smoke.test.ts +17 -30
  23. package/src/agents-api.ts +13 -13
  24. package/src/agents.integration.test.ts +21 -31
  25. package/src/agents.ts +27 -34
  26. package/src/agents.unit.test.ts +35 -55
  27. package/src/api.smoke.test.ts +2 -2
  28. package/src/chat-service-provisioning-client.integration.test.ts +1 -1
  29. package/src/server.smoke.test.ts +2 -2
  30. package/src/specialty-defaults.ts +4 -4
  31. package/src/specialty-types.ts +92 -0
  32. package/src/specialty-types.unit.test.ts +67 -0
  33. package/e2e/fixtures/specialties/coding/manifest.yaml +0 -33
  34. package/src/coding-task-store-provisioning-client.integration.test.ts +0 -372
  35. package/src/coding-task-store-provisioning-client.ts +0 -156
  36. package/src/fixtures/task-store-provisioning-cassette.json +0 -85
  37. package/src/specialty-manifest-loader.fixture.integration.test.ts +0 -67
  38. package/src/specialty-manifest-loader.ts +0 -285
  39. package/src/specialty-manifest-loader.unit.test.ts +0 -364
  40. package/src/specialty-registry.schema.unit.test.ts +0 -35
  41. package/src/specialty-registry.ts +0 -236
  42. package/src/specialty-registry.unit.test.ts +0 -272
@@ -304,7 +304,7 @@ test.describe("GET /admin/agents/new — type picker", () => {
304
304
  await expect(typeSelect).toHaveAttribute("required", "");
305
305
  });
306
306
 
307
- test('the select lists "Coding Agent" (value coding)', async ({
307
+ test('the select lists the configured default type "coding"', async ({
308
308
  page,
309
309
  context,
310
310
  }) => {
@@ -313,7 +313,7 @@ test.describe("GET /admin/agents/new — type picker", () => {
313
313
  'select[name="type"] option[value="coding"]',
314
314
  );
315
315
  await expect(codingOption).toBeAttached();
316
- await expect(codingOption).toHaveText("Coding Agent");
316
+ await expect(codingOption).toHaveText("coding");
317
317
  });
318
318
  });
319
319
 
package/openapi.json CHANGED
@@ -3521,7 +3521,7 @@
3521
3521
  "/agents/{id}": {
3522
3522
  "get": {
3523
3523
  "summary": "Get an agent",
3524
- "description": "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).",
3524
+ "description": "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).",
3525
3525
  "parameters": [
3526
3526
  {
3527
3527
  "schema": {
@@ -3742,7 +3742,7 @@
3742
3742
  },
3743
3743
  "post": {
3744
3744
  "summary": "Create an agent",
3745
- "description": "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.",
3745
+ "description": "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.",
3746
3746
  "requestBody": {
3747
3747
  "content": {
3748
3748
  "application/json": {
@@ -4047,7 +4047,7 @@
4047
4047
  "/agents/{id}/crons/reconcile": {
4048
4048
  "post": {
4049
4049
  "summary": "Reconcile system crons",
4050
- "description": "Reconciles the agent's system crons against the cron list declared by its Specialty manifest. 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 manifest. Called automatically on agent startup; returns created/updated/deleted counts.",
4050
+ "description": "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.",
4051
4051
  "parameters": [
4052
4052
  {
4053
4053
  "schema": {
@@ -3349,7 +3349,7 @@
3349
3349
  "/agents/{id}": {
3350
3350
  "get": {
3351
3351
  "summary": "Get an agent",
3352
- "description": "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).",
3352
+ "description": "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).",
3353
3353
  "parameters": [
3354
3354
  {
3355
3355
  "schema": {
@@ -3705,7 +3705,7 @@
3705
3705
  },
3706
3706
  "post": {
3707
3707
  "summary": "Create an agent",
3708
- "description": "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.",
3708
+ "description": "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.",
3709
3709
  "requestBody": {
3710
3710
  "content": {
3711
3711
  "application/json": {
@@ -4010,7 +4010,7 @@
4010
4010
  "/agents/{id}/crons/reconcile": {
4011
4011
  "post": {
4012
4012
  "summary": "Reconcile system crons",
4013
- "description": "Reconciles the agent's system crons against the cron list declared by its Specialty manifest. 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 manifest. Called automatically on agent startup; returns created/updated/deleted counts.",
4013
+ "description": "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.",
4014
4014
  "parameters": [
4015
4015
  {
4016
4016
  "schema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-admin",
3
- "version": "0.16.2",
3
+ "version": "1.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/app-vitals/agent.git",
@@ -21,8 +21,7 @@
21
21
  "./main": "./src/main.ts",
22
22
  "./openapi-schemas": "./src/openapi-schemas.ts",
23
23
  "./prisma-client": "./src/prisma-client.ts",
24
- "./specialty-manifest-loader": "./src/specialty-manifest-loader.ts",
25
- "./specialty-registry": "./src/specialty-registry.ts",
24
+ "./specialty-types": "./src/specialty-types.ts",
26
25
  "./theme-tokens.json": "./src/theme-tokens.json",
27
26
  "./*": "./src/*.ts",
28
27
  "./prisma/client/*": "./prisma/client/*",
@@ -43,7 +42,7 @@
43
42
  },
44
43
  "dependencies": {
45
44
  "@anthropic-ai/sdk": "0.135.0",
46
- "@app-vitals/agent-lib": "0.16.2",
45
+ "@app-vitals/agent-lib": "1.0.0",
47
46
  "@hono/zod-openapi": "^1.0.0",
48
47
  "@prisma/adapter-pg": "^7.10.0",
49
48
  "@prisma/client": "^7.10.0",
@@ -52,7 +51,6 @@
52
51
  "@slack/web-api": "^8.1.0",
53
52
  "hono": "^4.13.1",
54
53
  "pg": "^8.16.3",
55
- "yaml": "^2.9.0",
56
54
  "zod": "^4"
57
55
  },
58
56
  "devDependencies": {
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { CallerScope } from "./caller-scope.ts";
12
- import { resolveDefaultSpecialtyName } from "./specialty-manifest-loader.ts";
12
+ import { resolveDefaultSpecialtyName } from "./specialty-types.ts";
13
13
 
14
14
  /**
15
15
  * The only specialty account users may create: SHIPWRIGHT_SELF_SERVE_AGENT_TYPE
package/src/admin-boot.ts CHANGED
@@ -74,9 +74,6 @@ import {
74
74
  import { createAdminUIApp } from "./admin-ui.ts";
75
75
  import { AgentAskService } from "./agent-ask.ts";
76
76
  import { parseAgentAskConfig } from "./agent-ask-config.ts";
77
- import { CronRunNowService } from "./cron-run-now.ts";
78
- import { parseCronRunNowConfig } from "./cron-run-now-config.ts";
79
- import { parsePodServiceAccountConfig } from "./pod-service-account-config.ts";
80
77
  import { AgentChatTokenService } from "./agent-chat-tokens.ts";
81
78
  import { AgentCronJobService } from "./agent-cron-jobs.ts";
82
79
  import { AgentCronRunStatsService } from "./agent-cron-run-stats.ts";
@@ -97,6 +94,8 @@ import {
97
94
  } from "./caller-scope.ts";
98
95
  import { parseChatCredentialConfig } from "./chat-credential-config.ts";
99
96
  import { HttpChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
97
+ import { CronRunNowService } from "./cron-run-now.ts";
98
+ import { parseCronRunNowConfig } from "./cron-run-now-config.ts";
100
99
  import { CronRunTranscriptService } from "./cron-run-transcripts.ts";
101
100
  import { isDevAuthAllowed } from "./dev-auth-guard.ts";
102
101
  import { HttpGithubAppProvisioningClient } from "./github-app-provisioning-client.ts";
@@ -104,13 +103,14 @@ import { HttpGoogleAuthClient } from "./google-auth-client.ts";
104
103
  import { HttpChatClient } from "./http-chat-client.ts";
105
104
  import { HttpOktaAuthClient } from "./okta-auth-client.ts";
106
105
  import { mountPlatformDoc } from "./platform-doc-route.ts";
106
+ import { parsePodServiceAccountConfig } from "./pod-service-account-config.ts";
107
107
  import { createAdminPrismaClient } from "./prisma-client.ts";
108
108
  import { isPushEnabled } from "./push-sender.ts";
109
109
  import { PushService } from "./push-service.ts";
110
110
  import { parseRepoAccessConfig } from "./repo-access-config.ts";
111
111
  import { parseSelfServeConfig } from "./self-serve-config.ts";
112
112
  import { HttpSlackProvisioningClient } from "./slack-provisioning-client.ts";
113
- import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
113
+ import { SpecialtyTypes } from "./specialty-types.ts";
114
114
  import { runStartupPolicyReconcile } from "./startup-policy-reconcile.ts";
115
115
  import { makeTokenCrypto } from "./token-crypto.ts";
116
116
  import { parseTranscriptConfig } from "./transcript-config.ts";
@@ -181,10 +181,10 @@ export async function startAdmin(
181
181
  anthropicFederationEnabled,
182
182
  );
183
183
  // Extension-supplied specialty defaults (crons/tools/plugins), consulted by
184
- // the cron reconcile and create-time seeding before the manifest.
184
+ // the cron reconcile and create-time seeding before the built-in type.
185
185
  const specialtyDefaults = collectSpecialtyDefaults(extensions);
186
186
  const specialtyRegistry = withExtensionAgentTypes(
187
- new SpecialtyRegistry(),
187
+ new SpecialtyTypes(),
188
188
  extensions,
189
189
  );
190
190
  const agentCronJobService = new AgentCronJobService(
@@ -28,12 +28,12 @@
28
28
  * serviceIntegrations it needs no context and is
29
29
  * collected before the services are built; the cron
30
30
  * reconcile and create-time seeding consult it
31
- * before the specialty manifest, which remains the
32
- * fallback when no extension returns anything
31
+ * before the built-in specialist type, which remains
32
+ * the fallback when no extension returns anything
33
33
  * agentTypes() — agent types this extension offers in the new-agent
34
34
  * picker and accepts in POST /admin/agents, for
35
- * types with no manifest on disk. Extension entries
36
- * win over a manifest of the same name; a type's
35
+ * types beyond the built-in specialist. Extension
36
+ * entries win over a built-in of the same name; a type's
37
37
  * crons still come from specialtyDefaults(name)
38
38
  *
39
39
  * openApi() is static (no context): scripts/generate-admin-spec.ts calls it to
@@ -68,9 +68,9 @@ import type {
68
68
  SpecialtyDefaultsResolver,
69
69
  } from "./specialty-defaults.ts";
70
70
  import type {
71
- SpecialtyManifestResolver,
72
71
  SpecialtyOption,
73
- } from "./specialty-manifest-loader.ts";
72
+ SpecialtyTypeResolver,
73
+ } from "./specialty-types.ts";
74
74
 
75
75
  /** The platform services an extension may build on. */
76
76
  export interface AdminPlatformServices {
@@ -187,9 +187,9 @@ export interface AdminExtension {
187
187
  name?: string;
188
188
  /** See the header: env-only, resolved before the provisioner is built. */
189
189
  serviceIntegrations?(env: NodeJS.ProcessEnv): ServiceIntegration[];
190
- /** See the header: undefined means "no opinion" — the manifest applies. */
190
+ /** See the header: undefined means "no opinion" — the built-in type applies. */
191
191
  specialtyDefaults?(specialty: string): SpecialtyDefaults | undefined;
192
- /** See the header: additive; merged over the manifest-discovered types. */
192
+ /** See the header: additive; merged over the built-in types. */
193
193
  agentTypes?(): SpecialtyOption[];
194
194
  services?(ctx: AdminExtensionContext): AdminExtensionServices;
195
195
  apiRoutes?(ctx: AdminExtensionContext): AdminRouteContribution[];
@@ -369,8 +369,9 @@ export function collectServiceIntegrations(
369
369
 
370
370
  /**
371
371
  * Builds the resolver the cron reconcile and create-time seeding consult
372
- * before the manifest. The first extension returning defaults wins; with none
373
- * registered (or none answering) it returns undefined and the manifest applies.
372
+ * before the built-in type. The first extension returning defaults wins; with none
373
+ * registered (or none answering) it returns undefined and the built-in type's
374
+ * tools floor applies.
374
375
  */
375
376
  export function collectSpecialtyDefaults(
376
377
  extensions: readonly AdminExtension[],
@@ -385,7 +386,7 @@ export function collectSpecialtyDefaults(
385
386
  }
386
387
 
387
388
  /**
388
- * Merges every extension's agentTypes() over the manifest-discovered `base`
389
+ * Merges every extension's agentTypes() over the built-in `base`
389
390
  * list. Order is base first, then new extension names; on a name collision the
390
391
  * extension's entry replaces the base entry in place.
391
392
  */
@@ -406,12 +407,12 @@ export function mergeAgentTypes(
406
407
  * the wrapped registry behaves exactly like `registry`.
407
408
  */
408
409
  export function withExtensionAgentTypes(
409
- registry: SpecialtyManifestResolver,
410
+ registry: SpecialtyTypeResolver,
410
411
  extensions: readonly AdminExtension[],
411
- ): SpecialtyManifestResolver {
412
+ ): SpecialtyTypeResolver {
412
413
  return {
413
- getManifest: (specialty) => registry.getManifest(specialty),
414
- tryGetManifest: (specialty) => registry.tryGetManifest(specialty),
414
+ getType: (specialty) => registry.getType(specialty),
415
+ tryGetType: (specialty) => registry.tryGetType(specialty),
415
416
  listTypes: () => mergeAgentTypes(registry.listTypes(), extensions),
416
417
  hasExtensionType: (specialty) =>
417
418
  mergeAgentTypes([], extensions).some((type) => type.name === specialty),
@@ -591,10 +591,10 @@ describe("mergeAgentTypes", () => {
591
591
 
592
592
  describe("withExtensionAgentTypes", () => {
593
593
  const registry = {
594
- getManifest: () => {
594
+ getType: () => {
595
595
  throw new Error("unused");
596
596
  },
597
- tryGetManifest: () => undefined,
597
+ tryGetType: () => undefined,
598
598
  listTypes: () => [{ name: "specialist", displayName: "Specialist" }],
599
599
  };
600
600
 
@@ -20,15 +20,18 @@ import type {
20
20
  AdminUISlackClient,
21
21
  } from "./admin-ui.ts";
22
22
  import { createAdminUIApp } from "./admin-ui.ts";
23
- import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
24
23
  import type { GoogleAuthClient } from "./google-auth-client.ts";
24
+ import {
25
+ resolveDefaultSpecialtyName,
26
+ SpecialtyTypes,
27
+ } from "./specialty-types.ts";
25
28
 
26
29
  // The configured default specialty (the fixture under the platform test run,
27
30
  // "coding" in the monorepo).
28
- const TYPE_META = new SpecialtyRegistry().getManifest(
29
- process.env.SHIPWRIGHT_DEFAULT_AGENT_TYPE || "coding",
30
- ).metadata;
31
- const TYPE = TYPE_META.name;
31
+ const TYPE = resolveDefaultSpecialtyName();
32
+ const TYPE_META = new SpecialtyTypes()
33
+ .listTypes()
34
+ .find((t) => t.name === TYPE) as { name: string; displayName: string };
32
35
 
33
36
  // ─── Constants ────────────────────────────────────────────────────────────────
34
37
 
@@ -28,7 +28,7 @@ import type {
28
28
  ThreadStats,
29
29
  } from "./http-chat-client.ts";
30
30
  import { renderPushToggle } from "./push-toggle.ts";
31
- import type { SpecialtyOption } from "./specialty-manifest-loader.ts";
31
+ import type { SpecialtyOption } from "./specialty-types.ts";
32
32
 
33
33
  // ─── Helpers ──────────────────────────────────────────────────────────────────
34
34
 
@@ -83,7 +83,7 @@ export interface AgentDetail {
83
83
  /** The agent's type name (e.g. "specialist"), resolved at creation time. */
84
84
  specialty: string;
85
85
  /**
86
- * Required env keys declared by the agent's type manifest with no
86
+ * Required env keys declared by the agent's type with no
87
87
  * corresponding AgentEnv row yet — key names only, never values. Always
88
88
  * present; purely informational (ATS-4.2), see renderAgentDetailPage's Env
89
89
  * Vars card.
@@ -2351,7 +2351,7 @@ const STALL_INDICATOR_CLASS = "chat-stall-indicator";
2351
2351
  * hard stop.
2352
2352
  */
2353
2353
  export const STALL_WARN_AFTER_MS = 120_000;
2354
- // 65min — mirrors lib/claim-ttl.ts's DEFAULT_CLAIM_TTL_MS
2354
+ // 65min — mirrors DEFAULT_CLAIM_TTL_MS (Shipwright claim-ttl)
2355
2355
  // (DEFAULT_CLAUDE_TIMEOUT_MS 1hr + CLAIM_TTL_BUFFER_MS 5min). Inline JS can't
2356
2356
  // import the lib file, so the value is hardcoded here with this reference.
2357
2357
  export const ABSOLUTE_MAX_MS = 3_900_000;
@@ -2886,7 +2886,7 @@ export function renderChatThreadPage(
2886
2886
 
2887
2887
  // Stall-warning threshold — 120s in prod; e2e overrides to a tiny value.
2888
2888
  var STALL_WARN_AFTER_MS = ${stallWarnAfterMs};
2889
- // Hard ceiling: 65min, mirrors lib/claim-ttl.ts DEFAULT_CLAIM_TTL_MS
2889
+ // Hard ceiling: 65min, mirrors Shipwright's DEFAULT_CLAIM_TTL_MS
2890
2890
  // (1hr DEFAULT_CLAUDE_TIMEOUT_MS + 5min CLAIM_TTL_BUFFER_MS). Only hard stop.
2891
2891
  var ABSOLUTE_MAX_MS = ${ABSOLUTE_MAX_MS};
2892
2892
 
@@ -12,8 +12,6 @@ import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { sign } from "hono/jwt";
14
14
  import { withExtensionAgentTypes } from "./admin-extension.ts";
15
- import { createAdminUIApp } from "./admin-ui.ts";
16
- import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
17
15
  import {
18
16
  AGENT_ID,
19
17
  CRON_ID,
@@ -28,16 +26,22 @@ import {
28
26
  TOKEN_ID,
29
27
  TOOL_ID,
30
28
  } from "./admin-ui.fixtures.ts";
29
+ import { createAdminUIApp } from "./admin-ui.ts";
31
30
  import { AnthropicCredentialsBlockedError } from "./anthropic-credential-guard.ts";
32
31
  import type { PushService } from "./push-service.ts";
32
+ import {
33
+ resolveDefaultSpecialtyName,
34
+ SPECIALIST_TYPE,
35
+ SpecialtyTypes,
36
+ } from "./specialty-types.ts";
33
37
 
34
38
  // The specialty the create-agent flows use: the configured default type (the
35
39
  // platform test run points it at the fixture specialty; the monorepo falls
36
40
  // back to "coding").
37
- const TYPE_MANIFEST = new SpecialtyRegistry().getManifest(
38
- process.env.SHIPWRIGHT_DEFAULT_AGENT_TYPE || "coding",
39
- );
40
- const TYPE = TYPE_MANIFEST.metadata.name;
41
+ const TYPE = resolveDefaultSpecialtyName();
42
+ const TYPE_RECORD = new SpecialtyTypes()
43
+ .listTypes()
44
+ .find((t) => t.name === TYPE) as { name: string; displayName: string };
41
45
 
42
46
  // ─── Auth redirect tests ──────────────────────────────────────────────────────
43
47
 
@@ -1290,7 +1294,7 @@ describe("admin UI — authenticated pages", () => {
1290
1294
  '<select id="type" name="type" class="form-input" required>',
1291
1295
  );
1292
1296
  expect(html).toContain(
1293
- `<option value="${TYPE}">${TYPE_MANIFEST.metadata.displayName}</option>`,
1297
+ `<option value="${TYPE}">${TYPE_RECORD.displayName}</option>`,
1294
1298
  );
1295
1299
  });
1296
1300
 
@@ -5551,14 +5555,14 @@ describe("admin UI — create agent with patch author allowlist", () => {
5551
5555
  });
5552
5556
  });
5553
5557
 
5554
- describe("admin UI — create agent seeds manifest tools and plugins", () => {
5558
+ describe("admin UI — create agent seeds type tools and extension plugins", () => {
5555
5559
  let cookie: string;
5556
5560
 
5557
5561
  beforeAll(async () => {
5558
5562
  cookie = await makeSessionCookie();
5559
5563
  });
5560
5564
 
5561
- it("POST /admin/agents with the default type seeds an AgentTool row for every manifest tool and an AgentPlugin row for every manifest plugin", async () => {
5565
+ it("POST /admin/agents with the default type seeds an AgentTool row for every type tool and no AgentPlugin rows", async () => {
5562
5566
  const seededTools: string[] = [];
5563
5567
  const seededPlugins: string[] = [];
5564
5568
  const deps = makeMockDeps();
@@ -5596,28 +5600,13 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5596
5600
  });
5597
5601
  expect(res.status).toBe(302);
5598
5602
  expect(res.headers.get("Location")).toBe(`/admin/agents/${AGENT_ID}`);
5599
- expect(seededTools).toEqual(TYPE_MANIFEST.tools);
5603
+ expect(seededTools).toEqual([...SPECIALIST_TYPE.tools]);
5600
5604
  expect(seededTools).toContain("Bash");
5601
- expect(seededPlugins).toEqual(TYPE_MANIFEST.plugins);
5602
- });
5603
-
5604
- it("POST /admin/agents with the fixture specialty (AGR-1.9) seeds from its manifest and needs no product specialty", async () => {
5605
- const fixtureDir = [
5606
- join(
5607
- import.meta.dir,
5608
- "..",
5609
- "..",
5610
- "agent",
5611
- "src",
5612
- "fixtures",
5613
- "specialties",
5614
- ),
5615
- join(import.meta.dir, "..", "..", "runtime", "fixtures", "specialties"),
5616
- ].find((dir) => existsSync(dir)) as string;
5617
- const registry = new SpecialtyRegistry({
5618
- typesDir: fixtureDir,
5619
- defaultTypeName: "fixture",
5620
- });
5605
+ expect(seededPlugins).toEqual([]);
5606
+ });
5607
+
5608
+ it("POST /admin/agents with the configured default type (AGR-1.9) seeds the specialist floor and needs no product specialty", async () => {
5609
+ const registry = new SpecialtyTypes({ defaultTypeName: "fixture" });
5621
5610
  const seededTools: string[] = [];
5622
5611
  const seededPlugins: string[] = [];
5623
5612
  const deps = makeMockDeps();
@@ -5649,7 +5638,7 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5649
5638
  headers: { Cookie: `admin_session=${cookie}` },
5650
5639
  });
5651
5640
  expect(await form.text()).toContain(
5652
- '<option value="fixture">Fixture Agent</option>',
5641
+ '<option value="fixture">fixture</option>',
5653
5642
  );
5654
5643
  const res = await app.request("/admin/agents", {
5655
5644
  method: "POST",
@@ -5664,16 +5653,14 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5664
5653
  });
5665
5654
  expect(res.status).toBe(302);
5666
5655
  expect(res.headers.get("Location")).toBe(`/admin/agents/${AGENT_ID}`);
5667
- expect(seededTools).toEqual(registry.getManifest("fixture").tools);
5668
- expect(seededPlugins).toEqual(["fixture-plugin"]);
5656
+ expect(seededTools).toEqual([...registry.getType("fixture").tools]);
5657
+ expect(seededPlugins).toEqual([]);
5669
5658
  });
5670
5659
 
5671
- it("an extension-contributed type (ETP-1.2) shows in the picker and POST /admin/agents accepts it with no manifest", async () => {
5672
- const emptyDir = mkdtempSync(join(tmpdir(), "no-specialties-"));
5673
- const registry = withExtensionAgentTypes(
5674
- new SpecialtyRegistry({ typesDir: emptyDir }),
5675
- [{ agentTypes: () => [{ name: "coding", displayName: "Coding Agent" }] }],
5676
- );
5660
+ it("an extension-contributed type (ETP-1.2) shows in the picker and POST /admin/agents accepts it", async () => {
5661
+ const registry = withExtensionAgentTypes(new SpecialtyTypes(), [
5662
+ { agentTypes: () => [{ name: "coding", displayName: "Coding Agent" }] },
5663
+ ]);
5677
5664
  const deps = makeMockDeps();
5678
5665
  deps.specialtyRegistry = registry;
5679
5666
  const app = createAdminUIApp(deps);
@@ -5733,6 +5720,7 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5733
5720
  it("POST /admin/agents rolls back the agent and redirects with error when plugin seeding fails", async () => {
5734
5721
  let deletedId: string | undefined;
5735
5722
  const deps = makeMockDeps();
5723
+ deps.specialtyDefaults = () => ({ plugins: ["ext-plugin"] });
5736
5724
  deps.agentPluginService = {
5737
5725
  ...deps.agentPluginService,
5738
5726
  add: async () => {
package/src/admin-ui.ts CHANGED
@@ -74,11 +74,6 @@ import type { AgentPluginService } from "./agent-plugins.ts";
74
74
  import type { AgentProvisioner } from "./agent-provisioner.ts";
75
75
  import type { AgentTokenService } from "./agent-tokens.ts";
76
76
  import type { AgentToolService } from "./agent-tools.ts";
77
- import {
78
- type SpecialtyManifestResolver,
79
- SpecialtyRegistry,
80
- } from "./specialty-manifest-loader.ts";
81
- import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
82
77
  import type { AgentService } from "./agents.ts";
83
78
  import { createAgent, describeRepoNameCollision } from "./agents.ts";
84
79
  import { AnthropicCredentialsBlockedError } from "./anthropic-credential-guard.ts";
@@ -86,7 +81,6 @@ import {
86
81
  audioContentTypeForFilename,
87
82
  validateAttachment,
88
83
  } from "./attachment-validation.ts";
89
- import { parseByteRange } from "./http-range.ts";
90
84
  import {
91
85
  type CallerScopeResolver,
92
86
  memberOnlyCallerScopeResolver,
@@ -105,6 +99,7 @@ import {
105
99
  filterSince,
106
100
  toPolledMessage,
107
101
  } from "./http-chat-client.ts";
102
+ import { parseByteRange } from "./http-range.ts";
108
103
  import type { OktaAuthClient } from "./okta-auth-client.ts";
109
104
  import type { PrismaTransactionClient } from "./prisma-tx.ts";
110
105
  import type { PushService } from "./push-service.ts";
@@ -132,6 +127,11 @@ import {
132
127
  PROVISION_STATE_TTL_SECONDS,
133
128
  SlackProvisioningService,
134
129
  } from "./slack-provisioning-service.ts";
130
+ import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
131
+ import {
132
+ type SpecialtyTypeResolver,
133
+ SpecialtyTypes,
134
+ } from "./specialty-types.ts";
135
135
 
136
136
  export type AdminUIEnv = { Variables: { userEmail: string; isAdmin: boolean } };
137
137
 
@@ -347,13 +347,13 @@ export interface AdminUIDeps {
347
347
  /** WIF-2.1: Anthropic federation is enabled; defaults to false. */
348
348
  anthropicFederationEnabled?: boolean;
349
349
  /**
350
- * Resolves an agent's specialty to its parsed Specialty manifest and lists
351
- * the registry's discoverable types (for the new-specialty picker).
352
- * Defaults to a disk-backed SpecialtyRegistry, matching the DI pattern
350
+ * Resolves an agent's specialty to its type record and lists
351
+ * the registry's built-in types (for the new-specialty picker).
352
+ * Defaults to a disk-backed SpecialtyTypes, matching the DI pattern
353
353
  * already used by agents-api.ts's AdminDeps.
354
354
  */
355
- specialtyRegistry?: SpecialtyManifestResolver;
356
- /** Extension-supplied specialty defaults, consulted before the manifest. */
355
+ specialtyRegistry?: SpecialtyTypeResolver;
356
+ /** Extension-supplied specialty defaults, consulted before the built-in type. */
357
357
  specialtyDefaults?: SpecialtyDefaultsResolver;
358
358
  /**
359
359
  * Cleanup deps for the full delete-agent orchestration (POST
@@ -784,7 +784,7 @@ export function createAdminUIApp(deps: AdminUIDeps): Hono<AdminUIEnv> {
784
784
  accountOnboarding,
785
785
  accountCreatedNotifier,
786
786
  accountServices,
787
- specialtyRegistry = new SpecialtyRegistry(),
787
+ specialtyRegistry = new SpecialtyTypes(),
788
788
  specialtyDefaults,
789
789
  callerScopeResolver = memberOnlyCallerScopeResolver(agentMemberService),
790
790
  serviceIntegrations,
@@ -222,10 +222,10 @@ function makeMockDeps(opts?: {
222
222
  listByAgentId: async () => [],
223
223
  },
224
224
  specialtyRegistry: {
225
- getManifest: () => {
225
+ getType: () => {
226
226
  throw new Error("not implemented");
227
227
  },
228
- tryGetManifest: () => undefined,
228
+ tryGetType: () => undefined,
229
229
  listTypes: () => [],
230
230
  },
231
231
  agentChatTokenService: {
@@ -18,8 +18,14 @@ import {
18
18
  import { AgentCronJobService } from "./agent-cron-jobs.ts";
19
19
  import { NotFoundError, UnprocessableEntityError } from "./errors.ts";
20
20
  import { createAdminPrismaClient } from "./prisma-client.ts";
21
- import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
22
- import type { SpecialtyManifest } from "./specialty-registry.ts";
21
+ import type {
22
+ SpecialtyDefaultCron,
23
+ SpecialtyDefaultsResolver,
24
+ } from "./specialty-defaults.ts";
25
+ import {
26
+ SPECIALIST_TYPE,
27
+ type SpecialtyTypeResolver,
28
+ } from "./specialty-types.ts";
23
29
 
24
30
  const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
25
31
 
@@ -34,9 +40,7 @@ function makePrisma(): PrismaClient {
34
40
  // cron, covering every field reconcile maps into a row.
35
41
  const FIXTURE_TYPE = "fixture";
36
42
 
37
- type ManifestCron = SpecialtyManifest["crons"][number];
38
-
39
- const FIXTURE_CRONS: ManifestCron[] = [
43
+ const FIXTURE_CRONS: SpecialtyDefaultCron[] = [
40
44
  {
41
45
  name: "fixture-loop",
42
46
  schedule: "*/30 * * * *",
@@ -70,23 +74,16 @@ const FIXTURE_CRONS: ManifestCron[] = [
70
74
  },
71
75
  ];
72
76
 
73
- const fixtureManifest = {
74
- crons: FIXTURE_CRONS,
75
- } as unknown as SpecialtyManifest;
77
+ /** The extension hook supplying FIXTURE_TYPE's crons. */
78
+ const fixtureDefaults: SpecialtyDefaultsResolver = (specialty) =>
79
+ specialty === FIXTURE_TYPE ? { crons: FIXTURE_CRONS } : undefined;
76
80
 
77
81
  /** Resolves only FIXTURE_TYPE; every other type is unknown. */
78
- const fixtureResolver: SpecialtyManifestResolver = {
79
- getManifest(specialty: string): SpecialtyManifest {
80
- if (specialty !== FIXTURE_TYPE)
81
- throw new Error(`no manifest: ${specialty}`);
82
- return fixtureManifest;
83
- },
84
- tryGetManifest(specialty: string): SpecialtyManifest | undefined {
85
- return specialty === FIXTURE_TYPE ? fixtureManifest : undefined;
86
- },
87
- listTypes() {
88
- return [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }];
89
- },
82
+ const fixtureResolver: SpecialtyTypeResolver = {
83
+ getType: () => SPECIALIST_TYPE,
84
+ tryGetType: (specialty) =>
85
+ specialty === FIXTURE_TYPE ? SPECIALIST_TYPE : undefined,
86
+ listTypes: () => [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }],
90
87
  };
91
88
 
92
89
  async function createAgent(
@@ -109,7 +106,12 @@ describeOrSkip("AgentCronJobService (integration)", () => {
109
106
  await prisma.agentTool.deleteMany();
110
107
  await prisma.agentEnv.deleteMany();
111
108
  await prisma.agent.deleteMany();
112
- service = new AgentCronJobService(prisma, undefined, fixtureResolver);
109
+ service = new AgentCronJobService(
110
+ prisma,
111
+ undefined,
112
+ fixtureResolver,
113
+ fixtureDefaults,
114
+ );
113
115
  });
114
116
 
115
117
  afterEach(async () => {