@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/agent-tokens.integration.test.ts
|
|
3
|
+
* Integration tests for AgentTokenService against a real PostgreSQL DB.
|
|
4
|
+
*
|
|
5
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
10
|
+
import { AgentTokenService } from "./agent-tokens.ts";
|
|
11
|
+
import { UnprocessableEntityError } from "./errors.ts";
|
|
12
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
13
|
+
|
|
14
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
15
|
+
|
|
16
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
17
|
+
|
|
18
|
+
function makePrisma(): PrismaClient {
|
|
19
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
20
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function createAgent(
|
|
24
|
+
prisma: PrismaClient,
|
|
25
|
+
name = "Test Agent",
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const agent = await prisma.agent.create({ data: { name } });
|
|
28
|
+
return agent.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describeOrSkip("AgentTokenService (integration)", () => {
|
|
32
|
+
let prisma: PrismaClient;
|
|
33
|
+
let service: AgentTokenService;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
prisma = makePrisma();
|
|
37
|
+
await prisma.agentToken.deleteMany();
|
|
38
|
+
await prisma.agentCronJob.deleteMany();
|
|
39
|
+
await prisma.agentTool.deleteMany();
|
|
40
|
+
await prisma.agentEnv.deleteMany();
|
|
41
|
+
await prisma.agent.deleteMany();
|
|
42
|
+
service = new AgentTokenService(prisma);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await prisma.$disconnect();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ─── create ─────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
it("create() returns a token record and a raw token", async () => {
|
|
52
|
+
const agentId = await createAgent(prisma);
|
|
53
|
+
const { token, rawToken } = await service.create(agentId);
|
|
54
|
+
expect(token.agentId).toBe(agentId);
|
|
55
|
+
expect(typeof rawToken).toBe("string");
|
|
56
|
+
expect(rawToken).toHaveLength(64); // 32 bytes = 64 hex chars
|
|
57
|
+
expect(token.revokedAt).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("create() stores only the SHA-256 hash (not raw token)", async () => {
|
|
61
|
+
const agentId = await createAgent(prisma);
|
|
62
|
+
const { token, rawToken } = await service.create(agentId);
|
|
63
|
+
// The stored token should NOT be the raw token
|
|
64
|
+
expect(token.token).not.toBe(rawToken);
|
|
65
|
+
// But should be a valid 64-char hex SHA-256 hash
|
|
66
|
+
expect(token.token).toHaveLength(64);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("create() accepts an optional label", async () => {
|
|
70
|
+
const agentId = await createAgent(prisma);
|
|
71
|
+
const { token } = await service.create(agentId, "My CI Token");
|
|
72
|
+
expect(token.label).toBe("My CI Token");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("create() throws UnprocessableEntityError for unknown agentId", async () => {
|
|
76
|
+
await expect(service.create("nonexistent-id")).rejects.toBeInstanceOf(
|
|
77
|
+
UnprocessableEntityError,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ─── validate ───────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
it("validate() returns agentId for a valid token", async () => {
|
|
84
|
+
const agentId = await createAgent(prisma);
|
|
85
|
+
const { rawToken } = await service.create(agentId);
|
|
86
|
+
const result = await service.validate(rawToken);
|
|
87
|
+
expect(result).not.toBeNull();
|
|
88
|
+
expect(result?.agentId).toBe(agentId);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("validate() returns null for unknown token", async () => {
|
|
92
|
+
const result = await service.validate("0".repeat(64));
|
|
93
|
+
expect(result).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("validate() returns null for a revoked token", async () => {
|
|
97
|
+
const agentId = await createAgent(prisma);
|
|
98
|
+
const { token, rawToken } = await service.create(agentId);
|
|
99
|
+
await service.revoke(token.id);
|
|
100
|
+
const result = await service.validate(rawToken);
|
|
101
|
+
expect(result).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─── revoke ─────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
it("revoke() sets revokedAt on the token", async () => {
|
|
107
|
+
const agentId = await createAgent(prisma);
|
|
108
|
+
const { token } = await service.create(agentId);
|
|
109
|
+
const revoked = await service.revoke(token.id);
|
|
110
|
+
expect(revoked).not.toBeNull();
|
|
111
|
+
expect(revoked?.revokedAt).not.toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("revoke() returns null for unknown tokenId (P2025)", async () => {
|
|
115
|
+
const result = await service.revoke("nonexistent-id");
|
|
116
|
+
expect(result).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── listForAgent ────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
it("listForAgent() returns all tokens for an agent", async () => {
|
|
122
|
+
const agentId = await createAgent(prisma);
|
|
123
|
+
await service.create(agentId, "Token 1");
|
|
124
|
+
await service.create(agentId, "Token 2");
|
|
125
|
+
const tokens = await service.listForAgent(agentId);
|
|
126
|
+
expect(tokens).toHaveLength(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("listForAgent() does not return tokens from other agents", async () => {
|
|
130
|
+
const agentId1 = await createAgent(prisma, "Agent 1");
|
|
131
|
+
const agentId2 = await createAgent(prisma, "Agent 2");
|
|
132
|
+
await service.create(agentId1);
|
|
133
|
+
const tokens = await service.listForAgent(agentId2);
|
|
134
|
+
expect(tokens).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ─── getById ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
it("getById() returns a token by ID", async () => {
|
|
140
|
+
const agentId = await createAgent(prisma);
|
|
141
|
+
const { token } = await service.create(agentId);
|
|
142
|
+
const fetched = await service.getById(token.id);
|
|
143
|
+
expect(fetched?.id).toBe(token.id);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("getById() returns null for unknown tokenId", async () => {
|
|
147
|
+
const result = await service.getById("nonexistent");
|
|
148
|
+
expect(result).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/agent-tokens.ts
|
|
3
|
+
* AgentTokenService — scoped API tokens for agent authentication.
|
|
4
|
+
*
|
|
5
|
+
* The raw token (64-char hex) is returned once at creation; only the SHA-256
|
|
6
|
+
* hash is stored. Token validation is O(1) via the unique index on `token`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
10
|
+
import type { AgentToken, PrismaClient } from "../prisma/client/client.ts";
|
|
11
|
+
import { type Clock, SystemClock } from "./clock.ts";
|
|
12
|
+
import { UnprocessableEntityError } from "./errors.ts";
|
|
13
|
+
|
|
14
|
+
export type { AgentToken };
|
|
15
|
+
|
|
16
|
+
export interface AgentTokenValidated {
|
|
17
|
+
agentId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hashToken(raw: string): string {
|
|
21
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateRawToken(): string {
|
|
25
|
+
return randomBytes(32).toString("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class AgentTokenService {
|
|
29
|
+
constructor(
|
|
30
|
+
private prisma: PrismaClient,
|
|
31
|
+
private clock: Clock = SystemClock(),
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new token for an agent.
|
|
36
|
+
* Returns the token record plus the raw token — only opportunity to read it.
|
|
37
|
+
* Throws UnprocessableEntityError if agentId doesn't reference an existing Agent.
|
|
38
|
+
*/
|
|
39
|
+
async create(
|
|
40
|
+
agentId: string,
|
|
41
|
+
label?: string,
|
|
42
|
+
): Promise<{ token: AgentToken; rawToken: string }> {
|
|
43
|
+
const agent = await this.prisma.agent.findUnique({
|
|
44
|
+
where: { id: agentId },
|
|
45
|
+
});
|
|
46
|
+
if (!agent) {
|
|
47
|
+
throw new UnprocessableEntityError("agentId not found");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rawToken = generateRawToken();
|
|
51
|
+
const hashed = hashToken(rawToken);
|
|
52
|
+
|
|
53
|
+
const token = await this.prisma.agentToken.create({
|
|
54
|
+
data: { agentId, token: hashed, label },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { token, rawToken };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a raw token. Returns { agentId } if valid and not revoked,
|
|
62
|
+
* or null if the token is unknown or revoked.
|
|
63
|
+
*/
|
|
64
|
+
async validate(raw: string): Promise<AgentTokenValidated | null> {
|
|
65
|
+
const hashed = hashToken(raw);
|
|
66
|
+
const record = await this.prisma.agentToken.findUnique({
|
|
67
|
+
where: { token: hashed },
|
|
68
|
+
});
|
|
69
|
+
if (!record || record.revokedAt !== null) return null;
|
|
70
|
+
return { agentId: record.agentId };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Revoke a token by ID. Returns the updated record, or null if not found (P2025).
|
|
75
|
+
* Re-throws all other errors so real failures aren't silently swallowed.
|
|
76
|
+
*/
|
|
77
|
+
async revoke(tokenId: string): Promise<AgentToken | null> {
|
|
78
|
+
try {
|
|
79
|
+
return await this.prisma.agentToken.update({
|
|
80
|
+
where: { id: tokenId },
|
|
81
|
+
data: { revokedAt: this.clock.now() },
|
|
82
|
+
});
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
if (
|
|
85
|
+
typeof err === "object" &&
|
|
86
|
+
err !== null &&
|
|
87
|
+
"code" in err &&
|
|
88
|
+
(err as { code: string }).code === "P2025"
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List tokens for a given agent (without the hashed token value).
|
|
98
|
+
*/
|
|
99
|
+
async listForAgent(agentId: string): Promise<AgentToken[]> {
|
|
100
|
+
return this.prisma.agentToken.findMany({
|
|
101
|
+
where: { agentId },
|
|
102
|
+
orderBy: { createdAt: "asc" },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get a single token by ID. Used for ownership checks before mutating.
|
|
108
|
+
*/
|
|
109
|
+
async getById(tokenId: string): Promise<AgentToken | null> {
|
|
110
|
+
return this.prisma.agentToken.findUnique({ where: { id: tokenId } });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/agent-tools.integration.test.ts
|
|
3
|
+
* Integration tests for AgentToolService against a real PostgreSQL DB.
|
|
4
|
+
*
|
|
5
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
9
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
10
|
+
import { AgentToolService } from "./agent-tools.ts";
|
|
11
|
+
import { NotFoundError } from "./errors.ts";
|
|
12
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
13
|
+
|
|
14
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
15
|
+
|
|
16
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
17
|
+
|
|
18
|
+
function makePrisma(): PrismaClient {
|
|
19
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
20
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function createAgent(
|
|
24
|
+
prisma: PrismaClient,
|
|
25
|
+
name = "Test Agent",
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const agent = await prisma.agent.create({ data: { name } });
|
|
28
|
+
return agent.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describeOrSkip("AgentToolService (integration)", () => {
|
|
32
|
+
let prisma: PrismaClient;
|
|
33
|
+
let service: AgentToolService;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
prisma = makePrisma();
|
|
37
|
+
await prisma.agentToken.deleteMany();
|
|
38
|
+
await prisma.agentCronJob.deleteMany();
|
|
39
|
+
await prisma.agentTool.deleteMany();
|
|
40
|
+
await prisma.agentEnv.deleteMany();
|
|
41
|
+
await prisma.agent.deleteMany();
|
|
42
|
+
service = new AgentToolService(prisma);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await prisma.$disconnect();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("add() creates a new tool pattern", async () => {
|
|
50
|
+
const agentId = await createAgent(prisma);
|
|
51
|
+
const tool = await service.add(agentId, "Read");
|
|
52
|
+
expect(tool.agentId).toBe(agentId);
|
|
53
|
+
expect(tool.pattern).toBe("Read");
|
|
54
|
+
expect(tool.enabled).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("add() re-enables a disabled pattern (upsert behavior)", async () => {
|
|
58
|
+
const agentId = await createAgent(prisma);
|
|
59
|
+
const tool = await service.add(agentId, "Write");
|
|
60
|
+
await service.toggle(agentId, tool.id, false);
|
|
61
|
+
// Re-add should re-enable it
|
|
62
|
+
const reenabled = await service.add(agentId, "Write");
|
|
63
|
+
expect(reenabled.enabled).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("list() returns all tools for an agent ordered by createdAt", async () => {
|
|
67
|
+
const agentId = await createAgent(prisma);
|
|
68
|
+
await service.add(agentId, "Read");
|
|
69
|
+
await service.add(agentId, "Write");
|
|
70
|
+
await service.add(agentId, "Bash");
|
|
71
|
+
const tools = await service.list(agentId);
|
|
72
|
+
expect(tools).toHaveLength(3);
|
|
73
|
+
expect(tools.map((t) => t.pattern)).toEqual(["Read", "Write", "Bash"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("remove() deletes a tool pattern", async () => {
|
|
77
|
+
const agentId = await createAgent(prisma);
|
|
78
|
+
const tool = await service.add(agentId, "Bash");
|
|
79
|
+
await service.remove(agentId, tool.id);
|
|
80
|
+
const tools = await service.list(agentId);
|
|
81
|
+
expect(tools).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("remove() throws NotFoundError for unknown toolId", async () => {
|
|
85
|
+
const agentId = await createAgent(prisma);
|
|
86
|
+
await expect(service.remove(agentId, "nonexistent")).rejects.toBeInstanceOf(
|
|
87
|
+
NotFoundError,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("remove() throws NotFoundError when toolId belongs to a different agent", async () => {
|
|
92
|
+
const agentId1 = await createAgent(prisma, "Agent 1");
|
|
93
|
+
const agentId2 = await createAgent(prisma, "Agent 2");
|
|
94
|
+
const tool = await service.add(agentId1, "Read");
|
|
95
|
+
await expect(service.remove(agentId2, tool.id)).rejects.toBeInstanceOf(
|
|
96
|
+
NotFoundError,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("toggle() enables and disables a tool", async () => {
|
|
101
|
+
const agentId = await createAgent(prisma);
|
|
102
|
+
const tool = await service.add(agentId, "Write");
|
|
103
|
+
const disabled = await service.toggle(agentId, tool.id, false);
|
|
104
|
+
expect(disabled.enabled).toBe(false);
|
|
105
|
+
const enabled = await service.toggle(agentId, tool.id, true);
|
|
106
|
+
expect(enabled.enabled).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("toggle() throws NotFoundError for unknown toolId", async () => {
|
|
110
|
+
const agentId = await createAgent(prisma);
|
|
111
|
+
await expect(
|
|
112
|
+
service.toggle(agentId, "nonexistent", true),
|
|
113
|
+
).rejects.toBeInstanceOf(NotFoundError);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("updatePattern() changes the pattern string", async () => {
|
|
117
|
+
const agentId = await createAgent(prisma);
|
|
118
|
+
const tool = await service.add(agentId, "OldPattern");
|
|
119
|
+
const updated = await service.updatePattern(agentId, tool.id, "NewPattern");
|
|
120
|
+
expect(updated.pattern).toBe("NewPattern");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("updatePattern() throws NotFoundError for unknown toolId", async () => {
|
|
124
|
+
const agentId = await createAgent(prisma);
|
|
125
|
+
await expect(
|
|
126
|
+
service.updatePattern(agentId, "nonexistent", "Read"),
|
|
127
|
+
).rejects.toBeInstanceOf(NotFoundError);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/agent-tools.ts
|
|
3
|
+
* AgentToolService — CRUD for allowed tool patterns per agent.
|
|
4
|
+
*
|
|
5
|
+
* Each pattern grants an agent permission to use a specific tool (e.g. "Read",
|
|
6
|
+
* "Bash", "Write"). The unique constraint on [agentId, pattern] prevents
|
|
7
|
+
* duplicates; add() uses upsert so it re-enables a previously disabled pattern.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AgentTool, PrismaClient } from "../prisma/client/client.ts";
|
|
11
|
+
import { NotFoundError } from "./errors.ts";
|
|
12
|
+
import type { PrismaTransactionClient } from "./prisma-tx.ts";
|
|
13
|
+
|
|
14
|
+
export type { AgentTool };
|
|
15
|
+
|
|
16
|
+
export class AgentToolService {
|
|
17
|
+
constructor(private prisma: PrismaClient) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* List all tool patterns for a given agent, ordered by createdAt.
|
|
21
|
+
*/
|
|
22
|
+
async list(agentId: string): Promise<AgentTool[]> {
|
|
23
|
+
return this.prisma.agentTool.findMany({
|
|
24
|
+
where: { agentId },
|
|
25
|
+
orderBy: { createdAt: "asc" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a tool pattern for the given agent.
|
|
31
|
+
* Uses upsert so re-adding a disabled pattern re-enables it.
|
|
32
|
+
*
|
|
33
|
+
* @param client - defaults to `this.prisma`; pass the `tx` argument from a
|
|
34
|
+
* caller's `prisma.$transaction(async (tx) => ...)` to make this write
|
|
35
|
+
* participate in that transaction (see createAgent() in agents.ts).
|
|
36
|
+
*/
|
|
37
|
+
async add(
|
|
38
|
+
agentId: string,
|
|
39
|
+
pattern: string,
|
|
40
|
+
client: PrismaTransactionClient = this.prisma,
|
|
41
|
+
): Promise<AgentTool> {
|
|
42
|
+
return client.agentTool.upsert({
|
|
43
|
+
where: { agentId_pattern: { agentId, pattern } },
|
|
44
|
+
create: { agentId, pattern, enabled: true },
|
|
45
|
+
update: { enabled: true },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Delete a tool pattern. Verifies agentId ownership before deleting.
|
|
51
|
+
* Throws NotFoundError if the toolId doesn't exist or belongs to a different agent.
|
|
52
|
+
*/
|
|
53
|
+
async remove(agentId: string, toolId: string): Promise<void> {
|
|
54
|
+
const existing = await this.prisma.agentTool.findUnique({
|
|
55
|
+
where: { id: toolId },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!existing || existing.agentId !== agentId) {
|
|
59
|
+
throw new NotFoundError(`tool ${toolId} not found`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await this.prisma.agentTool.delete({ where: { id: toolId } });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Enable or disable a tool pattern.
|
|
67
|
+
* Throws NotFoundError if the toolId doesn't exist or belongs to a different agent.
|
|
68
|
+
*/
|
|
69
|
+
async toggle(
|
|
70
|
+
agentId: string,
|
|
71
|
+
toolId: string,
|
|
72
|
+
enabled: boolean,
|
|
73
|
+
): Promise<AgentTool> {
|
|
74
|
+
const existing = await this.prisma.agentTool.findUnique({
|
|
75
|
+
where: { id: toolId },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!existing || existing.agentId !== agentId) {
|
|
79
|
+
throw new NotFoundError(`tool ${toolId} not found`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.prisma.agentTool.update({
|
|
83
|
+
where: { id: toolId },
|
|
84
|
+
data: { enabled },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update the pattern string for a tool.
|
|
90
|
+
* Verifies agentId ownership before updating.
|
|
91
|
+
* Throws NotFoundError if the toolId doesn't exist or belongs to a different agent.
|
|
92
|
+
*/
|
|
93
|
+
async updatePattern(
|
|
94
|
+
agentId: string,
|
|
95
|
+
toolId: string,
|
|
96
|
+
pattern: string,
|
|
97
|
+
): Promise<AgentTool> {
|
|
98
|
+
const existing = await this.prisma.agentTool.findUnique({
|
|
99
|
+
where: { id: toolId },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!existing || existing.agentId !== agentId) {
|
|
103
|
+
throw new NotFoundError(`tool ${toolId} not found`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return this.prisma.agentTool.update({
|
|
107
|
+
where: { id: toolId },
|
|
108
|
+
data: { pattern },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|