@app-vitals/agent-admin 0.0.0-stage → 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 +64 -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,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/admin-extension.ts
|
|
3
|
+
*
|
|
4
|
+
* The extension seam between the generic admin platform (admin-boot.ts:
|
|
5
|
+
* agents, envs, crons, tokens, plugins, accounts, provisioning, the admin UI
|
|
6
|
+
* shell and its sweepers) and specialty-specific admin behavior (Shipwright:
|
|
7
|
+
* the coding extension). Mirrors the agent's seam in
|
|
8
|
+
* agent/src/runtime/agent-extension.ts.
|
|
9
|
+
*
|
|
10
|
+
* startAdmin(extensions) resolves every extension once, right after the
|
|
11
|
+
* platform services exist and before any route is mounted, calling each hook
|
|
12
|
+
* across all extensions in ADMIN_EXTENSION_HOOK_ORDER:
|
|
13
|
+
*
|
|
14
|
+
* serviceIntegrations(env) — optional per-agent service integrations
|
|
15
|
+
* (service-integration.ts). Unlike the hooks below
|
|
16
|
+
* it takes only env and is resolved BEFORE the
|
|
17
|
+
* provisioner is built, since the provisioner and
|
|
18
|
+
* agent deletion consume it
|
|
19
|
+
* services(ctx) — extra service deps spread into createAdminApp /
|
|
20
|
+
* createAdminUIApp (later extensions win)
|
|
21
|
+
* apiRoutes(ctx) — API sub-apps, mounted after the platform APIs and
|
|
22
|
+
* before the admin UI (so the UI never shadows them)
|
|
23
|
+
* adminUiRoutes(ctx) — admin UI sub-apps, mounted after the platform UI
|
|
24
|
+
* agentDetailCards(ctx) — extra cards on the agent detail page, each placed
|
|
25
|
+
* in the header or the Configuration group
|
|
26
|
+
* backgroundPasses(ctx) — interval loops, started after the platform sweepers
|
|
27
|
+
*
|
|
28
|
+
* openApi() is static (no context): scripts/generate-admin-spec.ts calls it to
|
|
29
|
+
* fold extension routes/schemas into admin/openapi.json without booting.
|
|
30
|
+
*
|
|
31
|
+
* Every hook is optional; no extension (or a hookless one) reproduces the bare
|
|
32
|
+
* platform exactly. This file must not import any coding module — main.ts
|
|
33
|
+
* (the composition root) is the only file that wires a concrete extension in.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { PoolLike } from "@app-vitals/agent-lib/pool-observability";
|
|
37
|
+
import type { ErrorCapturingClient } from "@app-vitals/agent-lib/sentry";
|
|
38
|
+
import type { Hono } from "hono";
|
|
39
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
40
|
+
import type { AccountService } from "./accounts.ts";
|
|
41
|
+
import type { AdminUIDeps } from "./admin-ui.ts";
|
|
42
|
+
import type { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
43
|
+
import type { AgentCronRunService } from "./agent-cron-runs.ts";
|
|
44
|
+
import type { AgentEnvService } from "./agent-envs.ts";
|
|
45
|
+
import type { AgentGitHubInstallationsService } from "./agent-github-installations.ts";
|
|
46
|
+
import type { AgentMemberService } from "./agent-members.ts";
|
|
47
|
+
import type { AgentProvisioner } from "./agent-provisioner.ts";
|
|
48
|
+
import type { AgentTokenService } from "./agent-tokens.ts";
|
|
49
|
+
import type { AgentService } from "./agents.ts";
|
|
50
|
+
import type { AdminDeps, parseAdminApiKeys } from "./agents-api.ts";
|
|
51
|
+
import type { AgentPhaseMethodologyServiceLike } from "./api.ts";
|
|
52
|
+
import type { createCallerScopeResolver } from "./caller-scope.ts";
|
|
53
|
+
import type { PushService } from "./push-service.ts";
|
|
54
|
+
import type { ServiceIntegration } from "./service-integration.ts";
|
|
55
|
+
|
|
56
|
+
/** The platform services an extension may build on. */
|
|
57
|
+
export interface AdminPlatformServices {
|
|
58
|
+
agentService: AgentService;
|
|
59
|
+
agentEnvService: AgentEnvService;
|
|
60
|
+
agentCronJobService: AgentCronJobService;
|
|
61
|
+
agentCronRunService: AgentCronRunService;
|
|
62
|
+
agentGitHubInstallationsService: AgentGitHubInstallationsService;
|
|
63
|
+
agentTokenService: AgentTokenService;
|
|
64
|
+
agentMemberService: AgentMemberService;
|
|
65
|
+
accountService: AccountService;
|
|
66
|
+
callerScopeResolver: ReturnType<typeof createCallerScopeResolver>;
|
|
67
|
+
provisioner: AgentProvisioner;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Everything an extension's hooks may need from the platform. */
|
|
71
|
+
export interface AdminExtensionContext {
|
|
72
|
+
env: NodeJS.ProcessEnv;
|
|
73
|
+
prisma: PrismaClient;
|
|
74
|
+
sessionSecret: string;
|
|
75
|
+
adminApiKeys: ReturnType<typeof parseAdminApiKeys>;
|
|
76
|
+
/** SHIPWRIGHT_ADMIN_ALLOWED_EMAILS, parsed: operators who see everything. */
|
|
77
|
+
adminAllowedEmails: string[];
|
|
78
|
+
/** The admin UI's public base URL (SHIPWRIGHT_ADMIN_APP_BASE_URL). */
|
|
79
|
+
appBaseUrl: string;
|
|
80
|
+
/** SHIPWRIGHT_ADMIN_TZ, when set: the admin UI's display timezone. */
|
|
81
|
+
timezone?: string;
|
|
82
|
+
/** Web Push, present only when VAPID is fully configured. */
|
|
83
|
+
push?: { service: PushService; maxDetail: string; vapidPublicKey: string };
|
|
84
|
+
/** Sentry, present only when SENTRY_DSN is set (as the admin API gets it). */
|
|
85
|
+
sentryClient?: ErrorCapturingClient;
|
|
86
|
+
/** Admin's pg pool (PLO-1.3), for pool-timeout capture context. */
|
|
87
|
+
pool?: PoolLike;
|
|
88
|
+
services: AdminPlatformServices;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Extension sub-apps carry their own Env/Schema generics; Hono's route() is
|
|
92
|
+
// generic over them, so the contribution type must accept any.
|
|
93
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above
|
|
94
|
+
type AnyHono = Hono<any, any, any>;
|
|
95
|
+
|
|
96
|
+
/** A sub-app mounted on the root at `path` via root.route(path, app). */
|
|
97
|
+
export interface AdminRouteContribution {
|
|
98
|
+
path: string;
|
|
99
|
+
app: AnyHono;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Extra deps merged over the platform's createAdminApp / createAdminUIApp deps. */
|
|
103
|
+
export interface AdminExtensionServices {
|
|
104
|
+
api?: Partial<AdminDeps>;
|
|
105
|
+
ui?: Partial<AdminUIDeps>;
|
|
106
|
+
/**
|
|
107
|
+
* Supplies the runtime API's phase-methodology service (GET
|
|
108
|
+
* /agents/:id/config's `phaseMethodology`). Absent from every extension →
|
|
109
|
+
* the runtime app omits the field.
|
|
110
|
+
*/
|
|
111
|
+
agentPhaseMethodologyService?: AgentPhaseMethodologyServiceLike;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface AgentDetailCardContext {
|
|
115
|
+
agentId: string;
|
|
116
|
+
userEmail: string;
|
|
117
|
+
isAdmin: boolean;
|
|
118
|
+
/** The agent's owning account; null for unassigned agents. */
|
|
119
|
+
accountId?: string | null;
|
|
120
|
+
/** The admin UI's display timezone (IANA name). */
|
|
121
|
+
timezone?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Where a card lands on the agent detail page:
|
|
126
|
+
* header — above the stat strip, after the page's alerts (default)
|
|
127
|
+
* configuration — inside the Configuration group, after Repos
|
|
128
|
+
*/
|
|
129
|
+
export type AgentDetailCardPlacement = "header" | "configuration";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One extra card on GET /admin/agents/:id. render() returns trusted HTML — the
|
|
133
|
+
* extension escapes its own content. A throwing render omits just that card.
|
|
134
|
+
*/
|
|
135
|
+
export interface AgentDetailCardSlot {
|
|
136
|
+
name: string;
|
|
137
|
+
/** Defaults to "header". */
|
|
138
|
+
placement?: AgentDetailCardPlacement;
|
|
139
|
+
render(ctx: AgentDetailCardContext): Promise<string>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** An interval loop registered in startAdmin, never inside an app factory. */
|
|
143
|
+
export interface AdminBackgroundPass {
|
|
144
|
+
name: string;
|
|
145
|
+
/** Prefix for the non-fatal failure log: `${logPrefix} tick error:`. */
|
|
146
|
+
logPrefix: string;
|
|
147
|
+
intervalMs: number;
|
|
148
|
+
tick(): Promise<unknown>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** OpenAPI fragments folded into the admin document (paths use `{param}`). */
|
|
152
|
+
export interface AdminOpenApiContribution {
|
|
153
|
+
paths?: Record<string, Record<string, unknown>>;
|
|
154
|
+
schemas?: Record<string, unknown>;
|
|
155
|
+
/**
|
|
156
|
+
* Where new paths/schemas land in the document: contributed key → the key
|
|
157
|
+
* it is inserted directly after (a platform key, or a contributed key placed
|
|
158
|
+
* earlier). New keys without a placement are appended. Lets an extension's
|
|
159
|
+
* routes keep a stable position in the generated document.
|
|
160
|
+
*/
|
|
161
|
+
placeAfter?: {
|
|
162
|
+
paths?: Record<string, string>;
|
|
163
|
+
schemas?: Record<string, string>;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface AdminExtension {
|
|
168
|
+
name?: string;
|
|
169
|
+
/** See the header: env-only, resolved before the provisioner is built. */
|
|
170
|
+
serviceIntegrations?(env: NodeJS.ProcessEnv): ServiceIntegration[];
|
|
171
|
+
services?(ctx: AdminExtensionContext): AdminExtensionServices;
|
|
172
|
+
apiRoutes?(ctx: AdminExtensionContext): AdminRouteContribution[];
|
|
173
|
+
adminUiRoutes?(ctx: AdminExtensionContext): AdminRouteContribution[];
|
|
174
|
+
agentDetailCards?(ctx: AdminExtensionContext): AgentDetailCardSlot[];
|
|
175
|
+
backgroundPasses?(ctx: AdminExtensionContext): AdminBackgroundPass[];
|
|
176
|
+
openApi?(): AdminOpenApiContribution;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The order resolveAdminExtensions calls the context hooks in. */
|
|
180
|
+
export const ADMIN_EXTENSION_HOOK_ORDER = [
|
|
181
|
+
"services",
|
|
182
|
+
"apiRoutes",
|
|
183
|
+
"adminUiRoutes",
|
|
184
|
+
"agentDetailCards",
|
|
185
|
+
"backgroundPasses",
|
|
186
|
+
] as const;
|
|
187
|
+
|
|
188
|
+
export interface ResolvedAdminExtensions {
|
|
189
|
+
apiDeps: Partial<AdminDeps>;
|
|
190
|
+
uiDeps: Partial<AdminUIDeps>;
|
|
191
|
+
agentPhaseMethodologyService?: AgentPhaseMethodologyServiceLike;
|
|
192
|
+
apiRoutes: AdminRouteContribution[];
|
|
193
|
+
adminUiRoutes: AdminRouteContribution[];
|
|
194
|
+
agentDetailCards: AgentDetailCardSlot[];
|
|
195
|
+
backgroundPasses: AdminBackgroundPass[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calls every context hook once, hook by hook (ADMIN_EXTENSION_HOOK_ORDER),
|
|
200
|
+
* each across all extensions in order. Lists concatenate; service deps merge
|
|
201
|
+
* with later extensions winning.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveAdminExtensions(
|
|
204
|
+
extensions: readonly AdminExtension[],
|
|
205
|
+
ctx: AdminExtensionContext,
|
|
206
|
+
): ResolvedAdminExtensions {
|
|
207
|
+
const services = extensions.map((ext) => ext.services?.(ctx) ?? {});
|
|
208
|
+
const collect = <T>(hook: (ext: AdminExtension) => T[] | undefined): T[] =>
|
|
209
|
+
extensions.flatMap((ext) => hook(ext) ?? []);
|
|
210
|
+
const apiRoutes = collect((ext) => ext.apiRoutes?.(ctx));
|
|
211
|
+
const adminUiRoutes = collect((ext) => ext.adminUiRoutes?.(ctx));
|
|
212
|
+
const agentDetailCards = collect((ext) => ext.agentDetailCards?.(ctx));
|
|
213
|
+
const backgroundPasses = collect((ext) => ext.backgroundPasses?.(ctx));
|
|
214
|
+
const agentPhaseMethodologyService = services
|
|
215
|
+
.map((s) => s.agentPhaseMethodologyService)
|
|
216
|
+
.filter((svc) => svc !== undefined)
|
|
217
|
+
.at(-1);
|
|
218
|
+
return {
|
|
219
|
+
...(agentPhaseMethodologyService ? { agentPhaseMethodologyService } : {}),
|
|
220
|
+
apiDeps: Object.assign({}, ...services.map((s) => s.api ?? {})),
|
|
221
|
+
uiDeps: Object.assign({}, ...services.map((s) => s.ui ?? {})),
|
|
222
|
+
apiRoutes,
|
|
223
|
+
adminUiRoutes,
|
|
224
|
+
agentDetailCards,
|
|
225
|
+
backgroundPasses,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Merges every extension's static OpenAPI contribution, in order. Placements
|
|
231
|
+
* are included only when some extension declared one.
|
|
232
|
+
*/
|
|
233
|
+
export function collectOpenApiContributions(
|
|
234
|
+
extensions: readonly AdminExtension[],
|
|
235
|
+
): Required<Pick<AdminOpenApiContribution, "paths" | "schemas">> &
|
|
236
|
+
Pick<AdminOpenApiContribution, "placeAfter"> {
|
|
237
|
+
const paths: Record<string, Record<string, unknown>> = {};
|
|
238
|
+
const schemas: Record<string, unknown> = {};
|
|
239
|
+
const pathsAfter: Record<string, string> = {};
|
|
240
|
+
const schemasAfter: Record<string, string> = {};
|
|
241
|
+
for (const ext of extensions) {
|
|
242
|
+
const contribution = ext.openApi?.() ?? {};
|
|
243
|
+
for (const [path, methods] of Object.entries(contribution.paths ?? {})) {
|
|
244
|
+
paths[path] = { ...paths[path], ...methods };
|
|
245
|
+
}
|
|
246
|
+
Object.assign(schemas, contribution.schemas);
|
|
247
|
+
Object.assign(pathsAfter, contribution.placeAfter?.paths);
|
|
248
|
+
Object.assign(schemasAfter, contribution.placeAfter?.schemas);
|
|
249
|
+
}
|
|
250
|
+
const placed =
|
|
251
|
+
Object.keys(pathsAfter).length > 0 || Object.keys(schemasAfter).length > 0;
|
|
252
|
+
return {
|
|
253
|
+
paths,
|
|
254
|
+
schemas,
|
|
255
|
+
...(placed
|
|
256
|
+
? { placeAfter: { paths: pathsAfter, schemas: schemasAfter } }
|
|
257
|
+
: {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Merges `extra` into `base` keeping base's key order: an existing key keeps
|
|
263
|
+
* its position (its value merged via `mergeValue`), a new key is inserted
|
|
264
|
+
* right after its `after` anchor or, unplaced, appended. An anchor that is not
|
|
265
|
+
* in the document is a programming error and throws.
|
|
266
|
+
*/
|
|
267
|
+
function mergeOrdered<V>(
|
|
268
|
+
base: Record<string, V>,
|
|
269
|
+
extra: Record<string, V>,
|
|
270
|
+
after: Record<string, string> | undefined,
|
|
271
|
+
mergeValue: (existing: V, added: V) => V,
|
|
272
|
+
): Record<string, V> {
|
|
273
|
+
const keys = Object.keys(base);
|
|
274
|
+
const values: Record<string, V> = { ...base };
|
|
275
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
276
|
+
if (Object.hasOwn(values, key)) {
|
|
277
|
+
values[key] = mergeValue(values[key] as V, value);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const anchor = after?.[key];
|
|
281
|
+
if (anchor === undefined) {
|
|
282
|
+
keys.push(key);
|
|
283
|
+
} else {
|
|
284
|
+
const at = keys.indexOf(anchor);
|
|
285
|
+
if (at === -1) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`OpenAPI contribution places "${key}" after unknown "${anchor}"`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
keys.splice(at + 1, 0, key);
|
|
291
|
+
}
|
|
292
|
+
values[key] = value;
|
|
293
|
+
}
|
|
294
|
+
return Object.fromEntries(keys.map((key) => [key, values[key] as V]));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
interface OpenApiDocLike {
|
|
298
|
+
paths?: Record<string, Record<string, unknown>>;
|
|
299
|
+
components?: { schemas?: Record<string, unknown> };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Folds a contribution into an OpenAPI document. An empty contribution returns
|
|
304
|
+
* the input itself, so the generated spec stays byte-identical. Paths merge at
|
|
305
|
+
* the method level (like the runtime/admin merge in generate-admin-spec.ts);
|
|
306
|
+
* new paths and schemas land at their `placeAfter` anchors, else at the end.
|
|
307
|
+
*/
|
|
308
|
+
export function applyOpenApiContributions<T extends OpenApiDocLike>(
|
|
309
|
+
doc: T,
|
|
310
|
+
contribution: AdminOpenApiContribution,
|
|
311
|
+
): T {
|
|
312
|
+
const extraPaths = Object.entries(contribution.paths ?? {});
|
|
313
|
+
const extraSchemas = contribution.schemas ?? {};
|
|
314
|
+
if (extraPaths.length === 0 && Object.keys(extraSchemas).length === 0) {
|
|
315
|
+
return doc;
|
|
316
|
+
}
|
|
317
|
+
const paths = mergeOrdered(
|
|
318
|
+
doc.paths ?? {},
|
|
319
|
+
Object.fromEntries(extraPaths),
|
|
320
|
+
contribution.placeAfter?.paths,
|
|
321
|
+
(existing, added) => ({ ...existing, ...added }),
|
|
322
|
+
);
|
|
323
|
+
const schemas = mergeOrdered(
|
|
324
|
+
doc.components?.schemas ?? {},
|
|
325
|
+
extraSchemas,
|
|
326
|
+
contribution.placeAfter?.schemas,
|
|
327
|
+
(_existing, added) => added,
|
|
328
|
+
);
|
|
329
|
+
return {
|
|
330
|
+
...doc,
|
|
331
|
+
paths,
|
|
332
|
+
components: { ...(doc.components ?? {}), schemas },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Collects every extension's service integrations, in extension order. Resolved
|
|
338
|
+
* before the provisioner exists, so it sees only the environment.
|
|
339
|
+
*/
|
|
340
|
+
export function collectServiceIntegrations(
|
|
341
|
+
extensions: readonly AdminExtension[],
|
|
342
|
+
env: NodeJS.ProcessEnv,
|
|
343
|
+
): ServiceIntegration[] {
|
|
344
|
+
return extensions.flatMap((ext) => ext.serviceIntegrations?.(env) ?? []);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Mounts each contribution on root, in order. */
|
|
348
|
+
export function mountRouteContributions(
|
|
349
|
+
root: AnyHono,
|
|
350
|
+
routes: readonly AdminRouteContribution[],
|
|
351
|
+
): void {
|
|
352
|
+
for (const { path, app } of routes) {
|
|
353
|
+
root.route(path, app);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface BackgroundPassRuntime {
|
|
358
|
+
schedule(fn: () => void, intervalMs: number): unknown;
|
|
359
|
+
log(message: string): void;
|
|
360
|
+
error(...args: unknown[]): void;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const defaultRuntime: BackgroundPassRuntime = {
|
|
364
|
+
schedule: (fn, intervalMs) => setInterval(fn, intervalMs),
|
|
365
|
+
log: (message) => console.log(message),
|
|
366
|
+
error: (...args) => console.error(...args),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Starts each pass on its own interval, same shape as the platform sweepers:
|
|
371
|
+
* a failing tick is logged with the pass's prefix and never throws.
|
|
372
|
+
*/
|
|
373
|
+
export function startBackgroundPasses(
|
|
374
|
+
passes: readonly AdminBackgroundPass[],
|
|
375
|
+
runtime: BackgroundPassRuntime = defaultRuntime,
|
|
376
|
+
): void {
|
|
377
|
+
for (const pass of passes) {
|
|
378
|
+
runtime.schedule(() => {
|
|
379
|
+
pass
|
|
380
|
+
.tick()
|
|
381
|
+
.catch((err) => runtime.error(`${pass.logPrefix} tick error:`, err));
|
|
382
|
+
}, pass.intervalMs);
|
|
383
|
+
runtime.log(
|
|
384
|
+
`[admin] ${pass.name} started (interval: ${pass.intervalMs}ms)`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Renders card slots in order; a throwing slot is logged and omitted. */
|
|
390
|
+
export async function renderAgentDetailCards(
|
|
391
|
+
slots: readonly AgentDetailCardSlot[],
|
|
392
|
+
ctx: AgentDetailCardContext,
|
|
393
|
+
): Promise<string[]> {
|
|
394
|
+
const rendered = await Promise.all(
|
|
395
|
+
slots.map((slot) =>
|
|
396
|
+
slot.render(ctx).catch((err) => {
|
|
397
|
+
console.error(
|
|
398
|
+
`[admin] agent detail card "${slot.name}" failed to render:`,
|
|
399
|
+
err,
|
|
400
|
+
);
|
|
401
|
+
return undefined;
|
|
402
|
+
}),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
return rendered.filter((html): html is string => html !== undefined);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Renders the slots (renderAgentDetailCards semantics: in order, a throwing
|
|
410
|
+
* slot omitted) and groups the HTML by placement. Slots whose placement is not
|
|
411
|
+
* in `placements` are not rendered: each holds its place as an empty string,
|
|
412
|
+
* so the page lays out exactly as a full render whose cards came back empty.
|
|
413
|
+
*/
|
|
414
|
+
export async function renderAgentDetailCardSlots(
|
|
415
|
+
slots: readonly AgentDetailCardSlot[],
|
|
416
|
+
ctx: AgentDetailCardContext,
|
|
417
|
+
placements: readonly AgentDetailCardPlacement[] = ["header", "configuration"],
|
|
418
|
+
): Promise<Record<AgentDetailCardPlacement, string[]>> {
|
|
419
|
+
const at = async (placement: AgentDetailCardPlacement) => {
|
|
420
|
+
const placed = slots.filter(
|
|
421
|
+
(slot) => (slot.placement ?? "header") === placement,
|
|
422
|
+
);
|
|
423
|
+
return placements.includes(placement)
|
|
424
|
+
? renderAgentDetailCards(placed, ctx)
|
|
425
|
+
: placed.map(() => "");
|
|
426
|
+
};
|
|
427
|
+
const [header, configuration] = await Promise.all([
|
|
428
|
+
at("header"),
|
|
429
|
+
at("configuration"),
|
|
430
|
+
]);
|
|
431
|
+
return { header, configuration };
|
|
432
|
+
}
|