@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,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/admin-boot-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Env-driven configuration helpers for the admin platform boot (admin-boot.ts):
|
|
5
|
+
* the migration preflight, provisioner selection, Anthropic federation parsing,
|
|
6
|
+
* agent-deletion client selection, the readiness DB check and the sweeper
|
|
7
|
+
* interval resolvers. Pure (or early-return) logic kept apart from
|
|
8
|
+
* startAdmin()'s process wiring so it is unit-testable in isolation;
|
|
9
|
+
* main.ts re-exports the public ones (main.unit.test.ts imports them there).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
14
|
+
import { AgentAnthropicIdentityService } from "./agent-anthropic-identity.ts";
|
|
15
|
+
import type { AgentAnthropicFederation } from "./agent-manifest.ts";
|
|
16
|
+
import type {
|
|
17
|
+
AgentProvisioner,
|
|
18
|
+
PerAgentIdentityConfig,
|
|
19
|
+
} from "./agent-provisioner.ts";
|
|
20
|
+
import {
|
|
21
|
+
KubernetesAgentProvisioner,
|
|
22
|
+
NoopAgentProvisioner,
|
|
23
|
+
} from "./agent-provisioner.ts";
|
|
24
|
+
import type { AgentTokenService } from "./agent-tokens.ts";
|
|
25
|
+
import { FederatedTokenProvider } from "./anthropic-federated-token-provider.ts";
|
|
26
|
+
import { HttpAnthropicIdentityClient } from "./anthropic-identity-client.ts";
|
|
27
|
+
import { AnthropicIdentityNameAllocator } from "./anthropic-identity-name.ts";
|
|
28
|
+
import type { ChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
29
|
+
import {
|
|
30
|
+
HttpChatServiceProvisioningClient,
|
|
31
|
+
NoopChatServiceProvisioningClient,
|
|
32
|
+
} from "./chat-service-provisioning-client.ts";
|
|
33
|
+
import { HttpKubernetesClient } from "./kubernetes-client.ts";
|
|
34
|
+
import type { ServiceIntegration } from "./service-integration.ts";
|
|
35
|
+
|
|
36
|
+
// ─── Migration preflight ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Runs `prisma migrate deploy` as a boot preflight.
|
|
40
|
+
* Idempotent — safe to call on every startup. Throws on migration failure.
|
|
41
|
+
*
|
|
42
|
+
* Exported for the early-return branch (DATABASE_URL_SHIPWRIGHT_ADMIN unset) to
|
|
43
|
+
* be unit-tested directly. The Bun.spawn path below it requires a real `bunx
|
|
44
|
+
* prisma migrate deploy` process and a live Postgres — that's process-wiring /
|
|
45
|
+
* real-I/O, exercised in deployed environments and the integration DB rather
|
|
46
|
+
* than a unit test.
|
|
47
|
+
*/
|
|
48
|
+
export async function runMigrations(): Promise<void> {
|
|
49
|
+
const databaseUrl = process.env.DATABASE_URL_SHIPWRIGHT_ADMIN;
|
|
50
|
+
if (!databaseUrl) {
|
|
51
|
+
console.warn(
|
|
52
|
+
"[admin] DATABASE_URL_SHIPWRIGHT_ADMIN not set — skipping prisma migrate deploy",
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("[admin] running prisma migrate deploy...");
|
|
58
|
+
|
|
59
|
+
const proc = Bun.spawn(
|
|
60
|
+
["bunx", "prisma", "migrate", "deploy", "--schema=prisma/schema"],
|
|
61
|
+
{
|
|
62
|
+
cwd: join(import.meta.dir, ".."),
|
|
63
|
+
env: { ...process.env, DATABASE_URL_SHIPWRIGHT_ADMIN: databaseUrl },
|
|
64
|
+
stdout: "pipe",
|
|
65
|
+
stderr: "pipe",
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const [stdout, stderr] = await Promise.all([
|
|
70
|
+
new Response(proc.stdout).text(),
|
|
71
|
+
new Response(proc.stderr).text(),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
await proc.exited;
|
|
75
|
+
|
|
76
|
+
if (proc.exitCode !== 0) {
|
|
77
|
+
console.error("[admin] prisma migrate deploy failed:");
|
|
78
|
+
console.error(stderr);
|
|
79
|
+
throw new Error(`prisma migrate deploy exited with code ${proc.exitCode}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (stdout.trim()) {
|
|
83
|
+
console.log("[admin]", stdout.trim());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log("[admin] migrations complete");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Provisioner selection ────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Select the agent provisioner from the environment.
|
|
93
|
+
*
|
|
94
|
+
* When `SHIPWRIGHT_K8S_PROVISIONING=enabled`, construct a real
|
|
95
|
+
* `KubernetesAgentProvisioner` that mints a per-agent token and creates the
|
|
96
|
+
* backing Secret + Deployment in the cluster. Otherwise (unset / any other
|
|
97
|
+
* value) return a `NoopAgentProvisioner` so create/delete behave exactly as
|
|
98
|
+
* before — no cluster required. This keeps the new wiring safe to deploy
|
|
99
|
+
* standalone behind the flag's default.
|
|
100
|
+
*
|
|
101
|
+
* Exported so its many env-driven branches (K8s provisioning flag, voice env
|
|
102
|
+
* passthrough, chat-service client construction, PVC name template)
|
|
103
|
+
* can be unit-tested directly with an injected/stub AgentTokenService, without
|
|
104
|
+
* needing a real Prisma connection or K8s cluster.
|
|
105
|
+
*
|
|
106
|
+
* `serviceIntegrations` are the extension-registered integrations (see
|
|
107
|
+
* service-integration.ts); the platform names none of them.
|
|
108
|
+
*/
|
|
109
|
+
export function buildProvisioner(
|
|
110
|
+
env: NodeJS.ProcessEnv,
|
|
111
|
+
agentTokenService: AgentTokenService,
|
|
112
|
+
prisma?: PrismaClient,
|
|
113
|
+
serviceIntegrations: readonly ServiceIntegration[] = [],
|
|
114
|
+
): AgentProvisioner {
|
|
115
|
+
if (env.SHIPWRIGHT_K8S_PROVISIONING !== "enabled") {
|
|
116
|
+
return new NoopAgentProvisioner();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const namespace = env.SHIPWRIGHT_K8S_NAMESPACE ?? "default";
|
|
120
|
+
const image = env.SHIPWRIGHT_AGENT_IMAGE ?? "";
|
|
121
|
+
const imageTag = env.SHIPWRIGHT_AGENT_IMAGE_TAG ?? "latest";
|
|
122
|
+
const apiUrl = env.SHIPWRIGHT_API_URL ?? "";
|
|
123
|
+
const replicasRaw = env.SHIPWRIGHT_AGENT_REPLICAS;
|
|
124
|
+
const replicas = replicasRaw ? Number(replicasRaw) : undefined;
|
|
125
|
+
const pvcStorageGiRaw = env.SHIPWRIGHT_AGENT_PVC_STORAGE_GI;
|
|
126
|
+
const pvcStorageGi = pvcStorageGiRaw ? Number(pvcStorageGiRaw) : undefined;
|
|
127
|
+
const pvcNameTemplate = env.SHIPWRIGHT_AGENT_PVC_NAME_TEMPLATE;
|
|
128
|
+
|
|
129
|
+
// Optional per-field container resource overrides. Resource quantities are
|
|
130
|
+
// strings (e.g. "500m", "2Gi") — no numeric parsing needed, unlike replicas/
|
|
131
|
+
// pvcStorageGi above. Each is passed through only when set (undefined, not
|
|
132
|
+
// an empty string, when unset) so resolveAgentContainerResources's default
|
|
133
|
+
// fallback applies field-by-field.
|
|
134
|
+
const cpuRequest = env.SHIPWRIGHT_K8S_AGENT_CPU_REQUEST;
|
|
135
|
+
const cpuLimit = env.SHIPWRIGHT_K8S_AGENT_CPU_LIMIT;
|
|
136
|
+
const memoryRequest = env.SHIPWRIGHT_K8S_AGENT_MEMORY_REQUEST;
|
|
137
|
+
const memoryLimit = env.SHIPWRIGHT_K8S_AGENT_MEMORY_LIMIT;
|
|
138
|
+
const ephemeralStorage = env.SHIPWRIGHT_K8S_AGENT_EPHEMERAL_STORAGE;
|
|
139
|
+
const resources = {
|
|
140
|
+
...(cpuRequest ? { cpuRequest } : {}),
|
|
141
|
+
...(cpuLimit ? { cpuLimit } : {}),
|
|
142
|
+
...(memoryRequest ? { memoryRequest } : {}),
|
|
143
|
+
...(memoryLimit ? { memoryLimit } : {}),
|
|
144
|
+
...(ephemeralStorage ? { ephemeralStorage } : {}),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Agent-voice (STT/TTS) env flowed into provisioned agent pods. The chart's
|
|
148
|
+
// voice Secret + Whisper Service URL land in the admin's env when
|
|
149
|
+
// agent.voice.enabled; absent → voice disabled and no voice env is injected.
|
|
150
|
+
const voice = {
|
|
151
|
+
...(env.WHISPER_SERVICE_URL
|
|
152
|
+
? { whisperServiceUrl: env.WHISPER_SERVICE_URL }
|
|
153
|
+
: {}),
|
|
154
|
+
...(env.GROQ_API_KEY ? { groqApiKey: env.GROQ_API_KEY } : {}),
|
|
155
|
+
...(env.ELEVENLABS_API_KEY
|
|
156
|
+
? { elevenLabsApiKey: env.ELEVENLABS_API_KEY }
|
|
157
|
+
: {}),
|
|
158
|
+
...(env.ELEVENLABS_VOICE_ID ? { voiceId: env.ELEVENLABS_VOICE_ID } : {}),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Claude Code managed-policy ConfigMap. Empty/unset name disables the
|
|
162
|
+
// feature; the hash is optional and only stamped on pods when non-empty.
|
|
163
|
+
const claudePolicyConfigMap =
|
|
164
|
+
env.SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_CONFIGMAP;
|
|
165
|
+
const claudePolicyHash = env.SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_HASH;
|
|
166
|
+
const claudePolicy = claudePolicyConfigMap
|
|
167
|
+
? {
|
|
168
|
+
configMapName: claudePolicyConfigMap,
|
|
169
|
+
...(claudePolicyHash ? { hash: claudePolicyHash } : {}),
|
|
170
|
+
}
|
|
171
|
+
: undefined;
|
|
172
|
+
|
|
173
|
+
// Anthropic workload identity federation. In shared mode an unset rule ID
|
|
174
|
+
// disables it; a partial config throws here so admin startup fails with a
|
|
175
|
+
// clear message. Shared mode passes the one shared identity; perAgent mode
|
|
176
|
+
// passes what the provisioner needs to create each agent's own identity.
|
|
177
|
+
const parsedFederation = parseAnthropicFederation(env);
|
|
178
|
+
let anthropicFederation: AgentAnthropicFederation | undefined;
|
|
179
|
+
let perAgentIdentity: PerAgentIdentityConfig | undefined;
|
|
180
|
+
if (parsedFederation?.mode === "shared") {
|
|
181
|
+
const { mode: _mode, ...shared } = parsedFederation;
|
|
182
|
+
anthropicFederation = shared;
|
|
183
|
+
} else if (parsedFederation?.mode === "perAgent") {
|
|
184
|
+
perAgentIdentity = buildPerAgentIdentity(env, parsedFederation, prisma);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const chatServiceUrl = env.SHIPWRIGHT_CHAT_SERVICE_URL;
|
|
188
|
+
const chatServiceAdminToken = env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN;
|
|
189
|
+
const chatService =
|
|
190
|
+
chatServiceUrl && chatServiceAdminToken
|
|
191
|
+
? new HttpChatServiceProvisioningClient(
|
|
192
|
+
chatServiceUrl,
|
|
193
|
+
chatServiceAdminToken,
|
|
194
|
+
)
|
|
195
|
+
: undefined;
|
|
196
|
+
|
|
197
|
+
const k8s = new HttpKubernetesClient();
|
|
198
|
+
|
|
199
|
+
return new KubernetesAgentProvisioner(k8s, agentTokenService, {
|
|
200
|
+
namespace,
|
|
201
|
+
image,
|
|
202
|
+
imageTag,
|
|
203
|
+
apiUrl,
|
|
204
|
+
...(replicas !== undefined && Number.isFinite(replicas)
|
|
205
|
+
? { replicas }
|
|
206
|
+
: {}),
|
|
207
|
+
...(pvcStorageGi !== undefined && Number.isFinite(pvcStorageGi)
|
|
208
|
+
? { pvcStorageGi }
|
|
209
|
+
: {}),
|
|
210
|
+
...(Object.keys(resources).length > 0 ? { resources } : {}),
|
|
211
|
+
...(Object.keys(voice).length > 0 ? { voice } : {}),
|
|
212
|
+
...(claudePolicy ? { claudePolicy } : {}),
|
|
213
|
+
...(anthropicFederation ? { anthropicFederation } : {}),
|
|
214
|
+
...(perAgentIdentity ? { perAgentIdentity } : {}),
|
|
215
|
+
// When SHIPWRIGHT_AGENT_PVC_NAME_TEMPLATE is set (e.g. "acme-agent-{name}-home"),
|
|
216
|
+
// substitute {name} with the pre-sanitized name resolved by pvcNameFor()
|
|
217
|
+
// (slug sanitized via sanitizeAgentName, or falls back to resourceName).
|
|
218
|
+
// When unset, the provisioner uses the default `${resourceName}-home` naming.
|
|
219
|
+
...(pvcNameTemplate
|
|
220
|
+
? {
|
|
221
|
+
pvcName: (name: string) => pvcNameTemplate.replace("{name}", name),
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
224
|
+
// Extension-registered service integrations (e.g. task-store): each one
|
|
225
|
+
// with a URL mints a per-agent token on provision (before chat) and injects
|
|
226
|
+
// it into the agent Secret + Deployment.
|
|
227
|
+
...(serviceIntegrations.length > 0 ? { serviceIntegrations } : {}),
|
|
228
|
+
// Chat-service token minting: when both SHIPWRIGHT_CHAT_SERVICE_URL and
|
|
229
|
+
// SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN are set, mint a per-agent token on
|
|
230
|
+
// provision and inject it into the agent Secret + Deployment.
|
|
231
|
+
...(chatService ? { chatService, chatServiceUrl } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The perAgent provisioner deps. The admin pod signs in to the Anthropic Admin
|
|
237
|
+
* API by federation with its own projected token and bootstrap rule (the
|
|
238
|
+
* chart renders ANTHROPIC_IDENTITY_TOKEN_FILE / _FEDERATION_RULE_ID /
|
|
239
|
+
* _ORGANIZATION_ID, plus _SERVICE_ACCOUNT_ID / _WORKSPACE_ID, in perAgent
|
|
240
|
+
* mode); identity IDs and names are kept on the Agent row.
|
|
241
|
+
* @throws when the admin sign-in env or the database client is missing
|
|
242
|
+
*/
|
|
243
|
+
function buildPerAgentIdentity(
|
|
244
|
+
env: NodeJS.ProcessEnv,
|
|
245
|
+
federation: PerAgentAnthropicFederation,
|
|
246
|
+
prisma: PrismaClient | undefined,
|
|
247
|
+
): PerAgentIdentityConfig {
|
|
248
|
+
const required = [
|
|
249
|
+
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
250
|
+
"ANTHROPIC_FEDERATION_RULE_ID",
|
|
251
|
+
"ANTHROPIC_ORGANIZATION_ID",
|
|
252
|
+
] as const;
|
|
253
|
+
const missing = required.filter((name) => !env[name]);
|
|
254
|
+
if (missing.length > 0) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`${FEDERATION_ENV_PREFIX}IDENTITY_MODE is perAgent but the admin's own Anthropic sign-in is not configured: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} missing.`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (!prisma) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"perAgent identity mode needs a database client to save agent identities.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const tokenProvider = new FederatedTokenProvider({
|
|
265
|
+
fetchFn: fetch,
|
|
266
|
+
tokenFilePath: env.ANTHROPIC_IDENTITY_TOKEN_FILE as string,
|
|
267
|
+
federationRuleId: env.ANTHROPIC_FEDERATION_RULE_ID as string,
|
|
268
|
+
organizationId: env.ANTHROPIC_ORGANIZATION_ID as string,
|
|
269
|
+
...(env.ANTHROPIC_SERVICE_ACCOUNT_ID
|
|
270
|
+
? { serviceAccountId: env.ANTHROPIC_SERVICE_ACCOUNT_ID }
|
|
271
|
+
: {}),
|
|
272
|
+
...(env.ANTHROPIC_WORKSPACE_ID
|
|
273
|
+
? { workspaceId: env.ANTHROPIC_WORKSPACE_ID }
|
|
274
|
+
: {}),
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
client: new HttpAnthropicIdentityClient({ fetchFn: fetch, tokenProvider }),
|
|
278
|
+
identities: new AgentAnthropicIdentityService(prisma),
|
|
279
|
+
names: new AnthropicIdentityNameAllocator(prisma, {
|
|
280
|
+
suffix: federation.nameSuffix,
|
|
281
|
+
}),
|
|
282
|
+
organizationId: federation.organizationId,
|
|
283
|
+
...(federation.workspaceId ? { workspaceId: federation.workspaceId } : {}),
|
|
284
|
+
issuerId: federation.issuerId,
|
|
285
|
+
audience: federation.audience,
|
|
286
|
+
expirationSeconds: federation.expirationSeconds,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const FEDERATION_ENV_PREFIX = "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_";
|
|
291
|
+
|
|
292
|
+
/** Resolve the identity mode (default "shared"); unknown values throw. */
|
|
293
|
+
function resolveFederationIdentityMode(
|
|
294
|
+
env: NodeJS.ProcessEnv,
|
|
295
|
+
): "shared" | "perAgent" {
|
|
296
|
+
const modeName = `${FEDERATION_ENV_PREFIX}IDENTITY_MODE`;
|
|
297
|
+
const mode = env[modeName] || "shared";
|
|
298
|
+
if (mode !== "shared" && mode !== "perAgent") {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`${modeName} must be "shared" or "perAgent", got "${mode}".`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return mode;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether Anthropic workload identity federation is active for provisioned
|
|
308
|
+
* agent pods (WIF-2.1, PAI-2.1). Derived from the identity mode, mirroring
|
|
309
|
+
* parseAnthropicFederation's gating without its completeness checks (those
|
|
310
|
+
* throw at buildProvisioner time): a deployment that provisions pods AND is in
|
|
311
|
+
* perAgent mode, or in shared mode with the rule ID set, federates agents.
|
|
312
|
+
* Gates the Anthropic credential write/read guards.
|
|
313
|
+
*/
|
|
314
|
+
export function isAnthropicFederationEnabled(env: NodeJS.ProcessEnv): boolean {
|
|
315
|
+
if (env.SHIPWRIGHT_K8S_PROVISIONING !== "enabled") return false;
|
|
316
|
+
return (
|
|
317
|
+
resolveFederationIdentityMode(env) === "perAgent" ||
|
|
318
|
+
Boolean(env[`${FEDERATION_ENV_PREFIX}RULE_ID`])
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Kubernetes rejects projected service-account tokens shorter than this. */
|
|
323
|
+
const MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS = 600;
|
|
324
|
+
|
|
325
|
+
/** Charset for the per-agent name suffix (Kubernetes / Anthropic-safe). */
|
|
326
|
+
const FEDERATION_NAME_SUFFIX_PATTERN = /^[a-z0-9-]*$/;
|
|
327
|
+
|
|
328
|
+
/** perAgent identity mode: every agent gets its own SA and federation rule. */
|
|
329
|
+
export interface PerAgentAnthropicFederation {
|
|
330
|
+
mode: "perAgent";
|
|
331
|
+
organizationId: string;
|
|
332
|
+
workspaceId?: string;
|
|
333
|
+
/** Anthropic issuer ID of this cluster (the per-agent rules' issuer). */
|
|
334
|
+
issuerId: string;
|
|
335
|
+
/** Appended to per-agent resource names so clusters sharing an org don't collide. */
|
|
336
|
+
nameSuffix?: string;
|
|
337
|
+
audience: string;
|
|
338
|
+
expirationSeconds: number;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Discriminated by `mode`; `undefined` (from the parser) means disabled. */
|
|
342
|
+
export type ParsedAnthropicFederation =
|
|
343
|
+
| ({ mode: "shared" } & AgentAnthropicFederation)
|
|
344
|
+
| PerAgentAnthropicFederation;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parse the Anthropic Workload Identity Federation config for provisioned agent
|
|
348
|
+
* pods. SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_IDENTITY_MODE selects the
|
|
349
|
+
* mode (default "shared"; unknown values throw).
|
|
350
|
+
*
|
|
351
|
+
* shared: an unset/empty RULE_ID disables the feature (returns undefined, other
|
|
352
|
+
* federation vars ignored). With the rule ID set, the agent ServiceAccount,
|
|
353
|
+
* organization ID and service account ID are required.
|
|
354
|
+
*
|
|
355
|
+
* perAgent: requires ORGANIZATION_ID and ISSUER_ID; the shared rule/service
|
|
356
|
+
* account are not used (per-agent identities are created at provision time).
|
|
357
|
+
*
|
|
358
|
+
* A partial config throws so startup fails loudly instead of provisioning pods
|
|
359
|
+
* that cannot authenticate.
|
|
360
|
+
*/
|
|
361
|
+
export function parseAnthropicFederation(
|
|
362
|
+
env: NodeJS.ProcessEnv,
|
|
363
|
+
): ParsedAnthropicFederation | undefined {
|
|
364
|
+
const prefix = FEDERATION_ENV_PREFIX;
|
|
365
|
+
const mode = resolveFederationIdentityMode(env);
|
|
366
|
+
|
|
367
|
+
const ruleId = env[`${prefix}RULE_ID`];
|
|
368
|
+
if (mode === "shared" && !ruleId) return undefined;
|
|
369
|
+
|
|
370
|
+
const required =
|
|
371
|
+
mode === "shared"
|
|
372
|
+
? ({
|
|
373
|
+
serviceAccountName: "SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT",
|
|
374
|
+
organizationId: `${prefix}ORGANIZATION_ID`,
|
|
375
|
+
serviceAccountId: `${prefix}SERVICE_ACCOUNT_ID`,
|
|
376
|
+
} as const)
|
|
377
|
+
: ({
|
|
378
|
+
organizationId: `${prefix}ORGANIZATION_ID`,
|
|
379
|
+
issuerId: `${prefix}ISSUER_ID`,
|
|
380
|
+
} as const);
|
|
381
|
+
const missing = Object.values(required).filter((name) => !env[name]);
|
|
382
|
+
if (missing.length > 0) {
|
|
383
|
+
const verb = missing.length === 1 ? "is" : "are";
|
|
384
|
+
throw new Error(
|
|
385
|
+
mode === "shared"
|
|
386
|
+
? `Anthropic workload identity federation is partially configured: ${prefix}RULE_ID is set but ${missing.join(", ")} ${verb} missing. Set all of them, or unset ${prefix}RULE_ID to disable federation.`
|
|
387
|
+
: `Anthropic workload identity federation is partially configured: ${prefix}IDENTITY_MODE is perAgent but ${missing.join(", ")} ${verb} missing.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const expirationName = `${prefix}TOKEN_EXPIRATION_SECONDS`;
|
|
392
|
+
const expirationRaw = env[expirationName];
|
|
393
|
+
const expirationSeconds = expirationRaw
|
|
394
|
+
? Number(expirationRaw)
|
|
395
|
+
: MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS;
|
|
396
|
+
if (
|
|
397
|
+
!Number.isInteger(expirationSeconds) ||
|
|
398
|
+
expirationSeconds < MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS
|
|
399
|
+
) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`${expirationName} must be an integer >= ${MIN_FEDERATION_TOKEN_EXPIRATION_SECONDS} (Kubernetes minimum for projected tokens), got "${expirationRaw}".`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const workspaceId = env[`${prefix}WORKSPACE_ID`];
|
|
406
|
+
const audience = env[`${prefix}AUDIENCE`] || "https://api.anthropic.com";
|
|
407
|
+
|
|
408
|
+
if (mode === "perAgent") {
|
|
409
|
+
const nameSuffix = env[`${prefix}NAME_SUFFIX`];
|
|
410
|
+
if (nameSuffix && !FEDERATION_NAME_SUFFIX_PATTERN.test(nameSuffix)) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`${prefix}NAME_SUFFIX must match ${FEDERATION_NAME_SUFFIX_PATTERN}, got "${nameSuffix}".`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
mode,
|
|
417
|
+
organizationId: env[`${prefix}ORGANIZATION_ID`] as string,
|
|
418
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
419
|
+
issuerId: env[`${prefix}ISSUER_ID`] as string,
|
|
420
|
+
...(nameSuffix ? { nameSuffix } : {}),
|
|
421
|
+
audience,
|
|
422
|
+
expirationSeconds,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
mode,
|
|
428
|
+
serviceAccountName: env.SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT as string,
|
|
429
|
+
ruleId: ruleId as string,
|
|
430
|
+
organizationId: env[`${prefix}ORGANIZATION_ID`] as string,
|
|
431
|
+
serviceAccountId: env[`${prefix}SERVICE_ACCOUNT_ID`] as string,
|
|
432
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
433
|
+
audience,
|
|
434
|
+
expirationSeconds,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ─── Agent-deletion client selection ──────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Select the chat-service client used by DELETE /agents/:id's
|
|
442
|
+
* `deleteAgentFully()` orchestration (agent-deletion.ts).
|
|
443
|
+
*
|
|
444
|
+
* Independent of `buildProvisioner`/`SHIPWRIGHT_K8S_PROVISIONING`: unlike the
|
|
445
|
+
* token-minting clients constructed inside that function's closure (only
|
|
446
|
+
* built when K8s provisioning is enabled), agent deletion must be able to
|
|
447
|
+
* revoke chat-service tokens regardless of whether K8s
|
|
448
|
+
* provisioning is on — e.g. a local dev setup with provisioning disabled but
|
|
449
|
+
* a real chat-service still running. Extension-registered integrations
|
|
450
|
+
* (task-store) come from AdminExtension.serviceIntegrations instead.
|
|
451
|
+
*
|
|
452
|
+
* Gated on the same env-var pairs as `buildProvisioner` (URL + admin token
|
|
453
|
+
* both set) so the "unconfigured" case is unambiguous. When unconfigured,
|
|
454
|
+
* returns the No-op client rather than `undefined` — constructing the
|
|
455
|
+
* client is cheap (no I/O happens until a call is made), so this avoids
|
|
456
|
+
* making `chatService` optional in `AdminDeps`/`DeleteAgentFullyDeps`
|
|
457
|
+
* for the common case (K8s provisioning disabled, e.g. local dev): a
|
|
458
|
+
* deleteAgentFully() call with nothing to revoke (listTokensForAgent → [])
|
|
459
|
+
* completes cleanly instead of surfacing a new error path.
|
|
460
|
+
*/
|
|
461
|
+
export function buildDeletionClients(env: NodeJS.ProcessEnv): {
|
|
462
|
+
chatService: ChatServiceProvisioningClient;
|
|
463
|
+
} {
|
|
464
|
+
const chatServiceUrl = env.SHIPWRIGHT_CHAT_SERVICE_URL;
|
|
465
|
+
const chatServiceAdminToken = env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN;
|
|
466
|
+
const chatService =
|
|
467
|
+
chatServiceUrl && chatServiceAdminToken
|
|
468
|
+
? new HttpChatServiceProvisioningClient(
|
|
469
|
+
chatServiceUrl,
|
|
470
|
+
chatServiceAdminToken,
|
|
471
|
+
)
|
|
472
|
+
: new NoopChatServiceProvisioningClient();
|
|
473
|
+
|
|
474
|
+
return { chatService };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ─── Readiness check ──────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* DB-aware readiness check backing GET /health/ready. Runs a lightweight
|
|
481
|
+
* `SELECT 1` and reports whether Postgres is actually reachable — unlike
|
|
482
|
+
* GET /health (liveness), which stays DB-independent so a transient DB blip
|
|
483
|
+
* never triggers a liveness-driven pod restart. Takes only the `$queryRaw`
|
|
484
|
+
* slice of PrismaClient so it's unit-testable with a mocked/injected client,
|
|
485
|
+
* no real I/O.
|
|
486
|
+
*/
|
|
487
|
+
export async function checkDbReady(prisma: {
|
|
488
|
+
$queryRaw: (
|
|
489
|
+
query: TemplateStringsArray,
|
|
490
|
+
...values: unknown[]
|
|
491
|
+
) => Promise<unknown>;
|
|
492
|
+
}): Promise<boolean> {
|
|
493
|
+
try {
|
|
494
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
495
|
+
return true;
|
|
496
|
+
} catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── Server config ────────────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
export const DEFAULT_PORT = 3000;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Resolve the public read-only task board repo from the environment.
|
|
507
|
+
* Reads SHIPWRIGHT_ADMIN_PUBLIC_REPO; trims whitespace so a blank/whitespace
|
|
508
|
+
* value behaves like unset (returns undefined → board stays in degraded mode)
|
|
509
|
+
* rather than producing an empty repo filter.
|
|
510
|
+
*/
|
|
511
|
+
export function resolvePublicRepo(
|
|
512
|
+
env: Record<string, string | undefined>,
|
|
513
|
+
): string | undefined {
|
|
514
|
+
return env.SHIPWRIGHT_ADMIN_PUBLIC_REPO?.trim() || undefined;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Default cadence of the session-alert sweeper — matches chat's stall reaper. */
|
|
518
|
+
export const DEFAULT_SESSION_ALERT_INTERVAL_MS = 60_000;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Hard timeout on the sweeper's task-store `GET /sessions` calls. Longer than
|
|
522
|
+
* the repo's usual 5s auth-lookup timeout because this fetches up to 500
|
|
523
|
+
* sessions, but still well inside the default 60s tick so a stalled
|
|
524
|
+
* task-store can't wedge the sweep loop behind its in-flight guard.
|
|
525
|
+
*/
|
|
526
|
+
export const SESSION_ALERT_FETCH_TIMEOUT_MS = 10_000;
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Resolve the session-alert sweeper's tick interval from the environment.
|
|
530
|
+
* Reads SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS; anything unset, blank,
|
|
531
|
+
* non-numeric, or non-positive falls back to the default rather than
|
|
532
|
+
* producing a setInterval that spins (0/NaN) or never fires.
|
|
533
|
+
*/
|
|
534
|
+
export function resolveSessionAlertIntervalMs(
|
|
535
|
+
env: Record<string, string | undefined>,
|
|
536
|
+
): number {
|
|
537
|
+
const raw = env.SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS?.trim();
|
|
538
|
+
if (!raw) return DEFAULT_SESSION_ALERT_INTERVAL_MS;
|
|
539
|
+
const parsed = Number(raw);
|
|
540
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
541
|
+
return DEFAULT_SESSION_ALERT_INTERVAL_MS;
|
|
542
|
+
}
|
|
543
|
+
return parsed;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Cadence of the trial-expiry *warning* sweeper (ATE-2.1). Coarser than the
|
|
548
|
+
* session-alert sweeper's 60s default: a trial-expiry warning window is
|
|
549
|
+
* measured in days, not seconds, so hourly polling still leaves enormous
|
|
550
|
+
* margin against ever missing the window.
|
|
551
|
+
*
|
|
552
|
+
* Distinct from trial-expiry-sweeper.ts's own
|
|
553
|
+
* DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS (60s), which paces the ATE-3.1
|
|
554
|
+
* lockdown sweeper — that one should react promptly once a trial lapses,
|
|
555
|
+
* whereas this one only has to land somewhere inside a multi-day window.
|
|
556
|
+
*/
|
|
557
|
+
export const DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Resolve the trial-expiry sweeper's warning window (in days) from the
|
|
561
|
+
* environment. Reads SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_WARNING_DAYS; anything
|
|
562
|
+
* unset, blank, non-numeric, or non-positive falls back to the sweeper's own
|
|
563
|
+
* default (DEFAULT_TRIAL_EXPIRY_WARNING_DAYS) rather than disabling the
|
|
564
|
+
* check or warning agents on every single tick.
|
|
565
|
+
*/
|
|
566
|
+
export function resolveTrialExpiryWarningDays(
|
|
567
|
+
env: Record<string, string | undefined>,
|
|
568
|
+
): number | undefined {
|
|
569
|
+
const raw = env.SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_WARNING_DAYS?.trim();
|
|
570
|
+
if (!raw) return undefined;
|
|
571
|
+
const parsed = Number(raw);
|
|
572
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
573
|
+
return parsed;
|
|
574
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildDeletionClients } from "./admin-boot-config.ts";
|
|
3
|
+
import {
|
|
4
|
+
HttpChatServiceProvisioningClient,
|
|
5
|
+
NoopChatServiceProvisioningClient,
|
|
6
|
+
} from "./chat-service-provisioning-client.ts";
|
|
7
|
+
|
|
8
|
+
describe("buildDeletionClients", () => {
|
|
9
|
+
it("returns a no-op client when nothing is configured", () => {
|
|
10
|
+
const { chatService } = buildDeletionClients({});
|
|
11
|
+
expect(chatService).toBeInstanceOf(NoopChatServiceProvisioningClient);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("needs both the URL and the admin token for the real client", () => {
|
|
15
|
+
const { chatService } = buildDeletionClients({
|
|
16
|
+
SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN: "chat-admin",
|
|
17
|
+
});
|
|
18
|
+
expect(chatService).toBeInstanceOf(NoopChatServiceProvisioningClient);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("builds the real client when fully configured, independent of K8s provisioning", () => {
|
|
22
|
+
const { chatService } = buildDeletionClients({
|
|
23
|
+
SHIPWRIGHT_CHAT_SERVICE_URL: "http://chat.test",
|
|
24
|
+
SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN: "chat-admin",
|
|
25
|
+
});
|
|
26
|
+
expect(chatService).toBeInstanceOf(HttpChatServiceProvisioningClient);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
// admin-boot.ts holds the boot sequence and main.ts is the composition root
|
|
5
|
+
// that runs it. Calling startAdmin() would run migrations, open a DB pool and
|
|
6
|
+
// bind a socket, so both are asserted structurally against their source text
|
|
7
|
+
// (mirrors agent/src/index.boot-order.unit.test.ts).
|
|
8
|
+
const source = await Bun.file(join(import.meta.dir, "admin-boot.ts")).text();
|
|
9
|
+
const entrySource = await Bun.file(join(import.meta.dir, "main.ts")).text();
|
|
10
|
+
|
|
11
|
+
function position(marker: string): number {
|
|
12
|
+
const at = source.indexOf(marker);
|
|
13
|
+
expect(
|
|
14
|
+
at,
|
|
15
|
+
`boot marker missing from admin-boot.ts: ${marker}`,
|
|
16
|
+
).toBeGreaterThan(-1);
|
|
17
|
+
return at;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertOrdered(markers: string[]): void {
|
|
21
|
+
const positions = markers.map(position);
|
|
22
|
+
expect(positions).toEqual([...positions].sort((a, b) => a - b));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("admin-boot.ts boot sequence", () => {
|
|
26
|
+
it("keeps the documented boot order: DB, services, routes, sweepers, serve, startup policy reconcile", () => {
|
|
27
|
+
assertOrdered([
|
|
28
|
+
'initSentry({ service: "admin" })',
|
|
29
|
+
"await runMigrations();",
|
|
30
|
+
"createAdminPrismaClient(",
|
|
31
|
+
"makeTokenCrypto()",
|
|
32
|
+
"new AgentService(prisma)",
|
|
33
|
+
"const provisioner = buildProvisioner(",
|
|
34
|
+
"const resolvedExtensions = resolveAdminExtensions(",
|
|
35
|
+
'root.get("/health"',
|
|
36
|
+
'root.route("/agents", runtimeApp)',
|
|
37
|
+
'root.route("/", adminApiApp)',
|
|
38
|
+
"createAccountsApp(",
|
|
39
|
+
"mountRouteContributions(root, resolvedExtensions.apiRoutes)",
|
|
40
|
+
'root.route("/", adminUIApp)',
|
|
41
|
+
"mountRouteContributions(root, resolvedExtensions.adminUiRoutes)",
|
|
42
|
+
"new TrialExpiryWarningSweeper(",
|
|
43
|
+
"new TrialExpirySweeper(",
|
|
44
|
+
"new AccountTrialWarningSweeper(",
|
|
45
|
+
"new AccountTrialExpirySweeper(",
|
|
46
|
+
"startBackgroundPasses(resolvedExtensions.backgroundPasses)",
|
|
47
|
+
"const server = Bun.serve(",
|
|
48
|
+
"const shutdown = registerGracefulShutdown(",
|
|
49
|
+
"void runStartupPolicyReconcile(",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("feeds extension service deps into the platform app factories", () => {
|
|
54
|
+
expect(source).toContain("...resolvedExtensions.apiDeps");
|
|
55
|
+
expect(source).toContain("...resolvedExtensions.uiDeps");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("only passes agent-detail card slots when an extension contributed some", () => {
|
|
59
|
+
expect(source).toContain("resolvedExtensions.agentDetailCards.length > 0");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("keeps the platform boot free of any coding extension import", () => {
|
|
63
|
+
expect(source).not.toContain("coding-admin-extension");
|
|
64
|
+
expect(source).not.toContain("./coding/");
|
|
65
|
+
expect(source).not.toContain("AgentPhaseMethodologyService");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("exports startAdmin(extensions) from admin-boot.ts", () => {
|
|
69
|
+
expect(source).toContain("export async function startAdmin(");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("main.ts composition root", () => {
|
|
74
|
+
it("builds no services, routes or sweepers itself", () => {
|
|
75
|
+
for (const forbidden of [
|
|
76
|
+
"new Hono(",
|
|
77
|
+
"const server = Bun.serve(",
|
|
78
|
+
"setInterval(",
|
|
79
|
+
"createAdminPrismaClient(",
|
|
80
|
+
"new AgentService(",
|
|
81
|
+
]) {
|
|
82
|
+
expect(entrySource).not.toContain(forbidden);
|
|
83
|
+
}
|
|
84
|
+
expect(entrySource.split("\n").length).toBeLessThan(80);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("runs only when invoked as the main entry (import-safe)", () => {
|
|
88
|
+
expect(entrySource).toContain("if (import.meta.main) {");
|
|
89
|
+
expect(entrySource).toContain('"[admin] fatal startup error:"');
|
|
90
|
+
});
|
|
91
|
+
});
|