@app-vitals/agent-admin 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/admin-boot.ts
|
|
3
|
+
*
|
|
4
|
+
* Admin platform boot sequence, exported as startAdmin(extensions). A consumer
|
|
5
|
+
* entry (admin/src/main.ts for Shipwright) passes its AdminExtensions; with
|
|
6
|
+
* none (or hookless ones) this is exactly the bare admin platform. See
|
|
7
|
+
* admin-extension.ts for the seam and where each hook lands in the sequence.
|
|
8
|
+
*
|
|
9
|
+
* Boot order (pinned by admin-boot.boot-order.unit.test.ts):
|
|
10
|
+
* Sentry → migrations → Prisma → platform services → resolve extensions →
|
|
11
|
+
* routes (health, runtime API, admin API, accounts API, extension APIs,
|
|
12
|
+
* admin UI, extension UI) → platform sweepers → extension background
|
|
13
|
+
* passes → Bun.serve → graceful shutdown → startup policy reconcile.
|
|
14
|
+
*
|
|
15
|
+
* Route mount order (important — avoids shadowing):
|
|
16
|
+
* GET /health — liveness check (no auth, DB-independent)
|
|
17
|
+
* GET /health/ready — readiness check (no auth, DB-aware)
|
|
18
|
+
* * /agents/* — runtime API (admin key | per-agent bearer | session JWT)
|
|
19
|
+
* + admin CRUD API (admin key | per-agent bearer | session JWT)
|
|
20
|
+
* * /admin/* — admin UI (session JWT)
|
|
21
|
+
*
|
|
22
|
+
* Both runtimeApp and adminApiApp serve routes under /agents/*.
|
|
23
|
+
* They use non-overlapping sub-paths so Hono resolves them correctly:
|
|
24
|
+
* runtime → GET /agents/:id/config, GET /agents/:id/crons
|
|
25
|
+
* admin → GET /agents/:id/crons/summary (enriched with run stats)
|
|
26
|
+
* POST/PATCH/DELETE /agents/:id/envs, /crons, /tools, /tokens, /plugins
|
|
27
|
+
*
|
|
28
|
+
* This file must not import any coding module — main.ts is the only file
|
|
29
|
+
* that wires a concrete extension in.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { registerGracefulShutdown } from "@app-vitals/agent-lib/graceful-shutdown";
|
|
34
|
+
import {
|
|
35
|
+
type PoolLike,
|
|
36
|
+
routeContextMiddleware,
|
|
37
|
+
} from "@app-vitals/agent-lib/pool-observability";
|
|
38
|
+
import {
|
|
39
|
+
buildSentryInitOptions,
|
|
40
|
+
initSentry,
|
|
41
|
+
} from "@app-vitals/agent-lib/sentry";
|
|
42
|
+
import * as Sentry from "@sentry/bun";
|
|
43
|
+
import { sentry } from "@sentry/hono/bun";
|
|
44
|
+
import { Hono } from "hono";
|
|
45
|
+
import { createAccountCreatedNotifier } from "./account-created-notifier.ts";
|
|
46
|
+
import { AccountInviteService } from "./account-invites.ts";
|
|
47
|
+
import { AccountLifecycle } from "./account-lifecycle.ts";
|
|
48
|
+
import { AccountMemberService } from "./account-members.ts";
|
|
49
|
+
import { AccountOnboardingService } from "./account-onboarding.ts";
|
|
50
|
+
import { AccountTrialExpirySweeper } from "./account-trial-expiry-sweeper.ts";
|
|
51
|
+
import { AccountTrialWarningSweeper } from "./account-trial-warning-sweeper.ts";
|
|
52
|
+
import { AccountService } from "./accounts.ts";
|
|
53
|
+
import { createAccountsApp } from "./accounts-api.ts";
|
|
54
|
+
import {
|
|
55
|
+
buildDeletionClients,
|
|
56
|
+
buildProvisioner,
|
|
57
|
+
checkDbReady,
|
|
58
|
+
DEFAULT_PORT,
|
|
59
|
+
DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS,
|
|
60
|
+
isAnthropicFederationEnabled,
|
|
61
|
+
resolveTrialExpiryWarningDays,
|
|
62
|
+
runMigrations,
|
|
63
|
+
} from "./admin-boot-config.ts";
|
|
64
|
+
import {
|
|
65
|
+
type AdminExtension,
|
|
66
|
+
type AdminExtensionContext,
|
|
67
|
+
collectServiceIntegrations,
|
|
68
|
+
mountRouteContributions,
|
|
69
|
+
resolveAdminExtensions,
|
|
70
|
+
startBackgroundPasses,
|
|
71
|
+
} from "./admin-extension.ts";
|
|
72
|
+
import { createAdminUIApp } from "./admin-ui.ts";
|
|
73
|
+
import { AgentAskService } from "./agent-ask.ts";
|
|
74
|
+
import { parseAgentAskConfig } from "./agent-ask-config.ts";
|
|
75
|
+
import { AgentChatTokenService } from "./agent-chat-tokens.ts";
|
|
76
|
+
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
77
|
+
import { AgentCronRunStatsService } from "./agent-cron-run-stats.ts";
|
|
78
|
+
import { AgentCronRunService } from "./agent-cron-runs.ts";
|
|
79
|
+
import { AgentEnvService } from "./agent-envs.ts";
|
|
80
|
+
import { AgentGitHubInstallationsService } from "./agent-github-installations.ts";
|
|
81
|
+
import { AgentMemberService } from "./agent-members.ts";
|
|
82
|
+
import { AgentPluginService } from "./agent-plugins.ts";
|
|
83
|
+
import { AgentTokenService } from "./agent-tokens.ts";
|
|
84
|
+
import { AgentToolService } from "./agent-tools.ts";
|
|
85
|
+
import { AgentService } from "./agents.ts";
|
|
86
|
+
import { createAdminApp, parseAdminApiKeys } from "./agents-api.ts";
|
|
87
|
+
import { createAgentRuntimeApp } from "./api.ts";
|
|
88
|
+
import { createAdminAuthMiddleware } from "./api-auth.ts";
|
|
89
|
+
import {
|
|
90
|
+
callerScopeDepsFromServices,
|
|
91
|
+
createCallerScopeResolver,
|
|
92
|
+
} from "./caller-scope.ts";
|
|
93
|
+
import { parseChatCredentialConfig } from "./chat-credential-config.ts";
|
|
94
|
+
import { HttpChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
95
|
+
import { isDevAuthAllowed } from "./dev-auth-guard.ts";
|
|
96
|
+
import { HttpGithubAppProvisioningClient } from "./github-app-provisioning-client.ts";
|
|
97
|
+
import { HttpGoogleAuthClient } from "./google-auth-client.ts";
|
|
98
|
+
import { HttpChatClient } from "./http-chat-client.ts";
|
|
99
|
+
import { HttpOktaAuthClient } from "./okta-auth-client.ts";
|
|
100
|
+
import { mountPlatformDoc } from "./platform-doc-route.ts";
|
|
101
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
102
|
+
import { isPushEnabled } from "./push-sender.ts";
|
|
103
|
+
import { PushService } from "./push-service.ts";
|
|
104
|
+
import { parseSelfServeConfig } from "./self-serve-config.ts";
|
|
105
|
+
import { HttpSlackProvisioningClient } from "./slack-provisioning-client.ts";
|
|
106
|
+
import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
|
|
107
|
+
import { runStartupPolicyReconcile } from "./startup-policy-reconcile.ts";
|
|
108
|
+
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
109
|
+
import {
|
|
110
|
+
resolveTrialExpirySweepIntervalMs,
|
|
111
|
+
TrialExpirySweeper,
|
|
112
|
+
} from "./trial-expiry-sweeper.ts";
|
|
113
|
+
import { TrialExpiryWarningSweeper } from "./trial-expiry-warning-sweeper.ts";
|
|
114
|
+
|
|
115
|
+
// startAdmin() is the process boot: it runs the real migration preflight,
|
|
116
|
+
// constructs a real PrismaClient (live DB connection), calls Bun.serve to bind
|
|
117
|
+
// an actual socket, and mounts every sub-app. This is process-wiring / real-I/O
|
|
118
|
+
// by design — its pure logic lives in admin-boot-config.ts (unit-tested via
|
|
119
|
+
// main.unit.test.ts) and admin-extension.ts (admin-extension.unit.test.ts),
|
|
120
|
+
// and its sequence is pinned structurally by admin-boot.boot-order.unit.test.ts.
|
|
121
|
+
// It is exercised in deployed environments (and via the smoke/integration
|
|
122
|
+
// suites of the services it wires together), not by a unit test — forcing a
|
|
123
|
+
// mock-heavy unit test here would mean re-implementing
|
|
124
|
+
// Bun.serve/PrismaClient/Hono routing as fakes without gaining real coverage
|
|
125
|
+
// of the wiring behavior itself.
|
|
126
|
+
export async function startAdmin(
|
|
127
|
+
extensions: readonly AdminExtension[] = [],
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
// Initializes Sentry (no-op when SENTRY_DSN is unset) so that startup
|
|
130
|
+
// failures below (e.g. a failed migration) are captured too. See
|
|
131
|
+
// task-store's main.ts for the rationale on why re-initializing is safe.
|
|
132
|
+
initSentry({ service: "admin" });
|
|
133
|
+
|
|
134
|
+
const port = Number(process.env.PORT ?? DEFAULT_PORT);
|
|
135
|
+
|
|
136
|
+
console.log(`[admin] starting admin service on port ${port}`);
|
|
137
|
+
|
|
138
|
+
// Run DB migrations as idempotent preflight
|
|
139
|
+
await runMigrations();
|
|
140
|
+
|
|
141
|
+
// Construct PrismaClient once at boot. Prisma 7 has no built-in query
|
|
142
|
+
// engine, so this is a PrismaPg driver adapter over a pg.Pool built from
|
|
143
|
+
// DATABASE_URL_SHIPWRIGHT_ADMIN — see prisma-client.ts. An unset URL throws
|
|
144
|
+
// there (an empty connection string would otherwise make pg fall back to the
|
|
145
|
+
// PG* env vars or localhost, silently pointing the service at a different
|
|
146
|
+
// database); the top-level catch turns that into a named
|
|
147
|
+
// "[admin] fatal startup error" and exit 1, matching how Prisma 6 refused to
|
|
148
|
+
// construct a client without the env var.
|
|
149
|
+
// Pool observability (PLO-1.3): hold tracking + waiting-clients monitor on
|
|
150
|
+
// the admin pool; stopped on shutdown below.
|
|
151
|
+
let adminPool: PoolLike | undefined;
|
|
152
|
+
let stopPoolObservability: () => void = () => {};
|
|
153
|
+
const prisma = createAdminPrismaClient(
|
|
154
|
+
process.env.DATABASE_URL_SHIPWRIGHT_ADMIN ?? "",
|
|
155
|
+
{
|
|
156
|
+
onPoolObserved: ({ pool, stop }) => {
|
|
157
|
+
adminPool = pool;
|
|
158
|
+
stopPoolObservability = stop;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Construct TokenCrypto — reads SHIPWRIGHT_ENCRYPTION_KEY at call time
|
|
164
|
+
const crypto = makeTokenCrypto();
|
|
165
|
+
|
|
166
|
+
// Construct all services with injected deps
|
|
167
|
+
const agentService = new AgentService(prisma);
|
|
168
|
+
const anthropicFederationEnabled = isAnthropicFederationEnabled(process.env);
|
|
169
|
+
const agentEnvService = new AgentEnvService(
|
|
170
|
+
prisma,
|
|
171
|
+
crypto,
|
|
172
|
+
undefined,
|
|
173
|
+
anthropicFederationEnabled,
|
|
174
|
+
);
|
|
175
|
+
const agentCronJobService = new AgentCronJobService(prisma);
|
|
176
|
+
const agentCronRunService = new AgentCronRunService(prisma);
|
|
177
|
+
const agentToolService = new AgentToolService(prisma);
|
|
178
|
+
const agentTokenService = new AgentTokenService(prisma);
|
|
179
|
+
const agentPluginService = new AgentPluginService(prisma);
|
|
180
|
+
const agentMemberService = new AgentMemberService(prisma);
|
|
181
|
+
// Account-aware caller scope (SSP-2.1). Account membership contributes
|
|
182
|
+
// agents only when the self-serve flag is on; off → AgentMember-only.
|
|
183
|
+
const accountService = new AccountService(prisma);
|
|
184
|
+
const callerScopeResolver = createCallerScopeResolver(
|
|
185
|
+
callerScopeDepsFromServices({
|
|
186
|
+
accountService,
|
|
187
|
+
agentMemberService,
|
|
188
|
+
}),
|
|
189
|
+
process.env.SHIPWRIGHT_SELF_SERVE_ENABLED === "enabled",
|
|
190
|
+
);
|
|
191
|
+
const agentChatTokenService = new AgentChatTokenService(prisma);
|
|
192
|
+
const agentCronRunStatsService = new AgentCronRunStatsService(prisma);
|
|
193
|
+
const agentGitHubInstallationsService = new AgentGitHubInstallationsService(
|
|
194
|
+
prisma,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Extension-registered service integrations (task-store, via the coding
|
|
198
|
+
// extension): env-only, so resolved before the provisioner that mints their
|
|
199
|
+
// tokens and the agent deletion that revokes them.
|
|
200
|
+
const serviceIntegrations = collectServiceIntegrations(
|
|
201
|
+
extensions,
|
|
202
|
+
process.env,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Real K8s provisioner when SHIPWRIGHT_K8S_PROVISIONING=enabled, else Noop.
|
|
206
|
+
const provisioner = buildProvisioner(
|
|
207
|
+
process.env,
|
|
208
|
+
agentTokenService,
|
|
209
|
+
prisma,
|
|
210
|
+
serviceIntegrations,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Read config values at call time (no module-level env reads)
|
|
214
|
+
const sessionSecret = process.env.SHIPWRIGHT_SESSION_SECRET ?? "";
|
|
215
|
+
const googleClientId = process.env.GOOGLE_CLIENT_ID ?? "";
|
|
216
|
+
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET ?? "";
|
|
217
|
+
const oktaIssuer = process.env.OKTA_ISSUER ?? "";
|
|
218
|
+
const oktaClientId = process.env.OKTA_CLIENT_ID ?? "";
|
|
219
|
+
const oktaClientSecret = process.env.OKTA_CLIENT_SECRET ?? "";
|
|
220
|
+
const adminAllowedEmails = (process.env.SHIPWRIGHT_ADMIN_ALLOWED_EMAILS ?? "")
|
|
221
|
+
.split(",")
|
|
222
|
+
.map((e) => e.trim())
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
const appBaseUrl =
|
|
225
|
+
process.env.SHIPWRIGHT_ADMIN_APP_BASE_URL ?? `http://localhost:${port}`;
|
|
226
|
+
// Read once at startup (not per-request) and interpolated into the
|
|
227
|
+
// service worker's cache name — see admin/src/pwa.ts's
|
|
228
|
+
// buildServiceWorkerBody. Falls back to "0.0.0" if version.txt is
|
|
229
|
+
// missing so a broken read never blocks server startup.
|
|
230
|
+
const appVersion = await Bun.file(
|
|
231
|
+
join(import.meta.dir, "..", "..", "version.txt"),
|
|
232
|
+
)
|
|
233
|
+
.text()
|
|
234
|
+
.then((v) => v.trim())
|
|
235
|
+
.catch(() => "0.0.0");
|
|
236
|
+
const adminApiKeys = parseAdminApiKeys(process.env.SHIPWRIGHT_ADMIN_API_KEYS);
|
|
237
|
+
// Self-serve provisioning config (SSP-1.3) — read once at startup and
|
|
238
|
+
// injected as deps; no consumers yet.
|
|
239
|
+
const selfServe = parseSelfServeConfig(process.env);
|
|
240
|
+
// Account status transitions → cron lockdown/restore (SSP-8.2). Shared by
|
|
241
|
+
// the /accounts PATCH handler, the admin UI and the account sweepers.
|
|
242
|
+
const accountLifecycle = new AccountLifecycle({
|
|
243
|
+
accounts: accountService,
|
|
244
|
+
cronJobs: agentCronJobService,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const googleClient = new HttpGoogleAuthClient();
|
|
248
|
+
// Constructed only when an issuer is configured — HttpOktaAuthClient requires
|
|
249
|
+
// `issuer` in its constructor, and an empty issuer would produce nonsensical
|
|
250
|
+
// (double-slash-free but bogus) endpoint URLs. When unset, oktaClient stays
|
|
251
|
+
// undefined and both Okta routes redirect to /admin/login?error=server_error
|
|
252
|
+
// without attempting any external call (see admin-ui.ts's AdminUIDeps doc).
|
|
253
|
+
const oktaClient = oktaIssuer
|
|
254
|
+
? new HttpOktaAuthClient({ issuer: oktaIssuer })
|
|
255
|
+
: undefined;
|
|
256
|
+
const slackClient = new HttpSlackProvisioningClient();
|
|
257
|
+
const githubAppClient = new HttpGithubAppProvisioningClient();
|
|
258
|
+
|
|
259
|
+
// Chat-service client for deleteAgentFully() cleanup, shared by both delete
|
|
260
|
+
// entry points (DELETE /agents/:id and the admin-ui danger zone) —
|
|
261
|
+
// independent of SHIPWRIGHT_K8S_PROVISIONING (see buildDeletionClients doc
|
|
262
|
+
// comment). Extension service integrations are cleaned up before it.
|
|
263
|
+
const { chatService: deletionChatService } = buildDeletionClients(
|
|
264
|
+
process.env,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Chat-credential routes need a real chat-service client; the no-op
|
|
268
|
+
// deletion client would mint nothing, so it stays absent (→ 503) unless
|
|
269
|
+
// both env vars are set.
|
|
270
|
+
const chatCredentialsClient =
|
|
271
|
+
process.env.SHIPWRIGHT_CHAT_SERVICE_URL &&
|
|
272
|
+
process.env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN
|
|
273
|
+
? new HttpChatServiceProvisioningClient(
|
|
274
|
+
process.env.SHIPWRIGHT_CHAT_SERVICE_URL,
|
|
275
|
+
process.env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN,
|
|
276
|
+
)
|
|
277
|
+
: undefined;
|
|
278
|
+
|
|
279
|
+
// Thread/message chat client, shared by the /admin/chat UI and the agent
|
|
280
|
+
// ask route (POST /agents/:id/ask). Built only when both
|
|
281
|
+
// SHIPWRIGHT_CHAT_SERVICE_URL and SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN are
|
|
282
|
+
// set; when absent the /admin/chat routes render in degraded mode and the
|
|
283
|
+
// ask route 503s.
|
|
284
|
+
const chatServiceUrl = process.env.SHIPWRIGHT_CHAT_SERVICE_URL;
|
|
285
|
+
const chatServiceAdminToken = process.env.SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN;
|
|
286
|
+
const chatClient =
|
|
287
|
+
chatServiceUrl && chatServiceAdminToken
|
|
288
|
+
? new HttpChatClient(chatServiceUrl, chatServiceAdminToken)
|
|
289
|
+
: undefined;
|
|
290
|
+
|
|
291
|
+
// Agent-to-agent ask (COS-31.1). Throws at boot on a malformed allowlist.
|
|
292
|
+
const agentAskConfig = parseAgentAskConfig(process.env);
|
|
293
|
+
|
|
294
|
+
// Web Push (CFB-4.2). Enabled ONLY when public + private + subject are all
|
|
295
|
+
// set — a partial config must not half-enable the feature. When disabled,
|
|
296
|
+
// pushService is undefined, the push routes 503, and the toggle renders "".
|
|
297
|
+
const vapidConfig = {
|
|
298
|
+
publicKey: process.env.SHIPWRIGHT_ADMIN_VAPID_PUBLIC_KEY ?? "",
|
|
299
|
+
privateKey: process.env.SHIPWRIGHT_ADMIN_VAPID_PRIVATE_KEY ?? "",
|
|
300
|
+
subject: process.env.SHIPWRIGHT_ADMIN_VAPID_SUBJECT ?? "",
|
|
301
|
+
};
|
|
302
|
+
const pushWebhookToken = process.env.SHIPWRIGHT_ADMIN_PUSH_WEBHOOK_TOKEN;
|
|
303
|
+
// Operator ceiling for how much detail a notification may reveal on a locked
|
|
304
|
+
// screen. Defaults to "title" per the content policy.
|
|
305
|
+
const pushMaxDetail = process.env.SHIPWRIGHT_ADMIN_PUSH_MAX_DETAIL ?? "title";
|
|
306
|
+
const pushService = isPushEnabled(vapidConfig)
|
|
307
|
+
? new PushService(prisma as never, vapidConfig, fetch, pushMaxDetail)
|
|
308
|
+
: undefined;
|
|
309
|
+
|
|
310
|
+
// AEP-2.1: resolve the extensions once, after every platform service they
|
|
311
|
+
// may build on exists and before any route is mounted. With no extensions
|
|
312
|
+
// (or hookless ones) every contribution below is empty and the admin is the
|
|
313
|
+
// bare platform.
|
|
314
|
+
const extensionContext: AdminExtensionContext = {
|
|
315
|
+
env: process.env,
|
|
316
|
+
prisma,
|
|
317
|
+
sessionSecret,
|
|
318
|
+
adminApiKeys,
|
|
319
|
+
adminAllowedEmails,
|
|
320
|
+
appBaseUrl,
|
|
321
|
+
...(process.env.SENTRY_DSN ? { sentryClient: Sentry } : {}),
|
|
322
|
+
...(adminPool ? { pool: adminPool } : {}),
|
|
323
|
+
...(process.env.SHIPWRIGHT_ADMIN_TZ
|
|
324
|
+
? { timezone: process.env.SHIPWRIGHT_ADMIN_TZ }
|
|
325
|
+
: {}),
|
|
326
|
+
...(pushService
|
|
327
|
+
? {
|
|
328
|
+
push: {
|
|
329
|
+
service: pushService,
|
|
330
|
+
maxDetail: pushMaxDetail,
|
|
331
|
+
vapidPublicKey: vapidConfig.publicKey,
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
335
|
+
services: {
|
|
336
|
+
agentService,
|
|
337
|
+
agentEnvService,
|
|
338
|
+
agentCronJobService,
|
|
339
|
+
agentCronRunService,
|
|
340
|
+
agentGitHubInstallationsService,
|
|
341
|
+
agentTokenService,
|
|
342
|
+
agentMemberService,
|
|
343
|
+
accountService,
|
|
344
|
+
callerScopeResolver,
|
|
345
|
+
provisioner,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
const resolvedExtensions = resolveAdminExtensions(
|
|
349
|
+
extensions,
|
|
350
|
+
extensionContext,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Reassigned below, after Bun.serve() + registerGracefulShutdown() run —
|
|
354
|
+
// the /health/ready handler only calls this at request time (well after
|
|
355
|
+
// startAdmin() finishes its synchronous setup), so it always sees the
|
|
356
|
+
// real graceful-shutdown flag once one exists.
|
|
357
|
+
let isShuttingDown = (): boolean => false;
|
|
358
|
+
|
|
359
|
+
const root = new Hono();
|
|
360
|
+
|
|
361
|
+
// Records method + route per request so held connections are attributable
|
|
362
|
+
// (covers every sub-app, including admin UI and extension routes).
|
|
363
|
+
root.use("*", routeContextMiddleware());
|
|
364
|
+
|
|
365
|
+
// Only mounted when SENTRY_DSN is set — a complete no-op otherwise, matching
|
|
366
|
+
// the app's behavior with Sentry absent. `@sentry/hono`'s `sentry()`
|
|
367
|
+
// middleware performs its own `Sentry.init` call internally, so this stays
|
|
368
|
+
// the only Hono-level init site; it's mounted on the root app (rather than
|
|
369
|
+
// inside a single sub-app) since root is the actual process entry composing
|
|
370
|
+
// the full route tree (health, runtime API, admin API, admin UI).
|
|
371
|
+
const sentryInitOptions = buildSentryInitOptions({ service: "admin" });
|
|
372
|
+
if (sentryInitOptions) {
|
|
373
|
+
root.use("*", sentry(root, sentryInitOptions));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 1. Health checks — no auth
|
|
377
|
+
// /health — liveness: process-alive only, independent of DB state.
|
|
378
|
+
// A transient DB blip must never trigger a
|
|
379
|
+
// liveness-driven restart.
|
|
380
|
+
// /health/ready — readiness: DB-aware. Used by the chart's readinessProbe
|
|
381
|
+
// so Kubernetes doesn't route traffic to this pod before
|
|
382
|
+
// the Cloud SQL proxy sidecar has finished connecting.
|
|
383
|
+
root.get("/health", (c) => c.json({ status: "ok" }));
|
|
384
|
+
root.get("/health/ready", async (c) => {
|
|
385
|
+
// Once a shutdown signal has been received, fail readiness immediately
|
|
386
|
+
// (no DB round-trip) so Kubernetes pulls this pod from Service endpoints
|
|
387
|
+
// as early as possible — see lib/graceful-shutdown.ts.
|
|
388
|
+
if (isShuttingDown()) {
|
|
389
|
+
return c.json({ status: "unavailable" }, 503);
|
|
390
|
+
}
|
|
391
|
+
const ready = await checkDbReady(prisma);
|
|
392
|
+
return c.json({ status: ready ? "ok" : "unavailable" }, ready ? 200 : 503);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Redirect bare root to the login page — no handler exists at "/" otherwise.
|
|
396
|
+
root.get("/", (c) => c.redirect("/admin/login", 302));
|
|
397
|
+
|
|
398
|
+
// 2. Runtime API — admin key | per-agent bearer | session JWT
|
|
399
|
+
// Mounted via root.route("/agents", runtimeApp). Hono v4 strips the prefix
|
|
400
|
+
// before dispatching, so runtimeApp routes are registered as /:id/config
|
|
401
|
+
// and /:id/crons (without the /agents prefix) and resolve correctly at
|
|
402
|
+
// GET /agents/:id/config and GET /agents/:id/crons from root.
|
|
403
|
+
const runtimeApp = createAgentRuntimeApp({
|
|
404
|
+
agentEnvService,
|
|
405
|
+
agentCronJobService,
|
|
406
|
+
agentService,
|
|
407
|
+
agentPluginService,
|
|
408
|
+
...(resolvedExtensions.agentPhaseMethodologyService
|
|
409
|
+
? {
|
|
410
|
+
agentPhaseMethodologyService:
|
|
411
|
+
resolvedExtensions.agentPhaseMethodologyService,
|
|
412
|
+
}
|
|
413
|
+
: {}),
|
|
414
|
+
sessionSecret,
|
|
415
|
+
adminApiKeys,
|
|
416
|
+
agentTokenService,
|
|
417
|
+
anthropicFederationEnabled,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
root.route("/agents", runtimeApp);
|
|
421
|
+
|
|
422
|
+
// 3. Admin CRUD API — /agents/:id/* — admin key | per-agent bearer | session JWT
|
|
423
|
+
// Routes are now at /agents/:id/* (same prefix as runtime, different sub-paths).
|
|
424
|
+
// MUST be mounted before admin-ui (/admin/*) to avoid shadowing.
|
|
425
|
+
const adminApiApp = createAdminApp({
|
|
426
|
+
agentService,
|
|
427
|
+
agentEnvService,
|
|
428
|
+
agentCronJobService,
|
|
429
|
+
agentCronRunService,
|
|
430
|
+
agentCronRunStatsService,
|
|
431
|
+
agentToolService,
|
|
432
|
+
agentTokenService,
|
|
433
|
+
agentPluginService,
|
|
434
|
+
agentMemberService,
|
|
435
|
+
callerScopeResolver,
|
|
436
|
+
specialtyRegistry: new SpecialtyRegistry(),
|
|
437
|
+
agentChatTokenService,
|
|
438
|
+
agentGitHubInstallationsService,
|
|
439
|
+
prisma,
|
|
440
|
+
provisioner,
|
|
441
|
+
serviceIntegrations,
|
|
442
|
+
chatService: deletionChatService,
|
|
443
|
+
slack: slackClient,
|
|
444
|
+
decrypt: crypto.decrypt,
|
|
445
|
+
sessionSecret,
|
|
446
|
+
adminApiKeys,
|
|
447
|
+
sentryClient: process.env.SENTRY_DSN ? Sentry : undefined,
|
|
448
|
+
pool: adminPool,
|
|
449
|
+
selfServe,
|
|
450
|
+
anthropicFederationEnabled,
|
|
451
|
+
chatCredentialsEnabled: parseChatCredentialConfig(process.env).enabled,
|
|
452
|
+
...(chatCredentialsClient
|
|
453
|
+
? { chatCredentials: chatCredentialsClient }
|
|
454
|
+
: {}),
|
|
455
|
+
agentAsk: {
|
|
456
|
+
config: agentAskConfig,
|
|
457
|
+
store: new AgentAskService(prisma),
|
|
458
|
+
},
|
|
459
|
+
...(chatClient ? { chatClient } : {}),
|
|
460
|
+
...resolvedExtensions.apiDeps,
|
|
461
|
+
});
|
|
462
|
+
root.route("/", adminApiApp);
|
|
463
|
+
|
|
464
|
+
// 3a. Platform OpenAPI document — GET /doc, flag-gated (not mounted when off)
|
|
465
|
+
mountPlatformDoc(root, {
|
|
466
|
+
env: process.env,
|
|
467
|
+
auth: createAdminAuthMiddleware({
|
|
468
|
+
sessionSecret,
|
|
469
|
+
agentTokenService,
|
|
470
|
+
adminApiKeys,
|
|
471
|
+
agentMemberService,
|
|
472
|
+
callerScopeResolver,
|
|
473
|
+
}),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// 3b. Accounts API — /accounts/* — admin-only, flag-gated (SSP-5.1)
|
|
477
|
+
root.route(
|
|
478
|
+
"/",
|
|
479
|
+
createAccountsApp({
|
|
480
|
+
accountService: new AccountService(prisma),
|
|
481
|
+
accountLifecycle,
|
|
482
|
+
selfServe,
|
|
483
|
+
sessionSecret,
|
|
484
|
+
adminApiKeys,
|
|
485
|
+
agentTokenService,
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// 3c. Extension API routes — after every platform API, before the admin UI
|
|
490
|
+
// (same no-shadowing rule as the admin CRUD API above).
|
|
491
|
+
mountRouteContributions(root, resolvedExtensions.apiRoutes);
|
|
492
|
+
|
|
493
|
+
// 4. Admin UI — /admin/* — session JWT
|
|
494
|
+
// Validate SHIPWRIGHT_ADMIN_TZ at startup — toLocaleDateString/toLocaleString
|
|
495
|
+
// will throw RangeError at render time if the timezone is invalid, causing a 500
|
|
496
|
+
// on every admin page. Catch it early with a clean error and exit.
|
|
497
|
+
const adminTz = process.env.SHIPWRIGHT_ADMIN_TZ;
|
|
498
|
+
if (adminTz) {
|
|
499
|
+
try {
|
|
500
|
+
Intl.DateTimeFormat(undefined, { timeZone: adminTz });
|
|
501
|
+
} catch {
|
|
502
|
+
console.error("[admin] Invalid SHIPWRIGHT_ADMIN_TZ:", adminTz);
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const adminUIApp = createAdminUIApp({
|
|
508
|
+
prisma: prisma as never,
|
|
509
|
+
agentEnvService,
|
|
510
|
+
agentCronJobService,
|
|
511
|
+
agentToolService,
|
|
512
|
+
agentTokenService,
|
|
513
|
+
agentPluginService,
|
|
514
|
+
agentMemberService,
|
|
515
|
+
callerScopeResolver,
|
|
516
|
+
agentService,
|
|
517
|
+
provisioner,
|
|
518
|
+
anthropicFederationEnabled,
|
|
519
|
+
serviceIntegrations,
|
|
520
|
+
chatService: deletionChatService,
|
|
521
|
+
slack: slackClient,
|
|
522
|
+
decrypt: crypto.decrypt,
|
|
523
|
+
sessionSecret,
|
|
524
|
+
googleClientId,
|
|
525
|
+
googleClientSecret,
|
|
526
|
+
adminAllowedEmails,
|
|
527
|
+
googleClient,
|
|
528
|
+
oktaClientId,
|
|
529
|
+
oktaClientSecret,
|
|
530
|
+
oktaIssuer,
|
|
531
|
+
...(oktaClient ? { oktaClient } : {}),
|
|
532
|
+
slackClient,
|
|
533
|
+
githubAppClient,
|
|
534
|
+
appBaseUrl,
|
|
535
|
+
appVersion,
|
|
536
|
+
devAuthEnabled: isDevAuthAllowed(process.env),
|
|
537
|
+
selfServe,
|
|
538
|
+
accountService,
|
|
539
|
+
accountOnboarding: new AccountOnboardingService(
|
|
540
|
+
prisma,
|
|
541
|
+
selfServe.defaultMaxAgents,
|
|
542
|
+
),
|
|
543
|
+
...(() => {
|
|
544
|
+
const accountCreatedNotifier = createAccountCreatedNotifier({
|
|
545
|
+
...(pushService ? { pushService } : {}),
|
|
546
|
+
adminEmails: adminAllowedEmails,
|
|
547
|
+
});
|
|
548
|
+
return accountCreatedNotifier ? { accountCreatedNotifier } : {};
|
|
549
|
+
})(),
|
|
550
|
+
accountServices: {
|
|
551
|
+
accounts: new AccountService(prisma),
|
|
552
|
+
members: new AccountMemberService(prisma),
|
|
553
|
+
invites: new AccountInviteService(prisma),
|
|
554
|
+
lifecycle: accountLifecycle,
|
|
555
|
+
},
|
|
556
|
+
timezone: adminTz,
|
|
557
|
+
...(chatClient ? { chatClient } : {}),
|
|
558
|
+
...(pushService
|
|
559
|
+
? {
|
|
560
|
+
pushService,
|
|
561
|
+
vapidPublicKey: vapidConfig.publicKey,
|
|
562
|
+
...(pushWebhookToken ? { pushWebhookToken } : {}),
|
|
563
|
+
}
|
|
564
|
+
: {}),
|
|
565
|
+
...(resolvedExtensions.agentDetailCards.length > 0
|
|
566
|
+
? { agentDetailCards: resolvedExtensions.agentDetailCards }
|
|
567
|
+
: {}),
|
|
568
|
+
...resolvedExtensions.uiDeps,
|
|
569
|
+
});
|
|
570
|
+
root.route("/", adminUIApp);
|
|
571
|
+
|
|
572
|
+
// 4b. Extension admin UI routes — after the platform UI.
|
|
573
|
+
mountRouteContributions(root, resolvedExtensions.adminUiRoutes);
|
|
574
|
+
|
|
575
|
+
// Trial-expiry warning sweeper (ATE-2.1) — warns an agent's own Slack alert
|
|
576
|
+
// channel once, shortly BEFORE its trial lapses. Registered HERE, never
|
|
577
|
+
// inside createAdminUIApp()/createAdminApp(), which must stay
|
|
578
|
+
// side-effect-free (same rule as the session alert sweeper's registration).
|
|
579
|
+
// Unlike the session alert sweeper, this one needs no external service
|
|
580
|
+
// config to be meaningful — its only dependencies (Prisma, AgentService,
|
|
581
|
+
// AgentEnvService) are always constructed above — so it always starts.
|
|
582
|
+
{
|
|
583
|
+
const trialExpiryWarningSweeper = new TrialExpiryWarningSweeper({
|
|
584
|
+
prisma: prisma as never,
|
|
585
|
+
agentService,
|
|
586
|
+
agentEnvService,
|
|
587
|
+
warningDays: resolveTrialExpiryWarningDays(process.env),
|
|
588
|
+
});
|
|
589
|
+
setInterval(() => {
|
|
590
|
+
trialExpiryWarningSweeper
|
|
591
|
+
.tick()
|
|
592
|
+
.catch((err) =>
|
|
593
|
+
console.error("[trial-expiry-warning-sweeper] tick error:", err),
|
|
594
|
+
);
|
|
595
|
+
}, DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS);
|
|
596
|
+
console.log(
|
|
597
|
+
`[admin] trial expiry warning sweeper started (interval: ${DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS}ms)`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Trial-expiry sweeper (ATE-3.1) — the lockdown half, running AFTER the
|
|
602
|
+
// warning sweeper above has had its say: disables every enabled AgentCronJob
|
|
603
|
+
// belonging to an agent whose trialExpiresAt has passed. Registered HERE,
|
|
604
|
+
// never inside createAdminUIApp(), same rule as the session alert sweeper
|
|
605
|
+
// above. Reuses the same agentCronJobService instance constructed earlier
|
|
606
|
+
// in this function — no HTTP self-call, just the service's setEnabled(),
|
|
607
|
+
// the identical method the PATCH /agents/:id/crons/:cronId route calls.
|
|
608
|
+
// Deliberately does NOT deprovision the agent (no deleteAgentFully()) — see
|
|
609
|
+
// trial-expiry-sweeper.ts's header comment.
|
|
610
|
+
{
|
|
611
|
+
const trialExpirySweeper = new TrialExpirySweeper({ agentCronJobService });
|
|
612
|
+
const trialExpirySweepIntervalMs = resolveTrialExpirySweepIntervalMs(
|
|
613
|
+
process.env,
|
|
614
|
+
);
|
|
615
|
+
setInterval(() => {
|
|
616
|
+
trialExpirySweeper
|
|
617
|
+
.tick()
|
|
618
|
+
.catch((err) =>
|
|
619
|
+
console.error("[trial-expiry-sweeper] tick error:", err),
|
|
620
|
+
);
|
|
621
|
+
}, trialExpirySweepIntervalMs);
|
|
622
|
+
console.log(
|
|
623
|
+
`[admin] trial expiry sweeper started (interval: ${trialExpirySweepIntervalMs}ms)`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Account trial sweepers (SSP-8.2) — the account-level counterparts of the
|
|
628
|
+
// two per-agent sweepers above (which keep running unchanged). Flag-gated:
|
|
629
|
+
// accounts only exist when self-serve is enabled. Same cadences as the
|
|
630
|
+
// per-agent pair: hourly warning, minutely expiry/lockdown. Registered
|
|
631
|
+
// HERE, never inside an app factory. No deleteAgentFully call anywhere.
|
|
632
|
+
if (selfServe.enabled) {
|
|
633
|
+
const accountTrialWarningSweeper = new AccountTrialWarningSweeper({
|
|
634
|
+
accounts: accountService,
|
|
635
|
+
agentEnvService,
|
|
636
|
+
warningDays: resolveTrialExpiryWarningDays(process.env),
|
|
637
|
+
});
|
|
638
|
+
setInterval(() => {
|
|
639
|
+
accountTrialWarningSweeper
|
|
640
|
+
.tick()
|
|
641
|
+
.catch((err) =>
|
|
642
|
+
console.error("[account-trial-warning-sweeper] tick error:", err),
|
|
643
|
+
);
|
|
644
|
+
}, DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS);
|
|
645
|
+
|
|
646
|
+
const accountTrialExpirySweeper = new AccountTrialExpirySweeper({
|
|
647
|
+
accounts: accountService,
|
|
648
|
+
lifecycle: accountLifecycle,
|
|
649
|
+
});
|
|
650
|
+
const accountTrialExpiryIntervalMs = resolveTrialExpirySweepIntervalMs(
|
|
651
|
+
process.env,
|
|
652
|
+
);
|
|
653
|
+
setInterval(() => {
|
|
654
|
+
accountTrialExpirySweeper
|
|
655
|
+
.tick()
|
|
656
|
+
.catch((err) =>
|
|
657
|
+
console.error("[account-trial-expiry-sweeper] tick error:", err),
|
|
658
|
+
);
|
|
659
|
+
}, accountTrialExpiryIntervalMs);
|
|
660
|
+
console.log(
|
|
661
|
+
`[admin] account trial sweepers started (warning: ${DEFAULT_TRIAL_EXPIRY_WARNING_SWEEP_INTERVAL_MS}ms, expiry: ${accountTrialExpiryIntervalMs}ms)`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Extension background passes (AEP-2.1) — after every platform sweeper,
|
|
666
|
+
// same "registered here, never inside an app factory" rule.
|
|
667
|
+
startBackgroundPasses(resolvedExtensions.backgroundPasses);
|
|
668
|
+
|
|
669
|
+
const server = Bun.serve({ fetch: root.fetch, port });
|
|
670
|
+
|
|
671
|
+
const shutdown = registerGracefulShutdown({
|
|
672
|
+
server,
|
|
673
|
+
cleanup: [async () => stopPoolObservability(), () => prisma.$disconnect()],
|
|
674
|
+
serviceName: "admin",
|
|
675
|
+
});
|
|
676
|
+
isShuttingDown = shutdown.isShuttingDown;
|
|
677
|
+
|
|
678
|
+
console.log(`[admin] admin service listening on port ${port}`);
|
|
679
|
+
|
|
680
|
+
// Policy-only reconcile (ACP-1.3): a Claude policy change restarts admin via
|
|
681
|
+
// the chart's policy-hash env var, so rolling only the agents whose policy
|
|
682
|
+
// wiring drifted happens here. Runs even with no policy configured so agents
|
|
683
|
+
// lose the mount once it is disabled. Fire-and-forget: it never throws and
|
|
684
|
+
// must not delay serving.
|
|
685
|
+
void runStartupPolicyReconcile({
|
|
686
|
+
agentService,
|
|
687
|
+
provisioner,
|
|
688
|
+
logger: console,
|
|
689
|
+
});
|
|
690
|
+
}
|