@app-vitals/agent-admin 0.0.1 → 0.1.1
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,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-manifest.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure builders for the Kubernetes manifests that provision a single Shipwright
|
|
5
|
+
* agent: a per-agent Deployment, its Opaque token Secret, and the RFC1123 name
|
|
6
|
+
* derivation that ties them together.
|
|
7
|
+
*
|
|
8
|
+
* These functions are PURE — no fs, no network, no clock. They only shape wire
|
|
9
|
+
* objects. The actual k8s API calls live in ./kubernetes-client.ts; this module
|
|
10
|
+
* returns the same `KubernetesDeployment` / `KubernetesSecret` wire types so the
|
|
11
|
+
* two stay structurally consistent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import type {
|
|
16
|
+
KubernetesDeployment,
|
|
17
|
+
KubernetesEnvVar,
|
|
18
|
+
KubernetesPvc,
|
|
19
|
+
KubernetesSecret,
|
|
20
|
+
} from "./kubernetes-client.ts";
|
|
21
|
+
import type { ServiceEnvSpec } from "./service-integration.ts";
|
|
22
|
+
|
|
23
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Agent health/liveness port (matches the agent's `SHIPWRIGHT_HEALTH_PORT`). */
|
|
26
|
+
export const AGENT_HEALTH_PORT = 3459;
|
|
27
|
+
|
|
28
|
+
/** Where the agent's persistent home volume is mounted in the container. */
|
|
29
|
+
export const AGENT_HOME_MOUNT_PATH = "/data/agent-home";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Repo clones and git worktrees must live on the PVC. The plugin commands
|
|
33
|
+
* default to `$HOME/src` / `$HOME/worktrees`, which sit on the container's
|
|
34
|
+
* ephemeral overlay layer — a single worktree with node_modules can exceed
|
|
35
|
+
* the pod's ephemeral-storage limit and get the pod evicted.
|
|
36
|
+
*/
|
|
37
|
+
const AGENT_REPO_DIR = `${AGENT_HOME_MOUNT_PATH}/workspace/repos`;
|
|
38
|
+
const AGENT_WORKTREE_DIR = `${AGENT_HOME_MOUNT_PATH}/workspace/worktrees`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional per-field overrides for the agent container's resources. Any field
|
|
42
|
+
* left unset falls back to the default in `resolveAgentContainerResources`.
|
|
43
|
+
*/
|
|
44
|
+
export interface AgentContainerResourceOverrides {
|
|
45
|
+
/** CPU request, e.g. "500m". */
|
|
46
|
+
cpuRequest?: string;
|
|
47
|
+
/**
|
|
48
|
+
* CPU limit, e.g. "2". Applied to tenant pods ONLY (agents with an
|
|
49
|
+
* accountId); platform agents never get a CPU limit.
|
|
50
|
+
*/
|
|
51
|
+
cpuLimit?: string;
|
|
52
|
+
/** Memory request, e.g. "2Gi". */
|
|
53
|
+
memoryRequest?: string;
|
|
54
|
+
/** Memory limit, e.g. "8Gi". */
|
|
55
|
+
memoryLimit?: string;
|
|
56
|
+
/**
|
|
57
|
+
* ephemeral-storage, applied to BOTH request and limit (kept equal — see
|
|
58
|
+
* rationale below). e.g. "4Gi".
|
|
59
|
+
*/
|
|
60
|
+
ephemeralStorage?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Default CPU limit for tenant pods (agents with an accountId). */
|
|
64
|
+
const DEFAULT_TENANT_CPU_LIMIT = "2";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the agent container's resources.requests/limits, applying any
|
|
68
|
+
* caller-supplied overrides on top of today's defaults (500m cpu / 2Gi memory
|
|
69
|
+
* request / 8Gi memory limit / 4Gi ephemeral storage, no CPU limit). Called
|
|
70
|
+
* from `buildAgentDeploymentManifest` with `opts.resources` — omitted or
|
|
71
|
+
* partial overrides fall back field-by-field to the default below.
|
|
72
|
+
*
|
|
73
|
+
* Precedence (highest wins): specialty override > env override > default.
|
|
74
|
+
* `overrides` here is the ENV layer (populated in admin/src/main.ts's
|
|
75
|
+
* buildProvisioner from SHIPWRIGHT_K8S_AGENT_* vars). The per-type
|
|
76
|
+
* SpecialtyResourcesSchema in specialty-registry.ts is already parsed as
|
|
77
|
+
* part of SpecialtyManifestSchema.resources but is NOT consumed here or by
|
|
78
|
+
* any provisioner — it stays inert until a future task wires specialty-level
|
|
79
|
+
* resource resolution in ahead of this function's env layer.
|
|
80
|
+
*
|
|
81
|
+
* Defaults: requests mirror the GKE Autopilot defaults the agent ran with
|
|
82
|
+
* before these were explicit. The memory limit exists to contain a runaway
|
|
83
|
+
* Claude run to its own container (OOM-kill) rather than letting it grow
|
|
84
|
+
* until the kubelet evicts neighbouring pods under node memory pressure
|
|
85
|
+
* (observed: ~11.6Gi used against the 2Gi request). No CPU limit for
|
|
86
|
+
* platform agents — CPU contention throttles instead of evicting. Tenant pods
|
|
87
|
+
* (`tenant: true`) get `DEFAULT_TENANT_CPU_LIMIT` (or `overrides.cpuLimit`) so
|
|
88
|
+
* one account's agent cannot starve neighbours in the shared namespace; with
|
|
89
|
+
* no ResourceQuota, per-account bounds are maxAgents x these fixed limits.
|
|
90
|
+
*
|
|
91
|
+
* ephemeral-storage defaults to 4Gi, not the 1Gi Autopilot default: 1Gi left
|
|
92
|
+
* no room for a single build step's scratch space, and agents were evicted
|
|
93
|
+
* mid-run ("Pod ephemeral local storage usage exceeds the total limit of
|
|
94
|
+
* containers 1Gi"), losing the in-flight task. Tool caches are pinned to the
|
|
95
|
+
* PVC in the agent's runMiseStartup, so this budget only has to cover
|
|
96
|
+
* genuinely transient writes — e.g. a scan skill staging tool binaries in a
|
|
97
|
+
* mktemp dir. Requests and limits are kept equal because Autopilot bills the
|
|
98
|
+
* request either way.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveAgentContainerResources(
|
|
101
|
+
overrides?: AgentContainerResourceOverrides,
|
|
102
|
+
tenant = false,
|
|
103
|
+
): { requests: Record<string, string>; limits: Record<string, string> } {
|
|
104
|
+
const ephemeralStorage = overrides?.ephemeralStorage ?? "4Gi";
|
|
105
|
+
return {
|
|
106
|
+
requests: {
|
|
107
|
+
cpu: overrides?.cpuRequest ?? "500m",
|
|
108
|
+
memory: overrides?.memoryRequest ?? "2Gi",
|
|
109
|
+
"ephemeral-storage": ephemeralStorage,
|
|
110
|
+
},
|
|
111
|
+
limits: {
|
|
112
|
+
...(tenant
|
|
113
|
+
? { cpu: overrides?.cpuLimit ?? DEFAULT_TENANT_CPU_LIMIT }
|
|
114
|
+
: {}),
|
|
115
|
+
memory: overrides?.memoryLimit ?? "8Gi",
|
|
116
|
+
"ephemeral-storage": ephemeralStorage,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Non-root uid/gid the agent runs as (matches the agent image). */
|
|
122
|
+
const AGENT_RUN_AS = 1000;
|
|
123
|
+
|
|
124
|
+
/** RFC1123 label length cap. */
|
|
125
|
+
const MAX_NAME_LEN = 63;
|
|
126
|
+
|
|
127
|
+
/** Length of the appended disambiguation hash suffix. */
|
|
128
|
+
const HASH_SUFFIX_LEN = 8;
|
|
129
|
+
|
|
130
|
+
const NAME_LABEL = "app.kubernetes.io/name";
|
|
131
|
+
const INSTANCE_LABEL = "app.kubernetes.io/instance";
|
|
132
|
+
const MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
|
|
133
|
+
const AGENT_ID_LABEL = "shipwright.dev/agent-id";
|
|
134
|
+
/** Marks pods belonging to a tenant account (set only when accountId is present). */
|
|
135
|
+
export const TENANT_LABEL = "shipwright.dev/tenant";
|
|
136
|
+
const AGENT_APP_NAME = "shipwright-agent";
|
|
137
|
+
|
|
138
|
+
/** Volume name, mount dir, and ConfigMap key for the Claude Code managed policy. */
|
|
139
|
+
export const CLAUDE_POLICY_VOLUME = "claude-policy";
|
|
140
|
+
export const CLAUDE_POLICY_MOUNT_PATH = "/etc/claude-code";
|
|
141
|
+
const CLAUDE_POLICY_FILE = "managed-settings.json";
|
|
142
|
+
/** Pod-template annotation carrying the policy hash so a policy change rolls pods. */
|
|
143
|
+
export const CLAUDE_POLICY_HASH_ANNOTATION =
|
|
144
|
+
"shipwright.dev/claude-policy-hash";
|
|
145
|
+
/** 0444 — policy is read-only to the agent user. */
|
|
146
|
+
const CLAUDE_POLICY_FILE_MODE = 292;
|
|
147
|
+
|
|
148
|
+
/** Volume name and mount dir for the projected Anthropic identity token. */
|
|
149
|
+
export const ANTHROPIC_TOKEN_VOLUME = "anthropic-token";
|
|
150
|
+
export const ANTHROPIC_TOKEN_MOUNT_PATH = "/var/run/secrets/anthropic.com";
|
|
151
|
+
const ANTHROPIC_TOKEN_FILE = "token";
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Every env var the federation wiring may inject, in manifest order. The
|
|
155
|
+
* reconcile patch walks this list so a var that is no longer desired (feature
|
|
156
|
+
* off, or workspace id unset) is deleted from live Deployments.
|
|
157
|
+
*/
|
|
158
|
+
export const ANTHROPIC_FEDERATION_ENV_NAMES = [
|
|
159
|
+
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
160
|
+
"ANTHROPIC_FEDERATION_RULE_ID",
|
|
161
|
+
"ANTHROPIC_ORGANIZATION_ID",
|
|
162
|
+
"ANTHROPIC_SERVICE_ACCOUNT_ID",
|
|
163
|
+
"ANTHROPIC_WORKSPACE_ID",
|
|
164
|
+
] as const;
|
|
165
|
+
|
|
166
|
+
// ─── Name sanitization ──────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Derive an RFC1123-compliant, collision-resistant Kubernetes object name from
|
|
170
|
+
* an arbitrary agent id.
|
|
171
|
+
*
|
|
172
|
+
* RFC1123 labels must be lowercase alphanumerics or `-`, start/end alphanumeric,
|
|
173
|
+
* and be ≤63 chars.
|
|
174
|
+
*
|
|
175
|
+
* Collision strategy: sanitization is inherently lossy — `agent_1`, `agent.1`,
|
|
176
|
+
* and `Agent-1` all reduce to the same `agent-1` base, and long ids must be
|
|
177
|
+
* truncated. Whenever the cleaned base is NOT a byte-for-byte match of the
|
|
178
|
+
* original id (i.e. any lowercasing, char replacement, collapsing, trimming, or
|
|
179
|
+
* truncation happened), we append `-<8-hex>` where the hex is the leading bytes
|
|
180
|
+
* of `sha256(originalId)`. The hash is over the FULL original id, so two ids
|
|
181
|
+
* that share a cleaned prefix still get distinct suffixes. A lossless id (already
|
|
182
|
+
* valid RFC1123) is returned unchanged. The base is truncated as needed so that
|
|
183
|
+
* `base + "-" + suffix` still fits within 63 chars.
|
|
184
|
+
*/
|
|
185
|
+
export function sanitizeAgentName(id: string): string {
|
|
186
|
+
const base = cleanBase(id);
|
|
187
|
+
|
|
188
|
+
// Lossless: the id was already a valid RFC1123 label — return as-is.
|
|
189
|
+
if (base === id && base.length > 0 && base.length <= MAX_NAME_LEN) {
|
|
190
|
+
return base;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const suffix = createHash("sha256")
|
|
194
|
+
.update(id)
|
|
195
|
+
.digest("hex")
|
|
196
|
+
.slice(0, HASH_SUFFIX_LEN);
|
|
197
|
+
|
|
198
|
+
// Reserve room for "-" + suffix, then re-trim any dash exposed at the new end.
|
|
199
|
+
const room = MAX_NAME_LEN - (suffix.length + 1);
|
|
200
|
+
const truncated = trimDashes(base.slice(0, Math.max(0, room)));
|
|
201
|
+
const prefix = truncated.length > 0 ? `${truncated}-` : "";
|
|
202
|
+
return `${prefix}${suffix}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Lowercase, replace invalid runs with a single dash, trim dash padding. */
|
|
206
|
+
function cleanBase(id: string): string {
|
|
207
|
+
const lowered = id.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
208
|
+
return trimDashes(lowered);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function trimDashes(s: string): string {
|
|
212
|
+
return s.replace(/^-+/, "").replace(/-+$/, "");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── PVC builder ────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
export interface AgentPvcOpts {
|
|
218
|
+
name: string;
|
|
219
|
+
namespace: string;
|
|
220
|
+
sizeGi: number;
|
|
221
|
+
storageClassName?: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function buildAgentPvcManifest(opts: AgentPvcOpts): KubernetesPvc {
|
|
225
|
+
const manifest: KubernetesPvc = {
|
|
226
|
+
apiVersion: "v1",
|
|
227
|
+
kind: "PersistentVolumeClaim",
|
|
228
|
+
metadata: {
|
|
229
|
+
name: opts.name,
|
|
230
|
+
namespace: opts.namespace,
|
|
231
|
+
},
|
|
232
|
+
spec: {
|
|
233
|
+
accessModes: ["ReadWriteOnce"],
|
|
234
|
+
resources: { requests: { storage: `${opts.sizeGi}Gi` } },
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
if (opts.storageClassName !== undefined) {
|
|
238
|
+
manifest.spec.storageClassName = opts.storageClassName;
|
|
239
|
+
}
|
|
240
|
+
return manifest;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Deployment builder ─────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
export interface AgentDeploymentOpts {
|
|
246
|
+
/** Original agent id (used verbatim in env + label, sanitized for the name). */
|
|
247
|
+
agentId: string;
|
|
248
|
+
namespace: string;
|
|
249
|
+
/** Agent container image (without tag). */
|
|
250
|
+
image: string;
|
|
251
|
+
/** Image tag, joined as `image:tag`. */
|
|
252
|
+
imageTag: string;
|
|
253
|
+
/** In-cluster admin/API base URL the agent calls home to. */
|
|
254
|
+
apiUrl: string;
|
|
255
|
+
/** Name of the pre-provisioned PersistentVolumeClaim for the agent home. */
|
|
256
|
+
pvcName: string;
|
|
257
|
+
/** Name of the Secret holding the per-agent token. */
|
|
258
|
+
secretName: string;
|
|
259
|
+
/** Key within the Secret's data that holds the token. Defaults to "token". */
|
|
260
|
+
tokenSecretKey?: string;
|
|
261
|
+
/** Replica count. Defaults to 1. */
|
|
262
|
+
replicas?: number;
|
|
263
|
+
/**
|
|
264
|
+
* Owning account. When set the pod is a tenant pod: the tenant label is added
|
|
265
|
+
* to the Deployment and pod template, and a CPU limit is applied. Omit for
|
|
266
|
+
* platform agents (manifest unchanged).
|
|
267
|
+
*/
|
|
268
|
+
accountId?: string | null;
|
|
269
|
+
/**
|
|
270
|
+
* Optional container resource overrides, merged field-by-field over the
|
|
271
|
+
* defaults in `resolveAgentContainerResources`. Omit (or leave individual
|
|
272
|
+
* fields unset) to keep today's values for those fields.
|
|
273
|
+
*/
|
|
274
|
+
resources?: AgentContainerResourceOverrides;
|
|
275
|
+
/**
|
|
276
|
+
* Optional agent-voice (STT/TTS) configuration. When omitted, the provisioned
|
|
277
|
+
* agent pod gets only the 3 base env vars (voice disabled). When present, the
|
|
278
|
+
* relevant vars are appended after the base vars:
|
|
279
|
+
* - whisperServiceUrl → WHISPER_SERVICE_URL (provider=whisper)
|
|
280
|
+
* - groqApiKey → GROQ_API_KEY (provider=groq)
|
|
281
|
+
* - elevenLabsApiKey → ELEVENLABS_API_KEY (TTS, either provider)
|
|
282
|
+
* - voiceId → ELEVENLABS_VOICE_ID (optional TTS voice override)
|
|
283
|
+
* Only the keys that are set are injected — this mirrors the agent's own
|
|
284
|
+
* env contract (agent/src/runtime/config.ts).
|
|
285
|
+
*/
|
|
286
|
+
voice?: AgentVoiceEnv;
|
|
287
|
+
/**
|
|
288
|
+
* Optional service integrations (see service-integration.ts), in order. Each
|
|
289
|
+
* entry with a `url` injects its `tokenEnv` (secretKeyRef from the agent
|
|
290
|
+
* Secret, key `secretKey`) then its `urlEnv` (plain value). Placed after the
|
|
291
|
+
* base vars and before the built-in chat-service env vars.
|
|
292
|
+
*/
|
|
293
|
+
serviceIntegrations?: ServiceEnvSpec[];
|
|
294
|
+
/**
|
|
295
|
+
* When set, inject SHIPWRIGHT_CHAT_SERVICE_TOKEN (via secretKeyRef from the
|
|
296
|
+
* agent Secret) and SHIPWRIGHT_CHAT_SERVICE_URL (as a plain value) into the
|
|
297
|
+
* agent Deployment. Placed after the service-integration env vars and before voice env vars.
|
|
298
|
+
*/
|
|
299
|
+
chatServiceUrl?: string;
|
|
300
|
+
/**
|
|
301
|
+
* Key within the agent Secret under which the chat-service token is stored.
|
|
302
|
+
* Defaults to "chat-service-token".
|
|
303
|
+
*/
|
|
304
|
+
chatServiceTokenSecretKey?: string;
|
|
305
|
+
/**
|
|
306
|
+
* When set, mount the named ConfigMap's `managed-settings.json` read-only at
|
|
307
|
+
* /etc/claude-code (Claude Code's managed-policy directory). `hash` is
|
|
308
|
+
* optional: when non-empty it is stamped on the pod template as an annotation
|
|
309
|
+
* so a policy change rolls the pods; when absent/empty no annotation is added.
|
|
310
|
+
* Omit entirely for an unchanged manifest.
|
|
311
|
+
*/
|
|
312
|
+
claudePolicy?: { configMapName: string; hash?: string };
|
|
313
|
+
/**
|
|
314
|
+
* When set, the pod runs as `serviceAccountName` and carries a projected
|
|
315
|
+
* service-account token (volume `anthropic-token`, mounted read-only at
|
|
316
|
+
* /var/run/secrets/anthropic.com) plus the env the Anthropic client needs to
|
|
317
|
+
* exchange that token for a short-lived access token (workload identity
|
|
318
|
+
* federation). Omit for an unchanged manifest.
|
|
319
|
+
*/
|
|
320
|
+
anthropicFederation?: AgentAnthropicFederation;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Anthropic Workload Identity Federation settings for provisioned agent pods.
|
|
325
|
+
* Parsed from the admin's SHIPWRIGHT_K8S_AGENT_* env in main.ts.
|
|
326
|
+
*/
|
|
327
|
+
export interface AgentAnthropicFederation {
|
|
328
|
+
/** Kubernetes ServiceAccount the pod runs as (the token's subject). */
|
|
329
|
+
serviceAccountName: string;
|
|
330
|
+
/** → ANTHROPIC_FEDERATION_RULE_ID */
|
|
331
|
+
ruleId: string;
|
|
332
|
+
/** → ANTHROPIC_ORGANIZATION_ID */
|
|
333
|
+
organizationId: string;
|
|
334
|
+
/** → ANTHROPIC_SERVICE_ACCOUNT_ID */
|
|
335
|
+
serviceAccountId: string;
|
|
336
|
+
/** → ANTHROPIC_WORKSPACE_ID (only injected when set). */
|
|
337
|
+
workspaceId?: string;
|
|
338
|
+
/** Audience of the projected token. */
|
|
339
|
+
audience: string;
|
|
340
|
+
/** Lifetime of the projected token; Kubernetes requires >= 600. */
|
|
341
|
+
expirationSeconds: number;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Build the federation env entries, in ANTHROPIC_FEDERATION_ENV_NAMES order.
|
|
346
|
+
* Returns an empty array when federation is disabled.
|
|
347
|
+
*/
|
|
348
|
+
function anthropicFederationEnvEntries(
|
|
349
|
+
fed: AgentAnthropicFederation | undefined,
|
|
350
|
+
): { name: string; value: string }[] {
|
|
351
|
+
if (!fed) return [];
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
name: "ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
355
|
+
value: `${ANTHROPIC_TOKEN_MOUNT_PATH}/${ANTHROPIC_TOKEN_FILE}`,
|
|
356
|
+
},
|
|
357
|
+
{ name: "ANTHROPIC_FEDERATION_RULE_ID", value: fed.ruleId },
|
|
358
|
+
{ name: "ANTHROPIC_ORGANIZATION_ID", value: fed.organizationId },
|
|
359
|
+
{ name: "ANTHROPIC_SERVICE_ACCOUNT_ID", value: fed.serviceAccountId },
|
|
360
|
+
...(fed.workspaceId
|
|
361
|
+
? [{ name: "ANTHROPIC_WORKSPACE_ID", value: fed.workspaceId }]
|
|
362
|
+
: []),
|
|
363
|
+
];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The subset of voice env an admin provisioner flows into a provisioned agent
|
|
368
|
+
* pod. The admin reads these from its OWN env (sourced from the chart's voice
|
|
369
|
+
* Secret + whisper Service URL); this builder only shapes the pod env from them.
|
|
370
|
+
*/
|
|
371
|
+
export interface AgentVoiceEnv {
|
|
372
|
+
/** http(s) URL of the self-hosted Whisper pod (provider=whisper). */
|
|
373
|
+
whisperServiceUrl?: string;
|
|
374
|
+
/** Groq API key (provider=groq). */
|
|
375
|
+
groqApiKey?: string;
|
|
376
|
+
/** ElevenLabs API key (TTS). */
|
|
377
|
+
elevenLabsApiKey?: string;
|
|
378
|
+
/** Optional ElevenLabs voice id override. */
|
|
379
|
+
voiceId?: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Build the ordered voice env entries from a partial voice config. Only set
|
|
384
|
+
* keys are emitted; an empty/undefined config yields no entries (voice
|
|
385
|
+
* disabled). Order is stable for deterministic manifests/tests.
|
|
386
|
+
*/
|
|
387
|
+
function voiceEnvEntries(
|
|
388
|
+
voice: AgentVoiceEnv | undefined,
|
|
389
|
+
): { name: string; value: string }[] {
|
|
390
|
+
if (!voice) return [];
|
|
391
|
+
const entries: { name: string; value: string }[] = [];
|
|
392
|
+
if (voice.whisperServiceUrl) {
|
|
393
|
+
entries.push({
|
|
394
|
+
name: "WHISPER_SERVICE_URL",
|
|
395
|
+
value: voice.whisperServiceUrl,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (voice.groqApiKey) {
|
|
399
|
+
entries.push({ name: "GROQ_API_KEY", value: voice.groqApiKey });
|
|
400
|
+
}
|
|
401
|
+
if (voice.elevenLabsApiKey) {
|
|
402
|
+
entries.push({ name: "ELEVENLABS_API_KEY", value: voice.elevenLabsApiKey });
|
|
403
|
+
}
|
|
404
|
+
if (voice.voiceId) {
|
|
405
|
+
entries.push({ name: "ELEVENLABS_VOICE_ID", value: voice.voiceId });
|
|
406
|
+
}
|
|
407
|
+
return entries;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Build env entries for the optional service integrations, in list order. Each
|
|
412
|
+
* configured integration (url set) yields two entries: its token env
|
|
413
|
+
* (secretKeyRef from the agent Secret) then its URL env (plain value).
|
|
414
|
+
* Unconfigured integrations yield nothing.
|
|
415
|
+
*/
|
|
416
|
+
function serviceIntegrationEnvEntries(
|
|
417
|
+
opts: AgentDeploymentOpts,
|
|
418
|
+
): KubernetesEnvVar[] {
|
|
419
|
+
return (opts.serviceIntegrations ?? []).flatMap((svc) => {
|
|
420
|
+
if (!svc.url) return [];
|
|
421
|
+
return [
|
|
422
|
+
{
|
|
423
|
+
name: svc.tokenEnv,
|
|
424
|
+
valueFrom: {
|
|
425
|
+
secretKeyRef: { name: opts.secretName, key: svc.secretKey },
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{ name: svc.urlEnv, value: svc.url },
|
|
429
|
+
];
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Build chat-service env entries when chatServiceUrl is configured. Returns two
|
|
435
|
+
* entries: SHIPWRIGHT_CHAT_SERVICE_TOKEN (secretKeyRef from the agent Secret)
|
|
436
|
+
* and SHIPWRIGHT_CHAT_SERVICE_URL (plain value). Returns an empty array when
|
|
437
|
+
* chatServiceUrl is absent (chat-service wiring disabled).
|
|
438
|
+
*/
|
|
439
|
+
function chatServiceEnvEntries(opts: AgentDeploymentOpts): KubernetesEnvVar[] {
|
|
440
|
+
if (!opts.chatServiceUrl) return [];
|
|
441
|
+
const csKey = opts.chatServiceTokenSecretKey ?? "chat-service-token";
|
|
442
|
+
return [
|
|
443
|
+
{
|
|
444
|
+
name: "SHIPWRIGHT_CHAT_SERVICE_TOKEN",
|
|
445
|
+
valueFrom: {
|
|
446
|
+
secretKeyRef: { name: opts.secretName, key: csKey },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: "SHIPWRIGHT_CHAT_SERVICE_URL",
|
|
451
|
+
value: opts.chatServiceUrl,
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function buildAgentDeploymentManifest(
|
|
457
|
+
opts: AgentDeploymentOpts,
|
|
458
|
+
): KubernetesDeployment {
|
|
459
|
+
const name = sanitizeAgentName(opts.agentId);
|
|
460
|
+
const tokenKey = opts.tokenSecretKey ?? "token";
|
|
461
|
+
const volumeName = "agent-home";
|
|
462
|
+
|
|
463
|
+
const labels: Record<string, string> = {
|
|
464
|
+
[NAME_LABEL]: AGENT_APP_NAME,
|
|
465
|
+
[INSTANCE_LABEL]: name,
|
|
466
|
+
[MANAGED_BY_LABEL]: "shipwright-admin",
|
|
467
|
+
[AGENT_ID_LABEL]: opts.agentId,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const tenant = Boolean(opts.accountId);
|
|
471
|
+
if (tenant) labels[TENANT_LABEL] = "true";
|
|
472
|
+
|
|
473
|
+
// Selectors are immutable post-create and must not include volatile values;
|
|
474
|
+
// the instance label is the stable per-agent identity.
|
|
475
|
+
const selectorLabels: Record<string, string> = {
|
|
476
|
+
[NAME_LABEL]: AGENT_APP_NAME,
|
|
477
|
+
[INSTANCE_LABEL]: name,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const policy = opts.claudePolicy;
|
|
481
|
+
const federation = opts.anthropicFederation;
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
apiVersion: "apps/v1",
|
|
485
|
+
kind: "Deployment",
|
|
486
|
+
metadata: {
|
|
487
|
+
name,
|
|
488
|
+
namespace: opts.namespace,
|
|
489
|
+
labels,
|
|
490
|
+
},
|
|
491
|
+
spec: {
|
|
492
|
+
replicas: opts.replicas ?? 1,
|
|
493
|
+
// Recreate: Slack Socket Mode allows only one active connection per token.
|
|
494
|
+
// RollingUpdate would briefly create a second pod that fights for the socket.
|
|
495
|
+
strategy: { type: "Recreate" },
|
|
496
|
+
selector: { matchLabels: selectorLabels },
|
|
497
|
+
template: {
|
|
498
|
+
metadata: {
|
|
499
|
+
labels: tenant
|
|
500
|
+
? { ...selectorLabels, [TENANT_LABEL]: "true" }
|
|
501
|
+
: selectorLabels,
|
|
502
|
+
...(policy?.hash
|
|
503
|
+
? { annotations: { [CLAUDE_POLICY_HASH_ANNOTATION]: policy.hash } }
|
|
504
|
+
: {}),
|
|
505
|
+
},
|
|
506
|
+
spec: {
|
|
507
|
+
// Federation: the projected token's subject is this ServiceAccount.
|
|
508
|
+
...(federation
|
|
509
|
+
? { serviceAccountName: federation.serviceAccountName }
|
|
510
|
+
: {}),
|
|
511
|
+
securityContext: {
|
|
512
|
+
fsGroup: AGENT_RUN_AS,
|
|
513
|
+
fsGroupChangePolicy: "OnRootMismatch",
|
|
514
|
+
runAsNonRoot: true,
|
|
515
|
+
runAsUser: AGENT_RUN_AS,
|
|
516
|
+
},
|
|
517
|
+
// 120s grace period mirrors the Helm-managed agent template and gives
|
|
518
|
+
// the agent time to finish an in-flight Claude response before SIGKILL.
|
|
519
|
+
terminationGracePeriodSeconds: 120,
|
|
520
|
+
volumes: [
|
|
521
|
+
{
|
|
522
|
+
name: volumeName,
|
|
523
|
+
persistentVolumeClaim: { claimName: opts.pvcName },
|
|
524
|
+
},
|
|
525
|
+
...(policy
|
|
526
|
+
? [
|
|
527
|
+
{
|
|
528
|
+
name: CLAUDE_POLICY_VOLUME,
|
|
529
|
+
configMap: {
|
|
530
|
+
name: policy.configMapName,
|
|
531
|
+
items: [
|
|
532
|
+
{ key: CLAUDE_POLICY_FILE, path: CLAUDE_POLICY_FILE },
|
|
533
|
+
],
|
|
534
|
+
defaultMode: CLAUDE_POLICY_FILE_MODE,
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
]
|
|
538
|
+
: []),
|
|
539
|
+
...(federation
|
|
540
|
+
? [
|
|
541
|
+
{
|
|
542
|
+
name: ANTHROPIC_TOKEN_VOLUME,
|
|
543
|
+
projected: {
|
|
544
|
+
sources: [
|
|
545
|
+
{
|
|
546
|
+
serviceAccountToken: {
|
|
547
|
+
audience: federation.audience,
|
|
548
|
+
expirationSeconds: federation.expirationSeconds,
|
|
549
|
+
path: ANTHROPIC_TOKEN_FILE,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
]
|
|
556
|
+
: []),
|
|
557
|
+
],
|
|
558
|
+
containers: [
|
|
559
|
+
{
|
|
560
|
+
name: AGENT_APP_NAME,
|
|
561
|
+
image: `${opts.image}:${opts.imageTag}`,
|
|
562
|
+
ports: [{ containerPort: AGENT_HEALTH_PORT, protocol: "TCP" }],
|
|
563
|
+
resources: resolveAgentContainerResources(opts.resources, tenant),
|
|
564
|
+
env: [
|
|
565
|
+
{ name: "SHIPWRIGHT_AGENT_ID", value: opts.agentId },
|
|
566
|
+
{ name: "SHIPWRIGHT_API_URL", value: opts.apiUrl },
|
|
567
|
+
{
|
|
568
|
+
name: "SHIPWRIGHT_AGENT_API_KEY",
|
|
569
|
+
valueFrom: {
|
|
570
|
+
secretKeyRef: { name: opts.secretName, key: tokenKey },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
// Tell the agent where its persistent home directory is mounted.
|
|
574
|
+
{ name: "AGENT_HOME", value: AGENT_HOME_MOUNT_PATH },
|
|
575
|
+
// Route repo clones and worktrees to the PVC (see constants).
|
|
576
|
+
{ name: "SHIPWRIGHT_REPO_DIR", value: AGENT_REPO_DIR },
|
|
577
|
+
{ name: "SHIPWRIGHT_WORKTREE_DIR", value: AGENT_WORKTREE_DIR },
|
|
578
|
+
// Service-integration env (e.g. task-store, registered by the
|
|
579
|
+
// coding extension) — injected after the base vars for each
|
|
580
|
+
// configured integration; omitted entirely when none are.
|
|
581
|
+
...serviceIntegrationEnvEntries(opts),
|
|
582
|
+
// Chat-service env — injected after integration vars when chat
|
|
583
|
+
// service is configured; omitted entirely when chatServiceUrl is absent.
|
|
584
|
+
...chatServiceEnvEntries(opts),
|
|
585
|
+
// Voice (STT/TTS) env — appended only for the keys that are set
|
|
586
|
+
// (none when voice is disabled, preserving the 6 base vars).
|
|
587
|
+
...voiceEnvEntries(opts.voice),
|
|
588
|
+
// Workload identity federation env — none when disabled.
|
|
589
|
+
...anthropicFederationEnvEntries(federation),
|
|
590
|
+
],
|
|
591
|
+
volumeMounts: [
|
|
592
|
+
{ name: volumeName, mountPath: AGENT_HOME_MOUNT_PATH },
|
|
593
|
+
// Directory mount (not subPath) so ConfigMap updates propagate.
|
|
594
|
+
...(policy
|
|
595
|
+
? [
|
|
596
|
+
{
|
|
597
|
+
name: CLAUDE_POLICY_VOLUME,
|
|
598
|
+
mountPath: CLAUDE_POLICY_MOUNT_PATH,
|
|
599
|
+
readOnly: true,
|
|
600
|
+
},
|
|
601
|
+
]
|
|
602
|
+
: []),
|
|
603
|
+
...(federation
|
|
604
|
+
? [
|
|
605
|
+
{
|
|
606
|
+
name: ANTHROPIC_TOKEN_VOLUME,
|
|
607
|
+
mountPath: ANTHROPIC_TOKEN_MOUNT_PATH,
|
|
608
|
+
readOnly: true,
|
|
609
|
+
},
|
|
610
|
+
]
|
|
611
|
+
: []),
|
|
612
|
+
],
|
|
613
|
+
// Gate liveness/readiness until the agent's health server binds.
|
|
614
|
+
// Startup runs `mise install` + plugin install, which can exceed a
|
|
615
|
+
// minute on a cold/contended node; without this, liveness would
|
|
616
|
+
// restart the container (~75s in) before startup ever finished.
|
|
617
|
+
// 18 × 10s = 180s grace, aligned with the entrypoint startup
|
|
618
|
+
// watchdog (DEFAULT_STARTUP_TIMEOUT_MS in agent/src/runtime/entrypoint.ts).
|
|
619
|
+
startupProbe: {
|
|
620
|
+
httpGet: { path: "/health", port: AGENT_HEALTH_PORT },
|
|
621
|
+
periodSeconds: 10,
|
|
622
|
+
failureThreshold: 18,
|
|
623
|
+
},
|
|
624
|
+
livenessProbe: {
|
|
625
|
+
httpGet: { path: "/health", port: AGENT_HEALTH_PORT },
|
|
626
|
+
initialDelaySeconds: 15,
|
|
627
|
+
periodSeconds: 30,
|
|
628
|
+
// Explicit, not the 1s k8s default — the health server shares
|
|
629
|
+
// its process with claude -p child spawns and check-helpers.ts's
|
|
630
|
+
// spawnSync("gh", ...) calls, which block the whole process for
|
|
631
|
+
// the duration of the gh CLI call (network round-trip to GitHub,
|
|
632
|
+
// not just local CPU work). 15s is a bridge until SIO-1.2 converts
|
|
633
|
+
// those to async spawn; no fixed timeout is truly safe against a
|
|
634
|
+
// slow gh response, so this isn't a permanent fix.
|
|
635
|
+
timeoutSeconds: 15,
|
|
636
|
+
failureThreshold: 3,
|
|
637
|
+
},
|
|
638
|
+
readinessProbe: {
|
|
639
|
+
httpGet: { path: "/health", port: AGENT_HEALTH_PORT },
|
|
640
|
+
initialDelaySeconds: 10,
|
|
641
|
+
periodSeconds: 10,
|
|
642
|
+
timeoutSeconds: 15,
|
|
643
|
+
failureThreshold: 3,
|
|
644
|
+
},
|
|
645
|
+
securityContext: {
|
|
646
|
+
runAsNonRoot: true,
|
|
647
|
+
runAsUser: AGENT_RUN_AS,
|
|
648
|
+
allowPrivilegeEscalation: false,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ─── Secret builder ─────────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
export interface AgentSecretOpts {
|
|
661
|
+
name: string;
|
|
662
|
+
namespace: string;
|
|
663
|
+
/** Plaintext per-agent token; base64-encoded into the Secret's `data`. */
|
|
664
|
+
token: string;
|
|
665
|
+
/** Key under which the token is stored. Defaults to "token". */
|
|
666
|
+
tokenKey?: string;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export function buildAgentSecretManifest(
|
|
670
|
+
opts: AgentSecretOpts,
|
|
671
|
+
): KubernetesSecret {
|
|
672
|
+
const tokenKey = opts.tokenKey ?? "token";
|
|
673
|
+
return {
|
|
674
|
+
apiVersion: "v1",
|
|
675
|
+
kind: "Secret",
|
|
676
|
+
type: "Opaque",
|
|
677
|
+
metadata: {
|
|
678
|
+
name: opts.name,
|
|
679
|
+
namespace: opts.namespace,
|
|
680
|
+
},
|
|
681
|
+
data: {
|
|
682
|
+
[tokenKey]: Buffer.from(opts.token).toString("base64"),
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|