@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
package/src/agent-ask.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-ask.ts
|
|
3
|
+
*
|
|
4
|
+
* Agent-to-agent ask (COS-31.1): POST /agents/{id}/ask posts a question from
|
|
5
|
+
* one agent (`from`) into a new `kind: "ask"` chat thread of the target agent
|
|
6
|
+
* (`{id}`), optionally waits for the reply, and audits every ask in the
|
|
7
|
+
* AgentAsk table. GET /agents/{id}/asks lists the audit rows.
|
|
8
|
+
*
|
|
9
|
+
* Gates, in order: flag (SHIPWRIGHT_AGENT_ASK_ENABLED, 404 when off) → caller
|
|
10
|
+
* (`*`, platform.ops, or the agent key whose id equals `from`) → chat client
|
|
11
|
+
* wired (503) → depth capped at 1 (400) → from>to pair allowlisted (403) →
|
|
12
|
+
* target agent exists (404). Nothing touches the chat service before every
|
|
13
|
+
* gate passed. The question text is never stored or logged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { callerLabel } from "@app-vitals/agent-lib/request-context";
|
|
17
|
+
import { createRoute, type OpenAPIHono } from "@hono/zod-openapi";
|
|
18
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
19
|
+
import { type AgentAskConfig, isAskAllowed } from "./agent-ask-config.ts";
|
|
20
|
+
import type { AgentService } from "./agents.ts";
|
|
21
|
+
import type { AdminAuthEnv, AdminScope } from "./api-auth.ts";
|
|
22
|
+
import { type Clock, SystemClock } from "./clock.ts";
|
|
23
|
+
import { BadRequestError, ForbiddenError, NotFoundError } from "./errors.ts";
|
|
24
|
+
import type { ChatClient, ChatMessage } from "./http-chat-client.ts";
|
|
25
|
+
import {
|
|
26
|
+
AgentAskBodySchema,
|
|
27
|
+
AgentAskListSchema,
|
|
28
|
+
AgentAskResponseSchema,
|
|
29
|
+
AgentIdParamSchema,
|
|
30
|
+
ErrorSchema,
|
|
31
|
+
ListAgentAsksQuerySchema,
|
|
32
|
+
} from "./openapi-schemas.ts";
|
|
33
|
+
|
|
34
|
+
// ─── Audit store ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export type AskStatus = "created" | "pending" | "replied" | "failed";
|
|
37
|
+
|
|
38
|
+
/** One AgentAsk audit row. No question text, by design. */
|
|
39
|
+
export interface AskAuditRow {
|
|
40
|
+
id: string;
|
|
41
|
+
fromAgentId: string;
|
|
42
|
+
toAgentId: string;
|
|
43
|
+
threadId: string | null;
|
|
44
|
+
depth: number;
|
|
45
|
+
waitMs: number;
|
|
46
|
+
ttlSeconds: number;
|
|
47
|
+
status: string;
|
|
48
|
+
callerLabel: string;
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
repliedAt: Date | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type AskAuditCreate = Pick<
|
|
54
|
+
AskAuditRow,
|
|
55
|
+
| "fromAgentId"
|
|
56
|
+
| "toAgentId"
|
|
57
|
+
| "depth"
|
|
58
|
+
| "waitMs"
|
|
59
|
+
| "ttlSeconds"
|
|
60
|
+
| "status"
|
|
61
|
+
| "callerLabel"
|
|
62
|
+
>;
|
|
63
|
+
|
|
64
|
+
type AskAuditPatch = Partial<
|
|
65
|
+
Pick<AskAuditRow, "threadId" | "status" | "repliedAt">
|
|
66
|
+
>;
|
|
67
|
+
|
|
68
|
+
export interface AgentAskStore {
|
|
69
|
+
create(input: AskAuditCreate): Promise<AskAuditRow>;
|
|
70
|
+
update(id: string, patch: AskAuditPatch): Promise<AskAuditRow>;
|
|
71
|
+
/** Asks the agent sent or received, newest first. */
|
|
72
|
+
listForAgent(
|
|
73
|
+
agentId: string,
|
|
74
|
+
page: { limit: number; offset: number },
|
|
75
|
+
): Promise<{ asks: AskAuditRow[]; total: number }>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Prisma-backed AgentAsk audit store. */
|
|
79
|
+
export class AgentAskService implements AgentAskStore {
|
|
80
|
+
constructor(private prisma: Pick<PrismaClient, "agentAsk">) {}
|
|
81
|
+
|
|
82
|
+
create(input: AskAuditCreate): Promise<AskAuditRow> {
|
|
83
|
+
return this.prisma.agentAsk.create({ data: input });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
update(id: string, patch: AskAuditPatch): Promise<AskAuditRow> {
|
|
87
|
+
return this.prisma.agentAsk.update({ where: { id }, data: patch });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async listForAgent(
|
|
91
|
+
agentId: string,
|
|
92
|
+
page: { limit: number; offset: number },
|
|
93
|
+
): Promise<{ asks: AskAuditRow[]; total: number }> {
|
|
94
|
+
const where = {
|
|
95
|
+
OR: [{ toAgentId: agentId }, { fromAgentId: agentId }],
|
|
96
|
+
};
|
|
97
|
+
const [asks, total] = await Promise.all([
|
|
98
|
+
this.prisma.agentAsk.findMany({
|
|
99
|
+
where,
|
|
100
|
+
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
|
101
|
+
take: page.limit,
|
|
102
|
+
skip: page.offset,
|
|
103
|
+
}),
|
|
104
|
+
this.prisma.agentAsk.count({ where }),
|
|
105
|
+
]);
|
|
106
|
+
return { asks, total };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Deps ─────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** The chat-service calls the ask flow makes. */
|
|
113
|
+
export type AskChatClient = Pick<
|
|
114
|
+
ChatClient,
|
|
115
|
+
"createThread" | "createMessage" | "listMessages"
|
|
116
|
+
>;
|
|
117
|
+
|
|
118
|
+
export interface AgentAskDeps {
|
|
119
|
+
config: AgentAskConfig;
|
|
120
|
+
store: AgentAskStore;
|
|
121
|
+
/** Injected for deterministic polling; defaults to the system clock. */
|
|
122
|
+
clock?: Clock;
|
|
123
|
+
/** Injected for deterministic polling; defaults to a real timer. */
|
|
124
|
+
sleep?: (ms: number) => Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface RegisterDeps {
|
|
128
|
+
agentAsk?: AgentAskDeps;
|
|
129
|
+
chatClient?: AskChatClient;
|
|
130
|
+
agentService: Pick<AgentService, "exists">;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Poll cadence while waiting for a reply. */
|
|
134
|
+
export const ASK_POLL_INTERVAL_MS = 1000;
|
|
135
|
+
|
|
136
|
+
const realSleep = (ms: number) =>
|
|
137
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
138
|
+
|
|
139
|
+
// ─── Routes ───────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const jsonError = {
|
|
142
|
+
content: { "application/json": { schema: ErrorSchema } },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const askRoute = createRoute({
|
|
146
|
+
method: "post",
|
|
147
|
+
path: "/agents/{id}/ask",
|
|
148
|
+
summary: "Ask another agent a question",
|
|
149
|
+
description:
|
|
150
|
+
"Posts `question` from agent `from` into a new `kind: ask` chat thread of agent `{id}` (always created with `askDepth: 1`), then, when `waitMs > 0`, polls once per second until the agent replies or the budget ends. Callers: `*`, `platform.ops`, or the agent key whose id equals `from` (never impersonation). The `from>{id}` pair must be on SHIPWRIGHT_AGENT_ASK_ALLOWLIST. Every ask writes an AgentAsk audit row (no question text). 404 unless SHIPWRIGHT_AGENT_ASK_ENABLED is `enabled`; 503 when the chat service is not configured.",
|
|
151
|
+
request: {
|
|
152
|
+
params: AgentIdParamSchema,
|
|
153
|
+
body: {
|
|
154
|
+
content: { "application/json": { schema: AgentAskBodySchema } },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
responses: {
|
|
158
|
+
201: {
|
|
159
|
+
description: "Ask posted; `replied` with the reply, or `pending`",
|
|
160
|
+
content: { "application/json": { schema: AgentAskResponseSchema } },
|
|
161
|
+
},
|
|
162
|
+
400: {
|
|
163
|
+
description: "Bad request (including depth other than 1)",
|
|
164
|
+
...jsonError,
|
|
165
|
+
},
|
|
166
|
+
403: {
|
|
167
|
+
description: "Caller not allowed, or the pair is not allowlisted",
|
|
168
|
+
...jsonError,
|
|
169
|
+
},
|
|
170
|
+
404: {
|
|
171
|
+
description: "Agent not found, or agent ask disabled",
|
|
172
|
+
...jsonError,
|
|
173
|
+
},
|
|
174
|
+
503: { description: "Chat service not configured", ...jsonError },
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const listAsksRoute = createRoute({
|
|
179
|
+
method: "get",
|
|
180
|
+
path: "/agents/{id}/asks",
|
|
181
|
+
summary: "List agent asks",
|
|
182
|
+
description:
|
|
183
|
+
"Lists the AgentAsk audit rows the agent sent or received, newest first. `*`, `platform.read` and `platform.ops` callers only. 404 unless SHIPWRIGHT_AGENT_ASK_ENABLED is `enabled`.",
|
|
184
|
+
request: { params: AgentIdParamSchema, query: ListAgentAsksQuerySchema },
|
|
185
|
+
responses: {
|
|
186
|
+
200: {
|
|
187
|
+
description: "Audit rows",
|
|
188
|
+
content: { "application/json": { schema: AgentAskListSchema } },
|
|
189
|
+
},
|
|
190
|
+
400: { description: "Bad request", ...jsonError },
|
|
191
|
+
403: { description: "Forbidden", ...jsonError },
|
|
192
|
+
404: { description: "Agent ask disabled", ...jsonError },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function scopeKind(c: { get: (k: "scope") => unknown }) {
|
|
197
|
+
return (c.get("scope") as AdminScope | undefined)?.kind;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The reply to `userMessageId`: the first assistant message after it. */
|
|
201
|
+
function findReply(
|
|
202
|
+
messages: ChatMessage[],
|
|
203
|
+
userMessageId: string,
|
|
204
|
+
): { repliedAt: Date; body?: string } | null {
|
|
205
|
+
const idx = messages.findIndex((m) => m.id === userMessageId);
|
|
206
|
+
const user = messages[idx];
|
|
207
|
+
if (!user?.repliedAt) return null;
|
|
208
|
+
const reply = messages.slice(idx + 1).find((m) => m.role === "assistant");
|
|
209
|
+
return {
|
|
210
|
+
repliedAt: new Date(user.repliedAt),
|
|
211
|
+
...(reply ? { body: reply.body } : {}),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function toRecord(row: AskAuditRow) {
|
|
216
|
+
return {
|
|
217
|
+
id: row.id,
|
|
218
|
+
fromAgentId: row.fromAgentId,
|
|
219
|
+
toAgentId: row.toAgentId,
|
|
220
|
+
threadId: row.threadId,
|
|
221
|
+
depth: row.depth,
|
|
222
|
+
waitMs: row.waitMs,
|
|
223
|
+
ttlSeconds: row.ttlSeconds,
|
|
224
|
+
status: row.status,
|
|
225
|
+
callerLabel: row.callerLabel,
|
|
226
|
+
createdAt: row.createdAt.toISOString(),
|
|
227
|
+
repliedAt: row.repliedAt ? row.repliedAt.toISOString() : null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Register the ask routes on the admin app. Must run after the /agents/*
|
|
233
|
+
* auth middleware is mounted.
|
|
234
|
+
*/
|
|
235
|
+
export function registerAgentAskRoutes(
|
|
236
|
+
app: OpenAPIHono<AdminAuthEnv>,
|
|
237
|
+
deps: RegisterDeps,
|
|
238
|
+
): void {
|
|
239
|
+
const { agentAsk, chatClient, agentService } = deps;
|
|
240
|
+
const enabled = agentAsk?.config.enabled === true;
|
|
241
|
+
|
|
242
|
+
// Flag gate ahead of body/query validation, so a disabled route is a plain
|
|
243
|
+
// 404 whatever the request looks like (as accounts-api does for self-serve).
|
|
244
|
+
for (const path of ["/agents/:id/ask", "/agents/:id/asks"]) {
|
|
245
|
+
app.use(path, async (c, next) => {
|
|
246
|
+
if (!enabled) return c.json({ error: "Not found" }, 404);
|
|
247
|
+
await next();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
app.openapi(askRoute, async (c) => {
|
|
252
|
+
if (!agentAsk) throw new NotFoundError();
|
|
253
|
+
const { id: to } = c.req.valid("param");
|
|
254
|
+
const body = c.req.valid("json");
|
|
255
|
+
const { from, question, waitMs, ttlSeconds } = body;
|
|
256
|
+
|
|
257
|
+
const scope = c.get("scope") as AdminScope | undefined;
|
|
258
|
+
const allowedCaller =
|
|
259
|
+
c.get("isAdmin") === true ||
|
|
260
|
+
scope?.kind === "platform-ops" ||
|
|
261
|
+
(scope?.kind === "agent" && scope.agentId === from);
|
|
262
|
+
if (!allowedCaller) {
|
|
263
|
+
throw new ForbiddenError(
|
|
264
|
+
"Only *, platform.ops or the agent's own key (as `from`) can ask",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (!chatClient) {
|
|
268
|
+
return c.json({ error: "chat service not configured" }, 503);
|
|
269
|
+
}
|
|
270
|
+
if (body.depth !== 1) throw new BadRequestError("depth is capped at 1");
|
|
271
|
+
if (!isAskAllowed(agentAsk.config, from, to)) {
|
|
272
|
+
throw new ForbiddenError("ask not allowed for this pair");
|
|
273
|
+
}
|
|
274
|
+
if (!(await agentService.exists(to))) {
|
|
275
|
+
throw new NotFoundError(`agent ${to} not found`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const clock = agentAsk.clock ?? SystemClock();
|
|
279
|
+
const sleep = agentAsk.sleep ?? realSleep;
|
|
280
|
+
const label = callerLabel(c.get("caller"), scope?.kind);
|
|
281
|
+
const log = (askId: string, status: string) =>
|
|
282
|
+
console.log(
|
|
283
|
+
JSON.stringify({
|
|
284
|
+
event: "agent-ask",
|
|
285
|
+
askId,
|
|
286
|
+
from,
|
|
287
|
+
to,
|
|
288
|
+
status,
|
|
289
|
+
callerLabel: label,
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const row = await agentAsk.store.create({
|
|
294
|
+
fromAgentId: from,
|
|
295
|
+
toAgentId: to,
|
|
296
|
+
depth: 1,
|
|
297
|
+
waitMs,
|
|
298
|
+
ttlSeconds,
|
|
299
|
+
status: "created",
|
|
300
|
+
callerLabel: label,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
let threadId: string | null = null;
|
|
304
|
+
let userMessageId: string;
|
|
305
|
+
try {
|
|
306
|
+
const thread = await chatClient.createThread(to, {
|
|
307
|
+
kind: "ask",
|
|
308
|
+
askFromAgentId: from,
|
|
309
|
+
askDepth: 1,
|
|
310
|
+
title: `ask from ${from} - ${clock.now().toISOString()}`,
|
|
311
|
+
});
|
|
312
|
+
threadId = thread.id;
|
|
313
|
+
userMessageId = (
|
|
314
|
+
await chatClient.createMessage(thread.id, "user", question)
|
|
315
|
+
).id;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
await agentAsk.store.update(row.id, { status: "failed", threadId });
|
|
318
|
+
log(row.id, "failed");
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let reply: { repliedAt: Date; body?: string } | null = null;
|
|
323
|
+
const deadline = clock.now().getTime() + waitMs;
|
|
324
|
+
for (;;) {
|
|
325
|
+
const remaining = deadline - clock.now().getTime();
|
|
326
|
+
if (remaining <= 0) break;
|
|
327
|
+
await sleep(Math.min(ASK_POLL_INTERVAL_MS, remaining));
|
|
328
|
+
try {
|
|
329
|
+
const { messages } = await chatClient.listMessages(threadId);
|
|
330
|
+
reply = findReply(messages, userMessageId);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
// The question is delivered; a failed poll only shortens the wait.
|
|
333
|
+
console.warn(
|
|
334
|
+
`[agent-ask] poll failed for ask ${row.id}: ${err instanceof Error ? err.message : String(err)}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (reply) break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const status = reply ? "replied" : "pending";
|
|
341
|
+
await agentAsk.store.update(row.id, {
|
|
342
|
+
threadId,
|
|
343
|
+
status,
|
|
344
|
+
repliedAt: reply?.repliedAt ?? null,
|
|
345
|
+
});
|
|
346
|
+
log(row.id, status);
|
|
347
|
+
return c.json(
|
|
348
|
+
{
|
|
349
|
+
askId: row.id,
|
|
350
|
+
threadId,
|
|
351
|
+
status,
|
|
352
|
+
...(reply?.body !== undefined ? { reply: reply.body } : {}),
|
|
353
|
+
},
|
|
354
|
+
201,
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
app.openapi(listAsksRoute, async (c) => {
|
|
359
|
+
if (!agentAsk) throw new NotFoundError();
|
|
360
|
+
const kind = scopeKind(c);
|
|
361
|
+
if (
|
|
362
|
+
c.get("isAdmin") !== true &&
|
|
363
|
+
kind !== "platform-read" &&
|
|
364
|
+
kind !== "platform-ops"
|
|
365
|
+
) {
|
|
366
|
+
throw new ForbiddenError(
|
|
367
|
+
"Only *, platform.read and platform.ops callers can list asks",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const { id } = c.req.valid("param");
|
|
371
|
+
const { limit, offset } = c.req.valid("query");
|
|
372
|
+
const { asks, total } = await agentAsk.store.listForAgent(id, {
|
|
373
|
+
limit,
|
|
374
|
+
offset,
|
|
375
|
+
});
|
|
376
|
+
return c.json({ asks: asks.map(toRecord), total, limit, offset }, 200);
|
|
377
|
+
});
|
|
378
|
+
}
|