@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,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-deletion.integration.test.ts
|
|
3
|
+
* Integration tests for deleteAgentFully() against a real PostgreSQL DB.
|
|
4
|
+
*
|
|
5
|
+
* Unlike agent-deletion.unit.test.ts (fully-mocked Prisma double), this suite
|
|
6
|
+
* passes a REAL PrismaClient for the DB-touching parts of the orchestration —
|
|
7
|
+
* proving the Agent row and every child row actually cascade-delete together
|
|
8
|
+
* (or don't, on failure) against the real schema's `onDelete: Cascade` FKs.
|
|
9
|
+
* The non-DB dependencies (provisioner/taskStore/chatService/slack/decrypt)
|
|
10
|
+
* are hand-written no-op stubs — they are not the thing under test here.
|
|
11
|
+
*
|
|
12
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
16
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
17
|
+
import {
|
|
18
|
+
type DeleteAgentFullyDeps,
|
|
19
|
+
deleteAgentFully,
|
|
20
|
+
} from "./agent-deletion.ts";
|
|
21
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
22
|
+
|
|
23
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
24
|
+
|
|
25
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
26
|
+
|
|
27
|
+
function makePrisma(): PrismaClient {
|
|
28
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
29
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function createAgent(
|
|
33
|
+
prisma: PrismaClient,
|
|
34
|
+
name = "Test Agent",
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
const agent = await prisma.agent.create({ data: { name } });
|
|
37
|
+
return agent.id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Populate one row in every child model that carries (directly or
|
|
42
|
+
* transitively) an agentId FK, so a single "full-fixture" agent exercises the
|
|
43
|
+
* entire cascade — including the two-hop Agent → AgentCronJob → AgentCronRun
|
|
44
|
+
* path.
|
|
45
|
+
*/
|
|
46
|
+
async function seedFullFixture(
|
|
47
|
+
prisma: PrismaClient,
|
|
48
|
+
agentId: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
await prisma.agentEnv.create({
|
|
51
|
+
data: { agentId, key: "GH_TOKEN", value: "encrypted-value", secret: true },
|
|
52
|
+
});
|
|
53
|
+
await prisma.agentToken.create({
|
|
54
|
+
data: { agentId, token: "a".repeat(64), label: "CI Token" },
|
|
55
|
+
});
|
|
56
|
+
const cronJob = await prisma.agentCronJob.create({
|
|
57
|
+
data: {
|
|
58
|
+
agentId,
|
|
59
|
+
schedule: "0 9 * * *",
|
|
60
|
+
prompt: "Good morning",
|
|
61
|
+
channel: "C123456",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await prisma.agentCronRun.create({
|
|
65
|
+
data: {
|
|
66
|
+
cronId: cronJob.id,
|
|
67
|
+
agentId,
|
|
68
|
+
startedAt: new Date(),
|
|
69
|
+
completedAt: new Date(),
|
|
70
|
+
skipped: false,
|
|
71
|
+
outcome: "success",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
await prisma.agentTool.create({
|
|
75
|
+
data: { agentId, pattern: "Read" },
|
|
76
|
+
});
|
|
77
|
+
await prisma.agentPlugin.create({
|
|
78
|
+
data: { agentId, name: "@shipwright/plugin" },
|
|
79
|
+
});
|
|
80
|
+
await prisma.agentMember.create({
|
|
81
|
+
data: { agentId, email: "member@example.com" },
|
|
82
|
+
});
|
|
83
|
+
await prisma.agentChatTokenUsageDailyByModel.create({
|
|
84
|
+
data: {
|
|
85
|
+
agentId,
|
|
86
|
+
date: "2026-07-14",
|
|
87
|
+
model: "claude-sonnet-4-5",
|
|
88
|
+
inputTokens: 100,
|
|
89
|
+
outputTokens: 50,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface FixtureCounts {
|
|
95
|
+
agent: number;
|
|
96
|
+
agentEnv: number;
|
|
97
|
+
agentToken: number;
|
|
98
|
+
agentCronJob: number;
|
|
99
|
+
agentCronRun: number;
|
|
100
|
+
agentTool: number;
|
|
101
|
+
agentPlugin: number;
|
|
102
|
+
agentMember: number;
|
|
103
|
+
agentChatTokenUsageDailyByModel: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function countFixtureRows(
|
|
107
|
+
prisma: PrismaClient,
|
|
108
|
+
agentId: string,
|
|
109
|
+
): Promise<FixtureCounts> {
|
|
110
|
+
const where = { where: { agentId } };
|
|
111
|
+
return {
|
|
112
|
+
agent: (await prisma.agent.findUnique({ where: { id: agentId } })) ? 1 : 0,
|
|
113
|
+
agentEnv: await prisma.agentEnv.count(where),
|
|
114
|
+
agentToken: await prisma.agentToken.count(where),
|
|
115
|
+
agentCronJob: await prisma.agentCronJob.count(where),
|
|
116
|
+
agentCronRun: await prisma.agentCronRun.count(where),
|
|
117
|
+
agentTool: await prisma.agentTool.count(where),
|
|
118
|
+
agentPlugin: await prisma.agentPlugin.count(where),
|
|
119
|
+
agentMember: await prisma.agentMember.count(where),
|
|
120
|
+
agentChatTokenUsageDailyByModel:
|
|
121
|
+
await prisma.agentChatTokenUsageDailyByModel.count(where),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ALL_ZERO: FixtureCounts = {
|
|
126
|
+
agent: 0,
|
|
127
|
+
agentEnv: 0,
|
|
128
|
+
agentToken: 0,
|
|
129
|
+
agentCronJob: 0,
|
|
130
|
+
agentCronRun: 0,
|
|
131
|
+
agentTool: 0,
|
|
132
|
+
agentPlugin: 0,
|
|
133
|
+
agentMember: 0,
|
|
134
|
+
agentChatTokenUsageDailyByModel: 0,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const ALL_ONE: FixtureCounts = {
|
|
138
|
+
agent: 1,
|
|
139
|
+
agentEnv: 1,
|
|
140
|
+
agentToken: 1,
|
|
141
|
+
agentCronJob: 1,
|
|
142
|
+
agentCronRun: 1,
|
|
143
|
+
agentTool: 1,
|
|
144
|
+
agentPlugin: 1,
|
|
145
|
+
agentMember: 1,
|
|
146
|
+
agentChatTokenUsageDailyByModel: 1,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** Hand-written no-op stub deps — everything succeeds. Not the thing under test. */
|
|
150
|
+
function makeSucceedingDeps(prisma: PrismaClient): DeleteAgentFullyDeps {
|
|
151
|
+
return {
|
|
152
|
+
prisma,
|
|
153
|
+
provisioner: {
|
|
154
|
+
deprovision: async () => {
|
|
155
|
+
// no-op: real k8s deprovision is out of scope for this DB-cascade test
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
serviceIntegrations: [
|
|
159
|
+
{
|
|
160
|
+
name: "task-store",
|
|
161
|
+
listTokensForAgent: async () => [],
|
|
162
|
+
revokeToken: async () => {
|
|
163
|
+
// no-op
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
chatService: {
|
|
168
|
+
listTokensForAgent: async () => [],
|
|
169
|
+
revokeToken: async () => {
|
|
170
|
+
// no-op
|
|
171
|
+
},
|
|
172
|
+
deleteThreadsForAgent: async () => ({ deleted: 0 }),
|
|
173
|
+
},
|
|
174
|
+
slack: {
|
|
175
|
+
deleteApp: async () => {
|
|
176
|
+
// no-op
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
decrypt: (value) => value,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
describeOrSkip("deleteAgentFully (integration)", () => {
|
|
184
|
+
let prisma: PrismaClient;
|
|
185
|
+
|
|
186
|
+
beforeEach(async () => {
|
|
187
|
+
prisma = makePrisma();
|
|
188
|
+
// FK-safe order: children before parents.
|
|
189
|
+
await prisma.agentCronRun.deleteMany();
|
|
190
|
+
await prisma.agentCronJob.deleteMany();
|
|
191
|
+
await prisma.agentToken.deleteMany();
|
|
192
|
+
await prisma.agentTool.deleteMany();
|
|
193
|
+
await prisma.agentPlugin.deleteMany();
|
|
194
|
+
await prisma.agentMember.deleteMany();
|
|
195
|
+
await prisma.agentChatTokenUsageDailyByModel.deleteMany();
|
|
196
|
+
await prisma.agentEnv.deleteMany();
|
|
197
|
+
await prisma.agent.deleteMany();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
afterEach(async () => {
|
|
201
|
+
await prisma.$disconnect();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("deletes the Agent row and cascades every child row on full success", async () => {
|
|
205
|
+
const agentId = await createAgent(prisma);
|
|
206
|
+
await seedFullFixture(prisma, agentId);
|
|
207
|
+
|
|
208
|
+
// Sanity: the fixture is fully seeded before we call the function under test.
|
|
209
|
+
expect(await countFixtureRows(prisma, agentId)).toEqual(ALL_ONE);
|
|
210
|
+
|
|
211
|
+
const deps = makeSucceedingDeps(prisma);
|
|
212
|
+
const result = await deleteAgentFully(agentId, deps, {
|
|
213
|
+
xoxpToken: "xoxp-user",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.agentDeleted).toBe(true);
|
|
217
|
+
expect(result.failed).toEqual([]);
|
|
218
|
+
|
|
219
|
+
// Every row for this agentId — including the transitively-cascaded
|
|
220
|
+
// AgentCronRun via AgentCronJob — must now be gone.
|
|
221
|
+
expect(await countFixtureRows(prisma, agentId)).toEqual(ALL_ZERO);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("does not delete the Agent row (or cascade anything) if an automatable step fails", async () => {
|
|
225
|
+
const agentId = await createAgent(prisma);
|
|
226
|
+
await seedFullFixture(prisma, agentId);
|
|
227
|
+
|
|
228
|
+
expect(await countFixtureRows(prisma, agentId)).toEqual(ALL_ONE);
|
|
229
|
+
|
|
230
|
+
const deps = makeSucceedingDeps(prisma);
|
|
231
|
+
deps.provisioner = {
|
|
232
|
+
deprovision: async () => {
|
|
233
|
+
throw new Error("k8s deprovision failed");
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = await deleteAgentFully(agentId, deps, {
|
|
238
|
+
xoxpToken: "xoxp-user",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(result.agentDeleted).toBe(false);
|
|
242
|
+
expect(result.failed).toEqual([
|
|
243
|
+
{ step: "k8s", error: "k8s deprovision failed" },
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
// Nothing was cascaded: the Agent row is deliberately left in place, and
|
|
247
|
+
// so is every child row — proving the row-deleted-last rollback-safety
|
|
248
|
+
// guarantee holds against a real DB, not just the in-memory unit double.
|
|
249
|
+
expect(await countFixtureRows(prisma, agentId)).toEqual(ALL_ONE);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("cascades AgentCronRun via its parent AgentCronJob on success, but leaves it intact on failure", async () => {
|
|
253
|
+
// Targeted check on the trickiest FK path — the two-hop
|
|
254
|
+
// Agent -> AgentCronJob -> AgentCronRun cascade — in isolation from the
|
|
255
|
+
// rest of the fixture.
|
|
256
|
+
const successAgentId = await createAgent(prisma, "Cascade Success Agent");
|
|
257
|
+
const successCron = await prisma.agentCronJob.create({
|
|
258
|
+
data: {
|
|
259
|
+
agentId: successAgentId,
|
|
260
|
+
schedule: "0 9 * * *",
|
|
261
|
+
prompt: "Good morning",
|
|
262
|
+
channel: "C1",
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const successRun = await prisma.agentCronRun.create({
|
|
266
|
+
data: {
|
|
267
|
+
cronId: successCron.id,
|
|
268
|
+
agentId: successAgentId,
|
|
269
|
+
startedAt: new Date(),
|
|
270
|
+
completedAt: new Date(),
|
|
271
|
+
skipped: false,
|
|
272
|
+
outcome: "success",
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const successResult = await deleteAgentFully(
|
|
277
|
+
successAgentId,
|
|
278
|
+
makeSucceedingDeps(prisma),
|
|
279
|
+
);
|
|
280
|
+
expect(successResult.agentDeleted).toBe(true);
|
|
281
|
+
expect(
|
|
282
|
+
await prisma.agentCronRun.findUnique({ where: { id: successRun.id } }),
|
|
283
|
+
).toBeNull();
|
|
284
|
+
expect(
|
|
285
|
+
await prisma.agentCronJob.findUnique({ where: { id: successCron.id } }),
|
|
286
|
+
).toBeNull();
|
|
287
|
+
|
|
288
|
+
const failAgentId = await createAgent(prisma, "Cascade Failure Agent");
|
|
289
|
+
const failCron = await prisma.agentCronJob.create({
|
|
290
|
+
data: {
|
|
291
|
+
agentId: failAgentId,
|
|
292
|
+
schedule: "0 9 * * *",
|
|
293
|
+
prompt: "Good morning",
|
|
294
|
+
channel: "C1",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const failRun = await prisma.agentCronRun.create({
|
|
298
|
+
data: {
|
|
299
|
+
cronId: failCron.id,
|
|
300
|
+
agentId: failAgentId,
|
|
301
|
+
startedAt: new Date(),
|
|
302
|
+
completedAt: new Date(),
|
|
303
|
+
skipped: false,
|
|
304
|
+
outcome: "success",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const deps = makeSucceedingDeps(prisma);
|
|
309
|
+
deps.provisioner = {
|
|
310
|
+
deprovision: async () => {
|
|
311
|
+
throw new Error("k8s deprovision failed");
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
const failResult = await deleteAgentFully(failAgentId, deps);
|
|
315
|
+
expect(failResult.agentDeleted).toBe(false);
|
|
316
|
+
expect(
|
|
317
|
+
await prisma.agentCronRun.findUnique({ where: { id: failRun.id } }),
|
|
318
|
+
).not.toBeNull();
|
|
319
|
+
expect(
|
|
320
|
+
await prisma.agentCronJob.findUnique({ where: { id: failCron.id } }),
|
|
321
|
+
).not.toBeNull();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-deletion.ts
|
|
3
|
+
*
|
|
4
|
+
* `deleteAgentFully()` — the single shared orchestration both agent-delete entry
|
|
5
|
+
* points (DELETE /agents/:id and the admin-ui "danger zone") are wired into.
|
|
6
|
+
* Previously each caller independently ran
|
|
7
|
+
* `provisioner.deprovision()` + `prisma.agent.delete()` with no shared cleanup,
|
|
8
|
+
* so any fix to one drifted from the other. This module centralizes the full
|
|
9
|
+
* teardown so there is one place to fix.
|
|
10
|
+
*
|
|
11
|
+
* ── Critical ordering ──────────────────────────────────────────────────────
|
|
12
|
+
* The Agent DB row is deleted LAST, and only once every automatable cleanup
|
|
13
|
+
* step has succeeded. This makes the whole operation IDEMPOTENT and RETRYABLE:
|
|
14
|
+
* a caller can re-invoke `deleteAgentFully` after a partial failure and it only
|
|
15
|
+
* re-attempts what didn't finish, because every underlying step is itself
|
|
16
|
+
* individually idempotent:
|
|
17
|
+
* - K8s deprovision swallows 404 (agent-provisioner.ts deprovision()).
|
|
18
|
+
* - Token revoke on an already-revoked token is a no-op (token-service).
|
|
19
|
+
* - Thread delete tolerates 404 (chat-service deleteThreadsForAgent()).
|
|
20
|
+
*
|
|
21
|
+
* The Agent row is the retry anchor: while it still exists, a retry re-reads it
|
|
22
|
+
* and re-runs the remaining cleanup; once it's deleted, the operation is done.
|
|
23
|
+
*
|
|
24
|
+
* Steps, in order:
|
|
25
|
+
* 1. Read the Agent row + its AgentEnv rows.
|
|
26
|
+
* 2. K8s deprovision (Deployment + Secret + PVC).
|
|
27
|
+
* Then, only for an agent with a per-agent Anthropic identity, remove it:
|
|
28
|
+
* archive the federation rule, then the Anthropic service account, then
|
|
29
|
+
* delete the K8s ServiceAccount (Anthropic refuses to archive a service
|
|
30
|
+
* account while a live rule references it). A failure keeps the row and
|
|
31
|
+
* adds the leftover pieces to the manual-steps checklist.
|
|
32
|
+
* 3. Revoke task-store token(s).
|
|
33
|
+
* 4. Revoke chat-service token(s) + delete chat threads.
|
|
34
|
+
* 5. (Optional) Delete the Slack app, gated on a supplied user token.
|
|
35
|
+
* 6. Build the secret checklist (always included, never blocks).
|
|
36
|
+
* 7. If every automatable step succeeded → delete the Agent row.
|
|
37
|
+
* 8. Otherwise → leave the Agent row in place so the caller can retry.
|
|
38
|
+
*
|
|
39
|
+
* Out of scope (decided during planning): no mutation of task-store Task rows
|
|
40
|
+
* for the deleted agent — tasks keep their historical assignment.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
buildManualStepsChecklist,
|
|
45
|
+
type LeftoverIdentity,
|
|
46
|
+
type ManualStep,
|
|
47
|
+
} from "./agent-deletion-checklist.ts";
|
|
48
|
+
import type { AgentProvisioner } from "./agent-provisioner.ts";
|
|
49
|
+
import type { ChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
50
|
+
import { NotFoundError } from "./errors.ts";
|
|
51
|
+
import type { DeletionServiceIntegration } from "./service-integration.ts";
|
|
52
|
+
import type { SlackProvisioningClient } from "./slack-provisioning-client.ts";
|
|
53
|
+
|
|
54
|
+
// ─── Step identifiers ─────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stable identifiers for each automatable cleanup step. Used both in the
|
|
58
|
+
* `completed` list and to tag entries in the `failed` array so a caller (or a
|
|
59
|
+
* test) can tell exactly which dependency broke.
|
|
60
|
+
*/
|
|
61
|
+
type DeleteAgentStep =
|
|
62
|
+
| "k8s"
|
|
63
|
+
| "anthropic-identity"
|
|
64
|
+
| `${string}-tokens`
|
|
65
|
+
| "chat-service-tokens-and-threads"
|
|
66
|
+
| "slack-app";
|
|
67
|
+
|
|
68
|
+
/** A step that threw, with its error message for the caller to log/surface. */
|
|
69
|
+
interface FailedStep {
|
|
70
|
+
step: DeleteAgentStep;
|
|
71
|
+
error: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Result ────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
interface DeleteAgentResult {
|
|
77
|
+
/**
|
|
78
|
+
* True only when every automatable step succeeded AND the Agent row was
|
|
79
|
+
* deleted. False when any step failed (the row is left in place for retry).
|
|
80
|
+
*/
|
|
81
|
+
agentDeleted: boolean;
|
|
82
|
+
/** Steps that completed successfully, in execution order. */
|
|
83
|
+
completed: DeleteAgentStep[];
|
|
84
|
+
/**
|
|
85
|
+
* Steps that failed. Empty when `agentDeleted` is true. A failed step does
|
|
86
|
+
* NOT abort the remaining steps — they are still attempted so a single retry
|
|
87
|
+
* makes maximum forward progress.
|
|
88
|
+
*/
|
|
89
|
+
failed: FailedStep[];
|
|
90
|
+
/**
|
|
91
|
+
* Operator-facing manual reminders (hand-pasted secrets with no automated
|
|
92
|
+
* revocation, plus an optional Slack-app entry when a token wasn't supplied).
|
|
93
|
+
* Always populated; never blocks the delete.
|
|
94
|
+
*/
|
|
95
|
+
manualStepsRequired: ManualStep[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Dependencies (DI boundary) ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Minimal Prisma surface this orchestration needs: read the Agent row, read its
|
|
102
|
+
* AgentEnv rows, and delete the Agent row (cascades child rows). Typed as a
|
|
103
|
+
* structural shape rather than `Pick<PrismaClient, ...>` so tests can inject a
|
|
104
|
+
* tiny in-memory double without pulling in the generated client.
|
|
105
|
+
*/
|
|
106
|
+
interface DeleteAgentPrisma {
|
|
107
|
+
agent: {
|
|
108
|
+
findUnique(args: {
|
|
109
|
+
where: { id: string };
|
|
110
|
+
select: {
|
|
111
|
+
id: true;
|
|
112
|
+
name: true;
|
|
113
|
+
anthropicFederationRuleId: true;
|
|
114
|
+
anthropicServiceAccountId: true;
|
|
115
|
+
k8sServiceAccountName: true;
|
|
116
|
+
};
|
|
117
|
+
}): Promise<({ id: string; name: string } & LeftoverIdentity) | null>;
|
|
118
|
+
delete(args: { where: { id: string } }): Promise<unknown>;
|
|
119
|
+
};
|
|
120
|
+
agentEnv: {
|
|
121
|
+
findMany(args: {
|
|
122
|
+
where: { agentId: string };
|
|
123
|
+
select: { key: true; value: true; secret: true };
|
|
124
|
+
}): Promise<{ key: string; value: string; secret: boolean }[]>;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DeleteAgentFullyDeps {
|
|
129
|
+
prisma: DeleteAgentPrisma;
|
|
130
|
+
provisioner: Pick<
|
|
131
|
+
AgentProvisioner,
|
|
132
|
+
"deprovision" | "removeAnthropicIdentity"
|
|
133
|
+
>;
|
|
134
|
+
/**
|
|
135
|
+
* Optional service integrations (e.g. task-store, registered by the coding
|
|
136
|
+
* extension), each cleaned up as its own `${name}-tokens` step, in list
|
|
137
|
+
* order, before the built-in chat-service step.
|
|
138
|
+
*/
|
|
139
|
+
serviceIntegrations: DeletionServiceIntegration[];
|
|
140
|
+
chatService: Pick<
|
|
141
|
+
ChatServiceProvisioningClient,
|
|
142
|
+
"listTokensForAgent" | "revokeToken" | "deleteThreadsForAgent"
|
|
143
|
+
>;
|
|
144
|
+
slack: Pick<SlackProvisioningClient, "deleteApp">;
|
|
145
|
+
/**
|
|
146
|
+
* Decrypt a stored AgentEnv value. AgentEnv values are AES-256-GCM encrypted
|
|
147
|
+
* at rest; only the SLACK_APP_ID value is decrypted here (to pass to the
|
|
148
|
+
* Slack delete call). Injected so the orchestration stays free of concrete
|
|
149
|
+
* crypto and is trivially unit-testable.
|
|
150
|
+
*/
|
|
151
|
+
decrypt(value: string): string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface DeleteAgentFullyOpts {
|
|
155
|
+
/**
|
|
156
|
+
* Slack user token (xoxp-) authorizing the Slack app deletion. Supplied
|
|
157
|
+
* per-request (same UX as the sync-manifest flow) — when omitted, a Slack app
|
|
158
|
+
* (if present) is NOT auto-deleted and instead becomes a manual checklist
|
|
159
|
+
* entry. Its absence is never a failure.
|
|
160
|
+
*/
|
|
161
|
+
xoxpToken?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Orchestration ──────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
const errMessage = (err: unknown): string =>
|
|
167
|
+
err instanceof Error ? err.message : String(err);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Fully delete an agent: tear down its workload and external state, then delete
|
|
171
|
+
* the Agent DB row LAST (only if every automatable step succeeded).
|
|
172
|
+
*
|
|
173
|
+
* Idempotent and retryable — see the module header. Throws `NotFoundError` only
|
|
174
|
+
* when the Agent row is genuinely absent at the start of the call: that means
|
|
175
|
+
* either the id was never valid, or a prior call already completed the delete.
|
|
176
|
+
* Treating a missing row as an error (rather than a silent success) keeps retry
|
|
177
|
+
* semantics honest — the caller asked to delete something that isn't there. A
|
|
178
|
+
* caller that races two deletes can catch it; the common retry-after-partial
|
|
179
|
+
* path always still sees the row (it's deleted last) and proceeds normally.
|
|
180
|
+
*/
|
|
181
|
+
export async function deleteAgentFully(
|
|
182
|
+
agentId: string,
|
|
183
|
+
deps: DeleteAgentFullyDeps,
|
|
184
|
+
opts: DeleteAgentFullyOpts = {},
|
|
185
|
+
): Promise<DeleteAgentResult> {
|
|
186
|
+
// ── Step 1: read the Agent row + its AgentEnv rows ──────────────────────────
|
|
187
|
+
const agentSelect = {
|
|
188
|
+
id: true,
|
|
189
|
+
name: true,
|
|
190
|
+
anthropicFederationRuleId: true,
|
|
191
|
+
anthropicServiceAccountId: true,
|
|
192
|
+
k8sServiceAccountName: true,
|
|
193
|
+
} as const;
|
|
194
|
+
const agent = await deps.prisma.agent.findUnique({
|
|
195
|
+
where: { id: agentId },
|
|
196
|
+
select: agentSelect,
|
|
197
|
+
});
|
|
198
|
+
if (!agent) {
|
|
199
|
+
throw new NotFoundError(`agent ${agentId} not found`);
|
|
200
|
+
}
|
|
201
|
+
const envRows = await deps.prisma.agentEnv.findMany({
|
|
202
|
+
where: { agentId },
|
|
203
|
+
select: { key: true, value: true, secret: true },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const completed: DeleteAgentStep[] = [];
|
|
207
|
+
const failed: FailedStep[] = [];
|
|
208
|
+
|
|
209
|
+
const runStep = async (
|
|
210
|
+
step: DeleteAgentStep,
|
|
211
|
+
fn: () => Promise<void>,
|
|
212
|
+
): Promise<void> => {
|
|
213
|
+
try {
|
|
214
|
+
await fn();
|
|
215
|
+
completed.push(step);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
failed.push({ step, error: errMessage(err) });
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// ── Step 2: K8s deprovision (Deployment + Secret + PVC) ─────────────────────
|
|
222
|
+
await runStep("k8s", async () => {
|
|
223
|
+
await deps.provisioner.deprovision(agentId, { slug: agent.name });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── Step 2b: remove the per-agent Anthropic identity ────────────────────────
|
|
227
|
+
// Runs after k8s so the pods are gone before their identity is pulled. Only
|
|
228
|
+
// present when the agent has a saved identity piece: agents without one (and
|
|
229
|
+
// shared/no-federation mode) get no step and behave exactly as before. The
|
|
230
|
+
// provisioner removes rule → Anthropic service account → K8s ServiceAccount
|
|
231
|
+
// and throws at the first failure.
|
|
232
|
+
const hasIdentity = Boolean(
|
|
233
|
+
agent.anthropicFederationRuleId ||
|
|
234
|
+
agent.anthropicServiceAccountId ||
|
|
235
|
+
agent.k8sServiceAccountName,
|
|
236
|
+
);
|
|
237
|
+
if (hasIdentity) {
|
|
238
|
+
await runStep("anthropic-identity", async () => {
|
|
239
|
+
if (!deps.provisioner.removeAnthropicIdentity) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"the provisioner cannot remove a per-agent Anthropic identity",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
await deps.provisioner.removeAnthropicIdentity(agentId);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Step 3: revoke each service integration's token(s) (e.g. task-store) ───
|
|
249
|
+
for (const integration of deps.serviceIntegrations) {
|
|
250
|
+
await runStep(`${integration.name}-tokens`, async () => {
|
|
251
|
+
await revokeAllTokens(integration, agentId);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Step 4: revoke chat-service token(s) + delete chat threads ──────────────
|
|
256
|
+
await runStep("chat-service-tokens-and-threads", async () => {
|
|
257
|
+
await revokeAllTokens(deps.chatService, agentId);
|
|
258
|
+
await deps.chatService.deleteThreadsForAgent(agentId);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── Step 5: (optional) delete the Slack app ─────────────────────────────────
|
|
262
|
+
// Only present when the agent stores a SLACK_APP_ID. Gated on a supplied user
|
|
263
|
+
// token: with one we auto-delete (a real cleanup step that can fail); without
|
|
264
|
+
// one it's an expected manual gap that adds a checklist entry, not a failure.
|
|
265
|
+
const slackAppIdRow = envRows.find((r) => r.key === "SLACK_APP_ID");
|
|
266
|
+
const manualSlackSteps: ManualStep[] = [];
|
|
267
|
+
if (slackAppIdRow) {
|
|
268
|
+
const slackAppId = deps.decrypt(slackAppIdRow.value);
|
|
269
|
+
if (opts.xoxpToken) {
|
|
270
|
+
await runStep("slack-app", async () => {
|
|
271
|
+
await deps.slack.deleteApp(opts.xoxpToken as string, slackAppId);
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
manualSlackSteps.push({
|
|
275
|
+
key: "SLACK_APP_ID",
|
|
276
|
+
message: `Slack app ${slackAppId} was not deleted automatically — no Slack user token was supplied. Delete it manually at api.slack.com/apps or retry with a token.`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Step 6: build the secret checklist (always included) ────────────────────
|
|
282
|
+
// When the identity step failed, re-read the IDs so the checklist lists only
|
|
283
|
+
// what is actually still live (the provisioner clears each ID as it goes).
|
|
284
|
+
let leftoverIdentity: LeftoverIdentity | undefined;
|
|
285
|
+
if (failed.some((f) => f.step === "anthropic-identity")) {
|
|
286
|
+
leftoverIdentity =
|
|
287
|
+
(await deps.prisma.agent.findUnique({
|
|
288
|
+
where: { id: agentId },
|
|
289
|
+
select: agentSelect,
|
|
290
|
+
})) ?? agent;
|
|
291
|
+
}
|
|
292
|
+
const manualStepsRequired = [
|
|
293
|
+
...buildManualStepsChecklist(envRows, leftoverIdentity),
|
|
294
|
+
...manualSlackSteps,
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
// ── Steps 7/8: delete the Agent row LAST, only if nothing failed ────────────
|
|
298
|
+
if (failed.length === 0) {
|
|
299
|
+
await deps.prisma.agent.delete({ where: { id: agentId } });
|
|
300
|
+
return { agentDeleted: true, completed, failed, manualStepsRequired };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { agentDeleted: false, completed, failed, manualStepsRequired };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* List every token scoped to the agent and revoke each. Revocation is
|
|
308
|
+
* individually idempotent (already-revoked / 404 is a no-op), so one failure
|
|
309
|
+
* mid-loop doesn't abort the rest — we attempt every token, then re-throw the
|
|
310
|
+
* first error so the enclosing step is recorded as failed for retry.
|
|
311
|
+
*/
|
|
312
|
+
async function revokeAllTokens(
|
|
313
|
+
client: Pick<
|
|
314
|
+
DeletionServiceIntegration,
|
|
315
|
+
"listTokensForAgent" | "revokeToken"
|
|
316
|
+
>,
|
|
317
|
+
agentId: string,
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
const tokens = await client.listTokensForAgent(agentId);
|
|
320
|
+
let firstError: unknown;
|
|
321
|
+
for (const token of tokens) {
|
|
322
|
+
try {
|
|
323
|
+
await client.revokeToken(token.id);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (firstError === undefined) firstError = err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (firstError !== undefined) throw firstError;
|
|
329
|
+
}
|