@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.
- package/e2e/agents-page.e2e.ts +2 -2
- 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/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/agents-page.e2e.ts
CHANGED
|
@@ -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
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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": {
|
package/openapi.platform.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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.
|
|
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-
|
|
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 {
|
|
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
|
|
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
|
|
187
|
+
new SpecialtyTypes(),
|
|
188
188
|
extensions,
|
|
189
189
|
);
|
|
190
190
|
const agentCronJobService = new AgentCronJobService(
|
package/src/admin-extension.ts
CHANGED
|
@@ -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
|
|
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
|
|
36
|
-
* win over a
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
373
|
-
* registered (or none answering) it returns undefined and the
|
|
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
|
|
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:
|
|
410
|
+
registry: SpecialtyTypeResolver,
|
|
410
411
|
extensions: readonly AdminExtension[],
|
|
411
|
-
):
|
|
412
|
+
): SpecialtyTypeResolver {
|
|
412
413
|
return {
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
594
|
+
getType: () => {
|
|
595
595
|
throw new Error("unused");
|
|
596
596
|
},
|
|
597
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
)
|
|
31
|
-
|
|
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
|
|
package/src/admin-ui-pages.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
|
|
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}">${
|
|
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
|
|
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
|
|
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(
|
|
5603
|
+
expect(seededTools).toEqual([...SPECIALIST_TYPE.tools]);
|
|
5600
5604
|
expect(seededTools).toContain("Bash");
|
|
5601
|
-
expect(seededPlugins).toEqual(
|
|
5602
|
-
});
|
|
5603
|
-
|
|
5604
|
-
it("POST /admin/agents with the
|
|
5605
|
-
const
|
|
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">
|
|
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.
|
|
5668
|
-
expect(seededPlugins).toEqual([
|
|
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
|
|
5672
|
-
const
|
|
5673
|
-
|
|
5674
|
-
|
|
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
|
|
351
|
-
* the registry's
|
|
352
|
-
* Defaults to a disk-backed
|
|
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?:
|
|
356
|
-
/** Extension-supplied specialty defaults, consulted before the
|
|
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
|
|
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
|
-
|
|
225
|
+
getType: () => {
|
|
226
226
|
throw new Error("not implemented");
|
|
227
227
|
},
|
|
228
|
-
|
|
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 {
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
109
|
+
service = new AgentCronJobService(
|
|
110
|
+
prisma,
|
|
111
|
+
undefined,
|
|
112
|
+
fixtureResolver,
|
|
113
|
+
fixtureDefaults,
|
|
114
|
+
);
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
afterEach(async () => {
|