@app-vitals/agent-admin 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +62 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/specialty-registry.ts
|
|
3
|
+
*
|
|
4
|
+
* Specialty manifest schema — the single source of truth for the
|
|
5
|
+
* `shipwright.dev/v1alpha1` / `Specialty` YAML manifest that declares an
|
|
6
|
+
* specialty's identity, crons, plugins, tools, env contract, membership,
|
|
7
|
+
* repos, and chat/voice toggles.
|
|
8
|
+
*
|
|
9
|
+
* Named "specialty-registry" (not "manifest") to avoid colliding with the
|
|
10
|
+
* two existing "manifest" concepts in this repo: the Kubernetes manifests in
|
|
11
|
+
* ./agent-manifest.ts and the Slack app manifest. This module is unrelated
|
|
12
|
+
* to either — it's a declarative spec fed into scripts/CLI tooling, not a
|
|
13
|
+
* wire type, so it uses plain `zod` rather than `@hono/zod-openapi`'s
|
|
14
|
+
* `.openapi()`-augmented `z` (kept simple for JSON Schema conversion).
|
|
15
|
+
*
|
|
16
|
+
* Manifests are treated as a trust boundary: they may originate from
|
|
17
|
+
* third-party contributors, so every object schema is `.strict()` (unknown
|
|
18
|
+
* keys rejected, not silently stripped) and the env contract is
|
|
19
|
+
* deliberately keys-only — no `value`/`default` field is permitted on an
|
|
20
|
+
* env entry, so a manifest can declare *that* a var is needed without ever
|
|
21
|
+
* carrying a real value.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { isOrgRepo } from "@app-vitals/agent-lib/org-repo";
|
|
25
|
+
import { parse as parseYaml } from "yaml";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
|
|
28
|
+
/** RFC1123 label pattern (see admin/src/agent-manifest.ts's sanitizeAgentName). */
|
|
29
|
+
const RFC1123_NAME = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
|
|
30
|
+
|
|
31
|
+
// ─── metadata.skills[] (A2A-aligned) ───────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimal A2A (Agent2Agent) "skill" shape: what the specialty can do, as a
|
|
35
|
+
* small catalog entry. Kept intentionally narrow — id/name/description plus
|
|
36
|
+
* optional free-form tags — since this repo has no existing A2A convention
|
|
37
|
+
* to align with beyond the general shape of an agent capability card.
|
|
38
|
+
*/
|
|
39
|
+
const SpecialtySkillSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
name: z.string().min(1),
|
|
43
|
+
description: z.string().min(1),
|
|
44
|
+
tags: z.array(z.string()).optional(),
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
47
|
+
|
|
48
|
+
// ─── metadata ───────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const SpecialtyMetadataSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
name: z
|
|
53
|
+
.string()
|
|
54
|
+
.regex(RFC1123_NAME, "metadata.name must be a valid RFC1123 label"),
|
|
55
|
+
displayName: z.string().min(1),
|
|
56
|
+
description: z.string().min(1),
|
|
57
|
+
version: z.string().min(1),
|
|
58
|
+
skills: z.array(SpecialtySkillSchema),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
|
|
62
|
+
// ─── identity ───────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const SpecialtyIdentitySchema = z
|
|
65
|
+
.object({
|
|
66
|
+
templatesDir: z.string().min(1),
|
|
67
|
+
})
|
|
68
|
+
.strict();
|
|
69
|
+
|
|
70
|
+
// ─── crons[] ────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A single cron template declared inside a Specialty manifest. Distinct
|
|
74
|
+
* from the runtime, DB-backed AgentCronJobSchema (admin/src/openapi-schemas.ts)
|
|
75
|
+
* and CreateAgentCronJobInput (admin/src/agent-cron-jobs.ts) — those describe
|
|
76
|
+
* per-agent-instance rows already created in Postgres; this describes a
|
|
77
|
+
* *template* inside a versioned spec. `parentCron` here is a name reference
|
|
78
|
+
* resolved within this manifest's own crons[] array (validated by the
|
|
79
|
+
* top-level schema's superRefine below), not a DB id reference.
|
|
80
|
+
*/
|
|
81
|
+
const SpecialtyCronSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
name: z.string().min(1),
|
|
84
|
+
schedule: z.string().min(1),
|
|
85
|
+
prompt: z.string().min(1),
|
|
86
|
+
preCheck: z.string().optional(),
|
|
87
|
+
silent: z.boolean().optional().default(false),
|
|
88
|
+
enabled: z.boolean().optional().default(true),
|
|
89
|
+
parentCron: z.string().optional(),
|
|
90
|
+
})
|
|
91
|
+
.strict();
|
|
92
|
+
|
|
93
|
+
// ─── env contract ───────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A single env contract entry. Keys-only by design: a manifest declares that
|
|
97
|
+
* a var is needed (and whether it's secret) — it never carries a `value` or
|
|
98
|
+
* `default`. `.strict()` ensures an unexpected `value` key is rejected, not
|
|
99
|
+
* silently stripped, since manifests are potential third-party input.
|
|
100
|
+
*/
|
|
101
|
+
const SpecialtyEnvEntrySchema = z
|
|
102
|
+
.object({
|
|
103
|
+
key: z.string().min(1),
|
|
104
|
+
description: z.string().min(1),
|
|
105
|
+
secret: z.boolean(),
|
|
106
|
+
})
|
|
107
|
+
.strict();
|
|
108
|
+
|
|
109
|
+
const SpecialtyEnvSchema = z
|
|
110
|
+
.object({
|
|
111
|
+
required: z.array(SpecialtyEnvEntrySchema),
|
|
112
|
+
optional: z.array(SpecialtyEnvEntrySchema),
|
|
113
|
+
})
|
|
114
|
+
.strict();
|
|
115
|
+
|
|
116
|
+
// ─── resources override (optional) ─────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Optional override of the default agent container resources (see
|
|
120
|
+
* resolveAgentContainerResources in admin/src/agent-manifest.ts). All fields
|
|
121
|
+
* optional — a manifest may override just one of requests/limits.
|
|
122
|
+
*
|
|
123
|
+
* Not yet wired to any provisioner — parsed as part of
|
|
124
|
+
* SpecialtyManifestSchema.resources but inert until a future task consumes it
|
|
125
|
+
* ahead of resolveAgentContainerResources's env-override layer (see that
|
|
126
|
+
* function's precedence-rule doc comment).
|
|
127
|
+
*/
|
|
128
|
+
const SpecialtyResourceListSchema = z
|
|
129
|
+
.object({
|
|
130
|
+
cpu: z.string().optional(),
|
|
131
|
+
memory: z.string().optional(),
|
|
132
|
+
"ephemeral-storage": z.string().optional(),
|
|
133
|
+
})
|
|
134
|
+
.strict();
|
|
135
|
+
|
|
136
|
+
const SpecialtyResourcesSchema = z
|
|
137
|
+
.object({
|
|
138
|
+
requests: SpecialtyResourceListSchema.optional(),
|
|
139
|
+
limits: SpecialtyResourceListSchema.optional(),
|
|
140
|
+
})
|
|
141
|
+
.strict();
|
|
142
|
+
|
|
143
|
+
// ─── Top-level Specialty manifest ──────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export const SpecialtyManifestSchema = z
|
|
146
|
+
.object({
|
|
147
|
+
apiVersion: z.literal("shipwright.dev/v1alpha1"),
|
|
148
|
+
kind: z.literal("Specialty"),
|
|
149
|
+
metadata: SpecialtyMetadataSchema,
|
|
150
|
+
identity: SpecialtyIdentitySchema,
|
|
151
|
+
crons: z.array(SpecialtyCronSchema),
|
|
152
|
+
plugins: z.array(z.string()),
|
|
153
|
+
tools: z.array(z.string()),
|
|
154
|
+
env: SpecialtyEnvSchema,
|
|
155
|
+
members: z.array(z.string()),
|
|
156
|
+
repos: z.array(
|
|
157
|
+
z.string().refine(isOrgRepo, {
|
|
158
|
+
message: "each repo must be in org/repo format",
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
161
|
+
chat: z.boolean(),
|
|
162
|
+
voice: z.boolean(),
|
|
163
|
+
resources: SpecialtyResourcesSchema.optional(),
|
|
164
|
+
})
|
|
165
|
+
.strict()
|
|
166
|
+
.superRefine((manifest, ctx) => {
|
|
167
|
+
const cronNames = new Set(manifest.crons.map((c) => c.name));
|
|
168
|
+
manifest.crons.forEach((cron, index) => {
|
|
169
|
+
if (cron.parentCron === undefined) return;
|
|
170
|
+
if (cron.parentCron === cron.name) {
|
|
171
|
+
ctx.addIssue({
|
|
172
|
+
code: "custom",
|
|
173
|
+
path: ["crons", index, "parentCron"],
|
|
174
|
+
message: `parentCron "${cron.parentCron}" cannot reference its own cron entry`,
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!cronNames.has(cron.parentCron)) {
|
|
179
|
+
ctx.addIssue({
|
|
180
|
+
code: "custom",
|
|
181
|
+
path: ["crons", index, "parentCron"],
|
|
182
|
+
message: `parentCron "${cron.parentCron}" does not match any cron name in this manifest`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export type SpecialtyManifest = z.infer<typeof SpecialtyManifestSchema>;
|
|
189
|
+
|
|
190
|
+
// ─── Parsing ────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse and validate a YAML Specialty manifest string.
|
|
194
|
+
*
|
|
195
|
+
* Pure — no I/O. `content` is the already-read YAML text; throws a
|
|
196
|
+
* descriptive Error (with the underlying ZodError's field-path issues
|
|
197
|
+
* embedded in `message`, and the ZodError itself reachable via `cause`) if
|
|
198
|
+
* the manifest fails schema validation.
|
|
199
|
+
*/
|
|
200
|
+
export function parseSpecialtyManifest(content: string): SpecialtyManifest {
|
|
201
|
+
const raw: unknown = parseYaml(content);
|
|
202
|
+
const result = SpecialtyManifestSchema.safeParse(raw);
|
|
203
|
+
if (!result.success) {
|
|
204
|
+
const details = result.error.issues
|
|
205
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
206
|
+
.join("; ");
|
|
207
|
+
throw new Error(`invalid Specialty manifest — ${details}`, {
|
|
208
|
+
cause: result.error,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return result.data;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── JSON Schema generation ─────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build the JSON-serializable JSON Schema object for SpecialtyManifestSchema.
|
|
218
|
+
*
|
|
219
|
+
* Pure — no file I/O — so both scripts/generate-specialty-schema.ts's CLI
|
|
220
|
+
* entrypoint and its content test can call this directly instead of shelling
|
|
221
|
+
* out. Uses zod 4's built-in `z.toJSONSchema` (draft-07 target, to stay
|
|
222
|
+
* contract-compatible with the previous zod-to-json-schema output).
|
|
223
|
+
*/
|
|
224
|
+
export function buildSpecialtyJsonSchema(): Record<string, unknown> {
|
|
225
|
+
const { $schema, ...definition } = z.toJSONSchema(SpecialtyManifestSchema, {
|
|
226
|
+
target: "draft-7",
|
|
227
|
+
io: "input",
|
|
228
|
+
});
|
|
229
|
+
// Wrap as a named definition (`$ref` + `definitions`) — the shape the
|
|
230
|
+
// previous zod-to-json-schema output used, so consumers see no change.
|
|
231
|
+
return {
|
|
232
|
+
$ref: "#/definitions/SpecialtyManifest",
|
|
233
|
+
definitions: { SpecialtyManifest: definition },
|
|
234
|
+
$schema,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/specialty-registry.unit.test.ts
|
|
3
|
+
* Unit tests for the pure Specialty manifest schema + parser. No I/O, no
|
|
4
|
+
* network, no fs — parseSpecialtyManifest takes a YAML string in memory and
|
|
5
|
+
* returns/throws in-process.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
10
|
+
import {
|
|
11
|
+
SpecialtyManifestSchema,
|
|
12
|
+
parseSpecialtyManifest,
|
|
13
|
+
} from "./specialty-registry.ts";
|
|
14
|
+
|
|
15
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** A minimal valid manifest object. Pass overrides for the fields a test cares about. */
|
|
18
|
+
function baseManifest(overrides: Record<string, unknown> = {}) {
|
|
19
|
+
return {
|
|
20
|
+
apiVersion: "shipwright.dev/v1alpha1",
|
|
21
|
+
kind: "Specialty",
|
|
22
|
+
metadata: {
|
|
23
|
+
name: "dude-agent",
|
|
24
|
+
displayName: "The Dude",
|
|
25
|
+
description: "A laid-back team member agent.",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
skills: [],
|
|
28
|
+
},
|
|
29
|
+
identity: { templatesDir: "templates/dude" },
|
|
30
|
+
crons: [],
|
|
31
|
+
plugins: [],
|
|
32
|
+
tools: [],
|
|
33
|
+
env: { required: [], optional: [] },
|
|
34
|
+
members: [],
|
|
35
|
+
repos: [],
|
|
36
|
+
chat: true,
|
|
37
|
+
voice: false,
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Round-trips a manifest object through YAML so tests exercise the real parse path. */
|
|
43
|
+
function parseManifestYaml(manifest: Record<string, unknown>) {
|
|
44
|
+
return parseSpecialtyManifest(stringifyYaml(manifest));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function expectThrowsWithMessage(manifest: Record<string, unknown>, ...fragments: string[]) {
|
|
48
|
+
expect(() => parseManifestYaml(manifest)).toThrow();
|
|
49
|
+
try {
|
|
50
|
+
parseManifestYaml(manifest);
|
|
51
|
+
throw new Error("expected parseSpecialtyManifest to throw");
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const message = (err as Error).message;
|
|
54
|
+
for (const fragment of fragments) {
|
|
55
|
+
expect(message).toContain(fragment);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Valid manifest ─────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("parseSpecialtyManifest — valid manifest", () => {
|
|
63
|
+
it("parses a valid minimal manifest successfully", () => {
|
|
64
|
+
const manifest = parseManifestYaml(
|
|
65
|
+
baseManifest({
|
|
66
|
+
metadata: {
|
|
67
|
+
name: "dude-agent",
|
|
68
|
+
displayName: "The Dude",
|
|
69
|
+
description: "A laid-back team member agent.",
|
|
70
|
+
version: "1.0.0",
|
|
71
|
+
skills: [
|
|
72
|
+
{
|
|
73
|
+
id: "chill",
|
|
74
|
+
name: "Stay Chill",
|
|
75
|
+
description: "Keeps things mellow.",
|
|
76
|
+
tags: ["vibe"],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
crons: [
|
|
81
|
+
{
|
|
82
|
+
name: "morning-brief",
|
|
83
|
+
schedule: "0 9 * * 1-5",
|
|
84
|
+
prompt: "Run the morning brief.",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
plugins: ["@shipwright/plugin"],
|
|
88
|
+
tools: ["Read", "Write"],
|
|
89
|
+
env: {
|
|
90
|
+
required: [
|
|
91
|
+
{
|
|
92
|
+
key: "SHIPWRIGHT_API_URL",
|
|
93
|
+
description: "Base URL of the admin API.",
|
|
94
|
+
secret: false,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
optional: [
|
|
98
|
+
{
|
|
99
|
+
key: "GROQ_API_KEY",
|
|
100
|
+
description: "Optional Groq API key for voice.",
|
|
101
|
+
secret: true,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
members: ["alice"],
|
|
106
|
+
repos: ["my-org/my-repo"],
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
expect(manifest.apiVersion).toBe("shipwright.dev/v1alpha1");
|
|
110
|
+
expect(manifest.kind).toBe("Specialty");
|
|
111
|
+
expect(manifest.metadata.name).toBe("dude-agent");
|
|
112
|
+
expect(manifest.identity.templatesDir).toBe("templates/dude");
|
|
113
|
+
expect(manifest.crons).toHaveLength(1);
|
|
114
|
+
expect(manifest.chat).toBe(true);
|
|
115
|
+
expect(manifest.voice).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("accepts a cron whose parentCron resolves to a sibling cron name", () => {
|
|
119
|
+
const manifest = parseManifestYaml(
|
|
120
|
+
baseManifest({
|
|
121
|
+
crons: [
|
|
122
|
+
{
|
|
123
|
+
name: "parent-cron",
|
|
124
|
+
schedule: "0 9 * * 1-5",
|
|
125
|
+
prompt: "Parent prompt.",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "child-cron",
|
|
129
|
+
schedule: "0 10 * * 1-5",
|
|
130
|
+
prompt: "Child prompt.",
|
|
131
|
+
parentCron: "parent-cron",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
expect(manifest.crons[1].parentCron).toBe("parent-cron");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── Acceptance criterion 1 — failure cases ────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("parseSpecialtyManifest — invalid manifests fail with field-path errors", () => {
|
|
143
|
+
it("rejects a cron missing its required schedule field", () => {
|
|
144
|
+
expectThrowsWithMessage(
|
|
145
|
+
baseManifest({
|
|
146
|
+
crons: [{ name: "bad-cron", prompt: "Missing schedule." }],
|
|
147
|
+
}),
|
|
148
|
+
"crons",
|
|
149
|
+
"schedule",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rejects an unknown top-level field", () => {
|
|
154
|
+
expectThrowsWithMessage(
|
|
155
|
+
baseManifest({ unknownField: "surprise" }),
|
|
156
|
+
"unknownField",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("rejects an env entry carrying a value key (trust boundary)", () => {
|
|
161
|
+
expectThrowsWithMessage(
|
|
162
|
+
baseManifest({
|
|
163
|
+
env: {
|
|
164
|
+
required: [
|
|
165
|
+
{
|
|
166
|
+
key: "SOME_KEY",
|
|
167
|
+
description: "has a value, which is forbidden",
|
|
168
|
+
secret: false,
|
|
169
|
+
value: "should not be allowed",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
optional: [],
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
"env",
|
|
176
|
+
"value",
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("rejects a dangling parentCron reference", () => {
|
|
181
|
+
expectThrowsWithMessage(
|
|
182
|
+
baseManifest({
|
|
183
|
+
crons: [
|
|
184
|
+
{
|
|
185
|
+
name: "only-cron",
|
|
186
|
+
schedule: "0 9 * * 1-5",
|
|
187
|
+
prompt: "Prompt.",
|
|
188
|
+
parentCron: "does-not-exist",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
}),
|
|
192
|
+
"parentCron",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("rejects a non-RFC1123 metadata.name", () => {
|
|
197
|
+
expectThrowsWithMessage(
|
|
198
|
+
baseManifest({
|
|
199
|
+
metadata: {
|
|
200
|
+
name: "Dude_Agent!",
|
|
201
|
+
displayName: "The Dude",
|
|
202
|
+
description: "desc",
|
|
203
|
+
version: "1.0.0",
|
|
204
|
+
skills: [],
|
|
205
|
+
},
|
|
206
|
+
}),
|
|
207
|
+
"metadata",
|
|
208
|
+
"name",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ─── Schema-level checks (direct safeParse, matching repo convention) ──────
|
|
214
|
+
|
|
215
|
+
describe("SpecialtyManifestSchema.safeParse", () => {
|
|
216
|
+
it("rejects a self-referencing parentCron", () => {
|
|
217
|
+
const result = SpecialtyManifestSchema.safeParse(
|
|
218
|
+
baseManifest({
|
|
219
|
+
crons: [
|
|
220
|
+
{
|
|
221
|
+
name: "self-cron",
|
|
222
|
+
schedule: "0 9 * * 1-5",
|
|
223
|
+
prompt: "Prompt.",
|
|
224
|
+
parentCron: "self-cron",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
expect(result.success).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("defaults silent/enabled on a cron entry when omitted", () => {
|
|
233
|
+
const result = SpecialtyManifestSchema.safeParse(
|
|
234
|
+
baseManifest({
|
|
235
|
+
crons: [
|
|
236
|
+
{ name: "morning-brief", schedule: "0 9 * * 1-5", prompt: "Prompt." },
|
|
237
|
+
],
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
expect(result.success).toBe(true);
|
|
241
|
+
if (result.success) {
|
|
242
|
+
expect(result.data.crons[0].silent).toBe(false);
|
|
243
|
+
expect(result.data.crons[0].enabled).toBe(true);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("rejects a bad apiVersion literal", () => {
|
|
248
|
+
const result = SpecialtyManifestSchema.safeParse(
|
|
249
|
+
baseManifest({ apiVersion: "shipwright.dev/v1" }),
|
|
250
|
+
);
|
|
251
|
+
expect(result.success).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("rejects a repos entry not in org/repo format", () => {
|
|
255
|
+
const result = SpecialtyManifestSchema.safeParse(
|
|
256
|
+
baseManifest({ repos: ["not-a-valid-repo"] }),
|
|
257
|
+
);
|
|
258
|
+
expect(result.success).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("accepts an optional resources override", () => {
|
|
262
|
+
const result = SpecialtyManifestSchema.safeParse(
|
|
263
|
+
baseManifest({
|
|
264
|
+
resources: {
|
|
265
|
+
requests: { cpu: "500m", memory: "2Gi", "ephemeral-storage": "1Gi" },
|
|
266
|
+
limits: { memory: "8Gi", "ephemeral-storage": "1Gi" },
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
expect(result.success).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/startup-policy-reconcile.ts
|
|
3
|
+
*
|
|
4
|
+
* One-shot policy-only reconcile run on admin startup (ACP-1.3).
|
|
5
|
+
*
|
|
6
|
+
* The chart passes the Claude policy hash to admin as an env var, so a policy
|
|
7
|
+
* change restarts admin. On boot this pass walks every managed agent through
|
|
8
|
+
* `provisioner.reconcile(..., { policyOnly: true })`, which patches only the
|
|
9
|
+
* policy wiring (volume, mount, hash annotation) — so only agents whose policy
|
|
10
|
+
* actually changed roll. It runs whether or not a policy is configured, so
|
|
11
|
+
* agents lose the mount after the policy is disabled. The same pass applies
|
|
12
|
+
* the Anthropic workload identity federation wiring (ServiceAccount, projected
|
|
13
|
+
* token volume, mount, federation env), so turning federation on or off and
|
|
14
|
+
* restarting admin corrects existing agents.
|
|
15
|
+
*
|
|
16
|
+
* The startup pass never moves agents onto their own per-agent identity: that
|
|
17
|
+
* creates Anthropic and Kubernetes resources, which should not happen as a side
|
|
18
|
+
* effect of a boot. Operators run POST /agents/reconcile (with dryRun=true
|
|
19
|
+
* first); this pass only reports how many agents have no owner (noOwner).
|
|
20
|
+
*
|
|
21
|
+
* Never throws: a Kubernetes or database failure is logged and admin keeps
|
|
22
|
+
* serving. Called once from main.ts after Bun.serve.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { AgentProvisioner, ReconcileTarget } from "./agent-provisioner.ts";
|
|
26
|
+
import type { AgentService, AgentSummary } from "./agents.ts";
|
|
27
|
+
|
|
28
|
+
interface StartupPolicyReconcileDeps {
|
|
29
|
+
agentService: Pick<AgentService, "list">;
|
|
30
|
+
provisioner: Pick<AgentProvisioner, "canProvision" | "reconcile">;
|
|
31
|
+
logger: Pick<Console, "log" | "error">;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The reconcile targets for a list of agents. Self-hosted agents manage their
|
|
36
|
+
* own workloads, so they are excluded from K8s reconciliation. Shared with
|
|
37
|
+
* POST /agents/reconcile so both paths select agents the same way.
|
|
38
|
+
*/
|
|
39
|
+
export function managedReconcileTargets(
|
|
40
|
+
agents: AgentSummary[],
|
|
41
|
+
): ReconcileTarget[] {
|
|
42
|
+
return agents
|
|
43
|
+
.filter((a) => !a.selfHosted)
|
|
44
|
+
.map((a) => ({
|
|
45
|
+
id: a.id,
|
|
46
|
+
slug: a.name,
|
|
47
|
+
accountId: a.accountId,
|
|
48
|
+
ownerEmail: a.ownerEmail,
|
|
49
|
+
anthropicIdentityName: a.anthropicIdentityName,
|
|
50
|
+
anthropicServiceAccountId: a.anthropicServiceAccountId,
|
|
51
|
+
anthropicFederationRuleId: a.anthropicFederationRuleId,
|
|
52
|
+
k8sServiceAccountName: a.k8sServiceAccountName,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runStartupPolicyReconcile({
|
|
57
|
+
agentService,
|
|
58
|
+
provisioner,
|
|
59
|
+
logger,
|
|
60
|
+
}: StartupPolicyReconcileDeps): Promise<void> {
|
|
61
|
+
if (!provisioner.canProvision) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const targets = managedReconcileTargets(await agentService.list());
|
|
65
|
+
const result = await provisioner.reconcile(targets, { policyOnly: true });
|
|
66
|
+
logger.log(
|
|
67
|
+
`[startup-policy-reconcile] agents=${targets.length} updated=${result.updated.length} recreated=${result.recreated.length} orphans=${result.orphans.length} failed=${result.failed.length} noOwner=${result.noOwner.length}`,
|
|
68
|
+
);
|
|
69
|
+
for (const { agentId, error } of result.failed) {
|
|
70
|
+
logger.error(
|
|
71
|
+
`[startup-policy-reconcile] agent ${agentId} failed: ${error}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.error("[startup-policy-reconcile] reconcile failed:", err);
|
|
76
|
+
}
|
|
77
|
+
}
|