@app-vitals/agent-admin 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +62 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-account-assignment.ts
|
|
3
|
+
* Post-assignment side effect for SSP-5.3 (admin assign/reassign an agent to
|
|
4
|
+
* an account). The task-store scope resolver reads Agent.accountId live, so no
|
|
5
|
+
* token re-mint is needed — only the tenant pod label / CPU limit (SSP-7.1)
|
|
6
|
+
* has to be re-applied, by reconciling the single agent's workload.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentAnthropicIdentity } from "./agent-anthropic-identity.ts";
|
|
10
|
+
import type { AgentProvisioner } from "./agent-provisioner.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reconciles the agent's K8s workload after its accountId changed. Self-hosted
|
|
14
|
+
* agents have no workload, so they are skipped. Never throws: the DB change is
|
|
15
|
+
* already committed, so a reconcile failure is returned as a warning string
|
|
16
|
+
* (the next full reconcile pass will converge it) instead of failing the call.
|
|
17
|
+
*/
|
|
18
|
+
export async function reconcileAgentAfterAccountChange(
|
|
19
|
+
provisioner: Pick<AgentProvisioner, "reconcile">,
|
|
20
|
+
agent: Partial<AgentAnthropicIdentity> & {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
selfHosted: boolean;
|
|
24
|
+
accountId?: string | null;
|
|
25
|
+
},
|
|
26
|
+
): Promise<string | undefined> {
|
|
27
|
+
if (agent.selfHosted) return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const result = await provisioner.reconcile([
|
|
30
|
+
{
|
|
31
|
+
id: agent.id,
|
|
32
|
+
slug: agent.name,
|
|
33
|
+
accountId: agent.accountId ?? null,
|
|
34
|
+
ownerEmail: agent.ownerEmail,
|
|
35
|
+
anthropicIdentityName: agent.anthropicIdentityName,
|
|
36
|
+
anthropicServiceAccountId: agent.anthropicServiceAccountId,
|
|
37
|
+
anthropicFederationRuleId: agent.anthropicFederationRuleId,
|
|
38
|
+
k8sServiceAccountName: agent.k8sServiceAccountName,
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
const failure = result.failed.find((f) => f.agentId === agent.id);
|
|
42
|
+
return failure
|
|
43
|
+
? `account updated, but reconciling the agent workload failed: ${failure.error}`
|
|
44
|
+
: undefined;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
return `account updated, but reconciling the agent workload failed: ${message}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { reconcileAgentAfterAccountChange } from "./agent-account-assignment.ts";
|
|
3
|
+
|
|
4
|
+
interface Result {
|
|
5
|
+
recreated: string[];
|
|
6
|
+
orphans: string[];
|
|
7
|
+
failed: Array<{ agentId: string; error: string }>;
|
|
8
|
+
updated: string[];
|
|
9
|
+
migrated: string[];
|
|
10
|
+
noOwner: string[];
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
}
|
|
13
|
+
const EMPTY: Result = {
|
|
14
|
+
recreated: [],
|
|
15
|
+
migrated: [],
|
|
16
|
+
noOwner: [],
|
|
17
|
+
dryRun: false,
|
|
18
|
+
orphans: [],
|
|
19
|
+
failed: [],
|
|
20
|
+
updated: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function makeProvisioner(
|
|
24
|
+
impl: (agents: unknown[]) => Promise<Result> = async () => EMPTY,
|
|
25
|
+
) {
|
|
26
|
+
const calls: unknown[][] = [];
|
|
27
|
+
return {
|
|
28
|
+
calls,
|
|
29
|
+
reconcile: async (agents: unknown[]) => {
|
|
30
|
+
calls.push(agents);
|
|
31
|
+
return impl(agents);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const agent = { id: "a1", name: "Bot", selfHosted: false, accountId: "acct" };
|
|
37
|
+
|
|
38
|
+
describe("reconcileAgentAfterAccountChange", () => {
|
|
39
|
+
it("reconciles exactly the one in-cluster agent with its new accountId", async () => {
|
|
40
|
+
const p = makeProvisioner();
|
|
41
|
+
expect(await reconcileAgentAfterAccountChange(p, agent)).toBeUndefined();
|
|
42
|
+
expect(p.calls).toEqual([[{ id: "a1", slug: "Bot", accountId: "acct" }]]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("carries the agent's owner and identity fields into the reconcile target", async () => {
|
|
46
|
+
const p = makeProvisioner();
|
|
47
|
+
await reconcileAgentAfterAccountChange(p, {
|
|
48
|
+
...agent,
|
|
49
|
+
ownerEmail: "jane.doe@example.com",
|
|
50
|
+
anthropicIdentityName: "jane-doe-a1",
|
|
51
|
+
anthropicServiceAccountId: "svac_1",
|
|
52
|
+
anthropicFederationRuleId: "fdrl_1",
|
|
53
|
+
k8sServiceAccountName: "a1",
|
|
54
|
+
});
|
|
55
|
+
expect(p.calls).toEqual([
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
id: "a1",
|
|
59
|
+
slug: "Bot",
|
|
60
|
+
accountId: "acct",
|
|
61
|
+
ownerEmail: "jane.doe@example.com",
|
|
62
|
+
anthropicIdentityName: "jane-doe-a1",
|
|
63
|
+
anthropicServiceAccountId: "svac_1",
|
|
64
|
+
anthropicFederationRuleId: "fdrl_1",
|
|
65
|
+
k8sServiceAccountName: "a1",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("passes null for a cleared account", async () => {
|
|
72
|
+
const p = makeProvisioner();
|
|
73
|
+
await reconcileAgentAfterAccountChange(p, { ...agent, accountId: null });
|
|
74
|
+
expect(p.calls).toEqual([[{ id: "a1", slug: "Bot", accountId: null }]]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("skips self-hosted agents", async () => {
|
|
78
|
+
const p = makeProvisioner();
|
|
79
|
+
await reconcileAgentAfterAccountChange(p, { ...agent, selfHosted: true });
|
|
80
|
+
expect(p.calls).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns a warning when reconcile reports a failure for the agent", async () => {
|
|
84
|
+
const p = makeProvisioner(async () => ({
|
|
85
|
+
...EMPTY,
|
|
86
|
+
failed: [{ agentId: "a1", error: "k8s down" }],
|
|
87
|
+
}));
|
|
88
|
+
expect(await reconcileAgentAfterAccountChange(p, agent)).toContain(
|
|
89
|
+
"k8s down",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns a warning instead of throwing when reconcile throws", async () => {
|
|
94
|
+
const p = makeProvisioner(async () => {
|
|
95
|
+
throw new Error("boom");
|
|
96
|
+
});
|
|
97
|
+
expect(await reconcileAgentAfterAccountChange(p, agent)).toContain("boom");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-anthropic-identity.integration.test.ts
|
|
3
|
+
* Integration tests for the PAI-1.1 owner / Anthropic identity columns on
|
|
4
|
+
* Agent against a real PostgreSQL DB.
|
|
5
|
+
*
|
|
6
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
10
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
11
|
+
import { AgentAnthropicIdentityService } from "./agent-anthropic-identity.ts";
|
|
12
|
+
import { ConflictError, NotFoundError } from "./errors.ts";
|
|
13
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
14
|
+
|
|
15
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
16
|
+
|
|
17
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
18
|
+
|
|
19
|
+
function makePrisma(): PrismaClient {
|
|
20
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
21
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describeOrSkip("AgentAnthropicIdentityService (integration)", () => {
|
|
25
|
+
let prisma: PrismaClient;
|
|
26
|
+
let service: AgentAnthropicIdentityService;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
prisma = makePrisma();
|
|
30
|
+
await prisma.agentPhaseMethodology.deleteMany();
|
|
31
|
+
await prisma.agentToken.deleteMany();
|
|
32
|
+
await prisma.agentCronJob.deleteMany();
|
|
33
|
+
await prisma.agentTool.deleteMany();
|
|
34
|
+
await prisma.agentEnv.deleteMany();
|
|
35
|
+
await prisma.agent.deleteMany();
|
|
36
|
+
service = new AgentAnthropicIdentityService(prisma);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await prisma.$disconnect();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("a newly created agent has null identity fields", async () => {
|
|
44
|
+
const agent = await prisma.agent.create({ data: { name: "A" } });
|
|
45
|
+
expect(await service.get(agent.id)).toEqual({
|
|
46
|
+
ownerEmail: null,
|
|
47
|
+
anthropicIdentityName: null,
|
|
48
|
+
anthropicServiceAccountId: null,
|
|
49
|
+
anthropicFederationRuleId: null,
|
|
50
|
+
k8sServiceAccountName: null,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("set() round-trips and supports partial updates", async () => {
|
|
55
|
+
const agent = await prisma.agent.create({ data: { name: "A" } });
|
|
56
|
+
await service.set(agent.id, {
|
|
57
|
+
ownerEmail: "o@example.com",
|
|
58
|
+
anthropicIdentityName: "agent-a",
|
|
59
|
+
anthropicServiceAccountId: "sa_1",
|
|
60
|
+
anthropicFederationRuleId: "fr_1",
|
|
61
|
+
k8sServiceAccountName: "agent-a-sa",
|
|
62
|
+
});
|
|
63
|
+
await service.set(agent.id, { ownerEmail: null });
|
|
64
|
+
expect(await service.get(agent.id)).toEqual({
|
|
65
|
+
ownerEmail: null,
|
|
66
|
+
anthropicIdentityName: "agent-a",
|
|
67
|
+
anthropicServiceAccountId: "sa_1",
|
|
68
|
+
anthropicFederationRuleId: "fr_1",
|
|
69
|
+
k8sServiceAccountName: "agent-a-sa",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rejects a duplicate anthropicIdentityName with ConflictError", async () => {
|
|
74
|
+
const a = await prisma.agent.create({ data: { name: "A" } });
|
|
75
|
+
const b = await prisma.agent.create({ data: { name: "B" } });
|
|
76
|
+
await service.set(a.id, { anthropicIdentityName: "dup" });
|
|
77
|
+
await expect(
|
|
78
|
+
service.set(b.id, { anthropicIdentityName: "dup" }),
|
|
79
|
+
).rejects.toBeInstanceOf(ConflictError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("the unique index also rejects duplicates at the DB level", async () => {
|
|
83
|
+
await prisma.agent.create({
|
|
84
|
+
data: { name: "A", anthropicIdentityName: "dup" },
|
|
85
|
+
});
|
|
86
|
+
await expect(
|
|
87
|
+
(async () =>
|
|
88
|
+
prisma.agent.create({
|
|
89
|
+
data: { name: "B", anthropicIdentityName: "dup" },
|
|
90
|
+
}))(),
|
|
91
|
+
).rejects.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("set() throws NotFoundError for an unknown agent", async () => {
|
|
95
|
+
await expect(
|
|
96
|
+
service.set("missing", { ownerEmail: "x@example.com" }),
|
|
97
|
+
).rejects.toBeInstanceOf(NotFoundError);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-anthropic-identity.ts
|
|
3
|
+
* AgentAnthropicIdentityService — typed read/update of an agent's owner and
|
|
4
|
+
* Anthropic identity columns (PAI-1.1). All fields are nullable; a duplicate
|
|
5
|
+
* anthropicIdentityName (unique index) surfaces as a ConflictError.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
9
|
+
import { ConflictError, NotFoundError } from "./errors.ts";
|
|
10
|
+
|
|
11
|
+
export interface AgentAnthropicIdentity {
|
|
12
|
+
ownerEmail: string | null;
|
|
13
|
+
anthropicIdentityName: string | null;
|
|
14
|
+
anthropicServiceAccountId: string | null;
|
|
15
|
+
anthropicFederationRuleId: string | null;
|
|
16
|
+
k8sServiceAccountName: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Partial update — omitted keys are left untouched, null clears a field. */
|
|
20
|
+
export type AgentAnthropicIdentityUpdate = Partial<AgentAnthropicIdentity>;
|
|
21
|
+
|
|
22
|
+
const IDENTITY_SELECT = {
|
|
23
|
+
ownerEmail: true,
|
|
24
|
+
anthropicIdentityName: true,
|
|
25
|
+
anthropicServiceAccountId: true,
|
|
26
|
+
anthropicFederationRuleId: true,
|
|
27
|
+
k8sServiceAccountName: true,
|
|
28
|
+
} as const satisfies Record<keyof AgentAnthropicIdentity, true>;
|
|
29
|
+
|
|
30
|
+
const IDENTITY_KEYS = Object.keys(
|
|
31
|
+
IDENTITY_SELECT,
|
|
32
|
+
) as (keyof AgentAnthropicIdentity)[];
|
|
33
|
+
|
|
34
|
+
function prismaCode(err: unknown): unknown {
|
|
35
|
+
return (err as { code?: unknown } | null)?.code;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AgentAnthropicIdentityService {
|
|
39
|
+
constructor(private prisma: PrismaClient) {}
|
|
40
|
+
|
|
41
|
+
/** Returns the identity fields, or null if the agent does not exist. */
|
|
42
|
+
async get(agentId: string): Promise<AgentAnthropicIdentity | null> {
|
|
43
|
+
return this.prisma.agent.findUnique({
|
|
44
|
+
where: { id: agentId },
|
|
45
|
+
select: IDENTITY_SELECT,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update the provided identity fields.
|
|
51
|
+
* @throws NotFoundError if the agent does not exist
|
|
52
|
+
* @throws ConflictError if anthropicIdentityName is already used
|
|
53
|
+
*/
|
|
54
|
+
async set(
|
|
55
|
+
agentId: string,
|
|
56
|
+
update: AgentAnthropicIdentityUpdate,
|
|
57
|
+
): Promise<AgentAnthropicIdentity> {
|
|
58
|
+
const data: AgentAnthropicIdentityUpdate = {};
|
|
59
|
+
for (const key of IDENTITY_KEYS) {
|
|
60
|
+
if (update[key] !== undefined) data[key] = update[key];
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await this.prisma.agent.update({
|
|
64
|
+
where: { id: agentId },
|
|
65
|
+
data,
|
|
66
|
+
select: IDENTITY_SELECT,
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (prismaCode(err) === "P2002") {
|
|
70
|
+
throw new ConflictError("anthropicIdentityName is already in use");
|
|
71
|
+
}
|
|
72
|
+
if (prismaCode(err) === "P2025") {
|
|
73
|
+
throw new NotFoundError(`Agent ${agentId} not found`);
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-anthropic-identity.unit.test.ts
|
|
3
|
+
* Unit tests for AgentAnthropicIdentityService — Prisma error mapping and
|
|
4
|
+
* partial-update semantics against an injected prisma test double.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import { AgentAnthropicIdentityService } from "./agent-anthropic-identity.ts";
|
|
9
|
+
import { ConflictError, NotFoundError } from "./errors.ts";
|
|
10
|
+
|
|
11
|
+
type Row = Record<string, string | null>;
|
|
12
|
+
|
|
13
|
+
function makeFakePrisma(opts: { row?: Row | null; updateError?: unknown }) {
|
|
14
|
+
const calls: { update?: unknown } = {};
|
|
15
|
+
const agent = {
|
|
16
|
+
async findUnique() {
|
|
17
|
+
return opts.row ?? null;
|
|
18
|
+
},
|
|
19
|
+
async update(args: { data: Row }) {
|
|
20
|
+
calls.update = args;
|
|
21
|
+
if (opts.updateError) throw opts.updateError;
|
|
22
|
+
return { ...(opts.row ?? {}), ...args.data };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: minimal prisma double
|
|
26
|
+
return { prisma: { agent } as any, calls };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EMPTY = {
|
|
30
|
+
ownerEmail: null,
|
|
31
|
+
anthropicIdentityName: null,
|
|
32
|
+
anthropicServiceAccountId: null,
|
|
33
|
+
anthropicFederationRuleId: null,
|
|
34
|
+
k8sServiceAccountName: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("AgentAnthropicIdentityService", () => {
|
|
38
|
+
it("get() returns the five identity fields", async () => {
|
|
39
|
+
const { prisma } = makeFakePrisma({ row: { ...EMPTY } });
|
|
40
|
+
expect(await new AgentAnthropicIdentityService(prisma).get("a1")).toEqual(
|
|
41
|
+
EMPTY,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("get() returns null for an unknown agent", async () => {
|
|
46
|
+
const { prisma } = makeFakePrisma({ row: null });
|
|
47
|
+
expect(await new AgentAnthropicIdentityService(prisma).get("x")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("set() only writes fields that are present", async () => {
|
|
51
|
+
const { prisma, calls } = makeFakePrisma({ row: { ...EMPTY } });
|
|
52
|
+
await new AgentAnthropicIdentityService(prisma).set("a1", {
|
|
53
|
+
ownerEmail: "o@example.com",
|
|
54
|
+
});
|
|
55
|
+
expect((calls.update as { data: unknown }).data).toEqual({
|
|
56
|
+
ownerEmail: "o@example.com",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("set() maps P2002 to ConflictError", async () => {
|
|
61
|
+
const { prisma } = makeFakePrisma({ updateError: { code: "P2002" } });
|
|
62
|
+
await expect(
|
|
63
|
+
new AgentAnthropicIdentityService(prisma).set("a1", {
|
|
64
|
+
anthropicIdentityName: "dup",
|
|
65
|
+
}),
|
|
66
|
+
).rejects.toBeInstanceOf(ConflictError);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("set() maps P2025 to NotFoundError", async () => {
|
|
70
|
+
const { prisma } = makeFakePrisma({ updateError: { code: "P2025" } });
|
|
71
|
+
await expect(
|
|
72
|
+
new AgentAnthropicIdentityService(prisma).set("nope", {
|
|
73
|
+
ownerEmail: "x@example.com",
|
|
74
|
+
}),
|
|
75
|
+
).rejects.toBeInstanceOf(NotFoundError);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-ask-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure parser for the agent-to-agent ask flag and its allowlist (COS-31.1).
|
|
5
|
+
* Takes an injected env object (no process.env reads) so it is
|
|
6
|
+
* deterministically unit-testable; admin-boot reads it once at startup and
|
|
7
|
+
* injects the resolved config as deps. Same "enabled" convention as the
|
|
8
|
+
* self-serve flag (self-serve-config.ts).
|
|
9
|
+
*
|
|
10
|
+
* SHIPWRIGHT_AGENT_ASK_ALLOWLIST is a comma-separated list of directional
|
|
11
|
+
* `from>to` agent-id pairs. Empty (or absent) denies every pair; a malformed
|
|
12
|
+
* pair throws at boot, naming every offending entry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Structural env shape — accepts `process.env` or a plain test object. */
|
|
16
|
+
type AgentAskEnv = Readonly<Record<string, string | undefined>>;
|
|
17
|
+
|
|
18
|
+
export interface AgentAskConfig {
|
|
19
|
+
/** True only when SHIPWRIGHT_AGENT_ASK_ENABLED === "enabled". */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Allowed directional pairs, each stored as `from>to`. */
|
|
22
|
+
allowlist: ReadonlySet<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One agent id on each side: no whitespace, no `>` and no `,`.
|
|
26
|
+
const PAIR_RE = /^([^\s>,]+)\s*>\s*([^\s>,]+)$/;
|
|
27
|
+
|
|
28
|
+
export function parseAgentAskConfig(env: AgentAskEnv): AgentAskConfig {
|
|
29
|
+
const enabled = env.SHIPWRIGHT_AGENT_ASK_ENABLED === "enabled";
|
|
30
|
+
const allowlist = new Set<string>();
|
|
31
|
+
const malformed: string[] = [];
|
|
32
|
+
for (const raw of (env.SHIPWRIGHT_AGENT_ASK_ALLOWLIST ?? "").split(",")) {
|
|
33
|
+
const entry = raw.trim();
|
|
34
|
+
if (!entry) continue;
|
|
35
|
+
const match = PAIR_RE.exec(entry);
|
|
36
|
+
if (!match) {
|
|
37
|
+
malformed.push(entry);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
allowlist.add(`${match[1]}>${match[2]}`);
|
|
41
|
+
}
|
|
42
|
+
if (malformed.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`[admin] invalid SHIPWRIGHT_AGENT_ASK_ALLOWLIST pair(s): ${malformed
|
|
45
|
+
.map((p) => JSON.stringify(p))
|
|
46
|
+
.join(", ")} (expected comma-separated from>to agent ids)`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return { enabled, allowlist };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether `from` may ask `to` (directional; empty allowlist denies all). */
|
|
53
|
+
export function isAskAllowed(
|
|
54
|
+
config: Pick<AgentAskConfig, "allowlist">,
|
|
55
|
+
from: string,
|
|
56
|
+
to: string,
|
|
57
|
+
): boolean {
|
|
58
|
+
return config.allowlist.has(`${from}>${to}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-ask-config.unit.test.ts
|
|
3
|
+
* Pure parser tests for the agent-ask flag and from>to allowlist (COS-31.1).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import { isAskAllowed, parseAgentAskConfig } from "./agent-ask-config.ts";
|
|
8
|
+
|
|
9
|
+
describe("parseAgentAskConfig — flag", () => {
|
|
10
|
+
it('is on only for the literal "enabled"', () => {
|
|
11
|
+
expect(
|
|
12
|
+
parseAgentAskConfig({ SHIPWRIGHT_AGENT_ASK_ENABLED: "enabled" }).enabled,
|
|
13
|
+
).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('"true", "" and absent are off', () => {
|
|
17
|
+
for (const value of ["true", "", "Enabled", " enabled", undefined]) {
|
|
18
|
+
expect(
|
|
19
|
+
parseAgentAskConfig({ SHIPWRIGHT_AGENT_ASK_ENABLED: value }).enabled,
|
|
20
|
+
).toBe(false);
|
|
21
|
+
}
|
|
22
|
+
expect(parseAgentAskConfig({}).enabled).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("parseAgentAskConfig — allowlist", () => {
|
|
27
|
+
it("parses comma-separated from>to pairs, trimming whitespace", () => {
|
|
28
|
+
const config = parseAgentAskConfig({
|
|
29
|
+
SHIPWRIGHT_AGENT_ASK_ALLOWLIST: " a > b ,c>d,, ",
|
|
30
|
+
});
|
|
31
|
+
expect([...config.allowlist].sort()).toEqual(["a>b", "c>d"]);
|
|
32
|
+
expect(isAskAllowed(config, "a", "b")).toBe(true);
|
|
33
|
+
expect(isAskAllowed(config, "c", "d")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("pairs are directional", () => {
|
|
37
|
+
const config = parseAgentAskConfig({
|
|
38
|
+
SHIPWRIGHT_AGENT_ASK_ALLOWLIST: "a>b",
|
|
39
|
+
});
|
|
40
|
+
expect(isAskAllowed(config, "b", "a")).toBe(false);
|
|
41
|
+
expect(isAskAllowed(config, "a", "c")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("an empty or absent allowlist denies every pair", () => {
|
|
45
|
+
for (const value of ["", " ", " , ", undefined]) {
|
|
46
|
+
const config = parseAgentAskConfig({
|
|
47
|
+
SHIPWRIGHT_AGENT_ASK_ENABLED: "enabled",
|
|
48
|
+
SHIPWRIGHT_AGENT_ASK_ALLOWLIST: value,
|
|
49
|
+
});
|
|
50
|
+
expect(config.allowlist.size).toBe(0);
|
|
51
|
+
expect(isAskAllowed(config, "a", "b")).toBe(false);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("a malformed pair throws naming it", () => {
|
|
56
|
+
for (const bad of ["ab", "a>", ">b", "a>b>c", "a b>c"]) {
|
|
57
|
+
expect(() =>
|
|
58
|
+
parseAgentAskConfig({
|
|
59
|
+
SHIPWRIGHT_AGENT_ASK_ALLOWLIST: `x>y,${bad}`,
|
|
60
|
+
}),
|
|
61
|
+
).toThrow(JSON.stringify(bad));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("names every malformed pair in one error", () => {
|
|
66
|
+
expect(() =>
|
|
67
|
+
parseAgentAskConfig({ SHIPWRIGHT_AGENT_ASK_ALLOWLIST: "one,a>b,two>" }),
|
|
68
|
+
).toThrow(/"one".*"two>"/);
|
|
69
|
+
});
|
|
70
|
+
});
|