@raybase/core 0.0.2 → 0.3.4
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/LICENSE +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
|
@@ -0,0 +1,2828 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isHostUserId } from "./host-user.js";
|
|
3
|
+
import { normalizeStoredAdminRole } from "./admin-role.js";
|
|
4
|
+
import { audit, authSecret, createInvite, normalizeRoles } from "./auth.js";
|
|
5
|
+
import { agentTokenClaims, raybaseAgentInternalReadPath, signAgentToken } from "./agent.js";
|
|
6
|
+
import { raybaseRemoteMcpSystemTables, remoteMcpSecretNameFor } from "./agent-remote-mcp.js";
|
|
7
|
+
import { bindAgentRemoteMcpServer, findInstalledRemoteMcpServerRow, installRemoteMcpServer, installedRemoteMcpOverview, parseRemoteMcpAccessSpec, rediscoverRemoteMcpServer, remoteMcpBindingView, remoteMcpInstallFindings, setInstalledRemoteMcpServerStatus, unbindAgentRemoteMcpServer } from "./agent-remote-mcp-install.js";
|
|
8
|
+
import { RemoteMcpCredentialStore, remoteMcpCredentialStatus, remoteMcpKeyFromEnv } from "./agent-remote-mcp-credentials.js";
|
|
9
|
+
import { beginRemoteMcpOAuthConnect, discoverRemoteMcpOAuth } from "./agent-remote-mcp-oauth.js";
|
|
10
|
+
import { discoverAgentSkills } from "./agent-skill-discovery.js";
|
|
11
|
+
import { applyAgentSkills, refreshAgentSkill, syncDueAgentSkillSources } from "./agent-skill-import.js";
|
|
12
|
+
import { createAgentSkillUpload, deleteAgentSkill, editAgentSkill, findAgentSkill, listAgentSkillFiles, listAgentSkills, raybaseAgentSkillSystemTables, readAgentSkillFile, removeManagedAgentSkillSource, setAgentSkillEnabled } from "./agent-skill-store.js";
|
|
13
|
+
import { auditArchiveEnabled, parseAuditHotDays } from "./audit-archive.js";
|
|
14
|
+
import { inputSchemaForTable } from "./data-client.js";
|
|
15
|
+
import { redactSecretValue } from "./data-secrets.js";
|
|
16
|
+
import { controlTableNames, createRaybaseDb, dataStub, isRaybaseBackupOperationalTableName } from "./db.js";
|
|
17
|
+
import { importRowsTableName, importRunsTableName } from "./imports.js";
|
|
18
|
+
import { raybaseAgentConversationName, raybaseRuntimeSettingsAppId } from "./platform.js";
|
|
19
|
+
import { describePolicy } from "./policies.js";
|
|
20
|
+
import { pitrRetentionMs, pitrUnavailableReason } from "./pitr.js";
|
|
21
|
+
import { getReferenceName } from "./schema.js";
|
|
22
|
+
import { compileOperatorAppDataQuery, operatorAppDataQueryInput, redactCompiledQueryParams, tableByInput } from "./operator-data-query.js";
|
|
23
|
+
import { retrievalIndexDetail, retrievalIndexesSummary, retrievalReindexRunsQuery } from "./retrieval.js";
|
|
24
|
+
import { raybaseRuntimeErrorEventsTableName, runtimeErrorDetailQuery, runtimeErrorRetentionDays, runtimeErrorSummaryQuery } from "./runtime-errors.js";
|
|
25
|
+
import { searchIndex, searchIndexDetail, searchIndexesSummary, searchReindexRunsQuery, updateSearchForDataChange } from "./search.js";
|
|
26
|
+
import { appSettingsSubjectId, canWriteCoreSetting, createSqlSettingsStorage, resetRaybaseSettingOverride, setRaybaseSettingOverride, settingAuditMetadata, settingValueChanged } from "./settings.js";
|
|
27
|
+
import { raybaseFilesTableName } from "./storage.js";
|
|
28
|
+
import { raybaseSupportRequestsTableName } from "./support.js";
|
|
29
|
+
import { listMcpGrants } from "./mcp.js";
|
|
30
|
+
import { nowIso, q } from "./sql.js";
|
|
31
|
+
import { ensureManifestTables } from "./system-tables.js";
|
|
32
|
+
import { validateOutboundOrigin } from "./outbound-https.js";
|
|
33
|
+
const listInput = z
|
|
34
|
+
.object({
|
|
35
|
+
limit: z.coerce.number().int().min(1).max(500).default(50),
|
|
36
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
const sampleListInput = z
|
|
40
|
+
.object({
|
|
41
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
42
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
const timeRangeInput = {
|
|
46
|
+
from: z.string().optional(),
|
|
47
|
+
to: z.string().optional()
|
|
48
|
+
};
|
|
49
|
+
const auditQueryInput = listInput
|
|
50
|
+
.extend({
|
|
51
|
+
...timeRangeInput,
|
|
52
|
+
action: z.string().optional(),
|
|
53
|
+
targetType: z.string().optional(),
|
|
54
|
+
targetId: z.string().optional(),
|
|
55
|
+
actorId: z.string().optional(),
|
|
56
|
+
actorEmail: z.string().optional(),
|
|
57
|
+
search: z.string().optional()
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
const jobRunsQueryInput = listInput
|
|
61
|
+
.extend({
|
|
62
|
+
...timeRangeInput,
|
|
63
|
+
status: z.string().optional(),
|
|
64
|
+
jobName: z.string().optional(),
|
|
65
|
+
actorId: z.string().optional(),
|
|
66
|
+
runId: z.string().optional(),
|
|
67
|
+
batchId: z.string().optional()
|
|
68
|
+
})
|
|
69
|
+
.strict();
|
|
70
|
+
const jobRunDetailInput = z.object({ runId: z.string().min(1) }).strict();
|
|
71
|
+
const jobBatchesQueryInput = z
|
|
72
|
+
.object({
|
|
73
|
+
limit: z.coerce.number().int().min(1).max(500).default(50),
|
|
74
|
+
status: z.enum(["open", "sealed", "completed"]).optional(),
|
|
75
|
+
name: z.string().optional(),
|
|
76
|
+
key: z.string().optional()
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
const jobBatchDetailInput = z
|
|
80
|
+
.object({
|
|
81
|
+
batchId: z.string().min(1),
|
|
82
|
+
status: z.enum(["queued", "leased", "running", "succeeded", "failed", "denied", "cancelled", "expired"]).optional(),
|
|
83
|
+
cursor: z.string().optional(),
|
|
84
|
+
limit: z.coerce.number().int().min(1).max(100).default(50)
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
const retrievalIndexDetailInput = z.object({ indexName: z.string().min(1) }).strict();
|
|
88
|
+
const retrievalRunsQueryInput = listInput
|
|
89
|
+
.extend({
|
|
90
|
+
indexName: z.string().optional(),
|
|
91
|
+
backend: z.enum(["vectorize", "sqlite"]).optional(),
|
|
92
|
+
namespace: z.string().optional()
|
|
93
|
+
})
|
|
94
|
+
.strict();
|
|
95
|
+
const searchIndexDetailInput = z.object({ indexName: z.string().min(1) }).strict();
|
|
96
|
+
const searchRunsQueryInput = listInput
|
|
97
|
+
.extend({
|
|
98
|
+
indexName: z.string().optional(),
|
|
99
|
+
status: z.string().optional()
|
|
100
|
+
})
|
|
101
|
+
.strict();
|
|
102
|
+
const importRunsQueryInput = listInput
|
|
103
|
+
.extend({
|
|
104
|
+
...timeRangeInput,
|
|
105
|
+
table: z.string().optional(),
|
|
106
|
+
status: z.string().optional(),
|
|
107
|
+
actorId: z.string().optional(),
|
|
108
|
+
runId: z.string().optional()
|
|
109
|
+
})
|
|
110
|
+
.strict();
|
|
111
|
+
const importRunDetailInput = z.object({ runId: z.string().min(1) }).strict();
|
|
112
|
+
const userDetailInput = z
|
|
113
|
+
.object({
|
|
114
|
+
id: z.string().optional(),
|
|
115
|
+
email: z.string().email().optional()
|
|
116
|
+
})
|
|
117
|
+
.refine((value) => Boolean(value.id || value.email), "id or email is required");
|
|
118
|
+
const appDataSearchInput = listInput
|
|
119
|
+
.extend({
|
|
120
|
+
indexName: z.string().default("app"),
|
|
121
|
+
table: z.string().optional(),
|
|
122
|
+
query: z.string().min(1)
|
|
123
|
+
})
|
|
124
|
+
.strict();
|
|
125
|
+
const appRecordDetailInput = z.object({ table: z.string().min(1), id: z.string().min(1) }).strict();
|
|
126
|
+
const appDataCreateInput = z
|
|
127
|
+
.object({
|
|
128
|
+
table: z.string().min(1),
|
|
129
|
+
values: z.record(z.string(), z.unknown())
|
|
130
|
+
})
|
|
131
|
+
.strict();
|
|
132
|
+
const appDataUpdateInput = z
|
|
133
|
+
.object({
|
|
134
|
+
table: z.string().min(1),
|
|
135
|
+
id: z.string().min(1),
|
|
136
|
+
patch: z.record(z.string(), z.unknown())
|
|
137
|
+
})
|
|
138
|
+
.strict();
|
|
139
|
+
const appDataDeleteInput = z
|
|
140
|
+
.object({
|
|
141
|
+
table: z.string().min(1),
|
|
142
|
+
id: z.string().min(1),
|
|
143
|
+
confirm: z.literal(true)
|
|
144
|
+
})
|
|
145
|
+
.strict();
|
|
146
|
+
const appSettingSetInput = z
|
|
147
|
+
.object({
|
|
148
|
+
key: z.string().min(1),
|
|
149
|
+
value: z.unknown()
|
|
150
|
+
})
|
|
151
|
+
.strict();
|
|
152
|
+
const appSettingResetInput = z.object({ key: z.string().min(1) }).strict();
|
|
153
|
+
const userInviteCreateInput = z
|
|
154
|
+
.object({
|
|
155
|
+
email: z.string().email(),
|
|
156
|
+
appRoles: z.array(z.string().min(1)).optional(),
|
|
157
|
+
name: z.string().min(1).optional()
|
|
158
|
+
})
|
|
159
|
+
.strict();
|
|
160
|
+
const userDeactivateInput = z
|
|
161
|
+
.object({
|
|
162
|
+
id: z.string().min(1).optional(),
|
|
163
|
+
email: z.string().email().optional(),
|
|
164
|
+
reason: z.string().min(1).max(500).optional(),
|
|
165
|
+
confirm: z.literal(true)
|
|
166
|
+
})
|
|
167
|
+
.strict()
|
|
168
|
+
.refine((value) => Boolean(value.id || value.email), "id or email is required");
|
|
169
|
+
const userReactivateInput = z
|
|
170
|
+
.object({
|
|
171
|
+
id: z.string().min(1).optional(),
|
|
172
|
+
email: z.string().email().optional()
|
|
173
|
+
})
|
|
174
|
+
.strict()
|
|
175
|
+
.refine((value) => Boolean(value.id || value.email), "id or email is required");
|
|
176
|
+
const userSessionsRevokeInput = z
|
|
177
|
+
.object({
|
|
178
|
+
id: z.string().min(1).optional(),
|
|
179
|
+
email: z.string().email().optional(),
|
|
180
|
+
reason: z.string().min(1).max(500).optional(),
|
|
181
|
+
confirm: z.literal(true)
|
|
182
|
+
})
|
|
183
|
+
.strict()
|
|
184
|
+
.refine((value) => Boolean(value.id || value.email), "id or email is required");
|
|
185
|
+
const findingQueryInput = listInput
|
|
186
|
+
.extend({
|
|
187
|
+
status: z.string().optional(),
|
|
188
|
+
severity: z.string().optional(),
|
|
189
|
+
confidence: z.string().optional(),
|
|
190
|
+
route: z.string().optional(),
|
|
191
|
+
action: z.string().optional(),
|
|
192
|
+
job: z.string().optional(),
|
|
193
|
+
table: z.string().optional()
|
|
194
|
+
})
|
|
195
|
+
.strict();
|
|
196
|
+
const findingDetailInput = z.object({ id: z.string().min(1) }).strict();
|
|
197
|
+
const agentConversationsQueryInput = listInput
|
|
198
|
+
.extend({
|
|
199
|
+
...timeRangeInput,
|
|
200
|
+
agentName: z.string().min(1).optional(),
|
|
201
|
+
runtime: z.string().optional(),
|
|
202
|
+
triggerType: z.string().optional(),
|
|
203
|
+
status: z.enum(["active", "cleanupRequested", "deleting", "cleanupError", "deleted", "archived"]).optional(),
|
|
204
|
+
lifecycleState: z.enum(["active", "cleanup", "deleted", "archived"]).optional(),
|
|
205
|
+
principalKind: z.enum(["user", "public"]).optional(),
|
|
206
|
+
userId: z.string().optional(),
|
|
207
|
+
publicScopeHash: z.string().optional(),
|
|
208
|
+
organizationId: z.string().optional(),
|
|
209
|
+
surface: z.string().optional(),
|
|
210
|
+
route: z.string().optional(),
|
|
211
|
+
search: z.string().optional()
|
|
212
|
+
})
|
|
213
|
+
.strict();
|
|
214
|
+
const agentConversationReadInput = listInput
|
|
215
|
+
.extend({
|
|
216
|
+
conversationId: z.string().min(1),
|
|
217
|
+
agentName: z.string().min(1).optional()
|
|
218
|
+
})
|
|
219
|
+
.strict();
|
|
220
|
+
const agentRunsQueryInput = listInput
|
|
221
|
+
.extend({
|
|
222
|
+
...timeRangeInput,
|
|
223
|
+
conversationId: z.string().optional(),
|
|
224
|
+
agentName: z.string().optional(),
|
|
225
|
+
status: z.string().optional(),
|
|
226
|
+
triggerType: z.string().optional(),
|
|
227
|
+
runId: z.string().optional()
|
|
228
|
+
})
|
|
229
|
+
.strict();
|
|
230
|
+
const aiUsageQueryInput = listInput
|
|
231
|
+
.extend({
|
|
232
|
+
...timeRangeInput,
|
|
233
|
+
conversationId: z.string().optional(),
|
|
234
|
+
agent: z.string().optional(),
|
|
235
|
+
model: z.string().optional(),
|
|
236
|
+
requestKind: z.string().optional(),
|
|
237
|
+
status: z.enum(["ok", "error"]).optional(),
|
|
238
|
+
principalKind: z.enum(["user", "public", "service"]).optional(),
|
|
239
|
+
userId: z.string().optional(),
|
|
240
|
+
serviceName: z.string().optional(),
|
|
241
|
+
jobName: z.string().optional(),
|
|
242
|
+
jobRunId: z.string().optional()
|
|
243
|
+
})
|
|
244
|
+
.strict();
|
|
245
|
+
const runtimeErrorsSummaryInput = listInput
|
|
246
|
+
.extend({
|
|
247
|
+
...timeRangeInput,
|
|
248
|
+
source: z.enum(["server", "client"]).optional(),
|
|
249
|
+
route: z.string().optional(),
|
|
250
|
+
release: z.string().optional(),
|
|
251
|
+
kind: z.string().optional()
|
|
252
|
+
})
|
|
253
|
+
.strict();
|
|
254
|
+
const runtimeErrorDetailInput = sampleListInput
|
|
255
|
+
.extend({
|
|
256
|
+
fingerprint: z.string().min(1),
|
|
257
|
+
source: z.enum(["server", "client"]).optional(),
|
|
258
|
+
route: z.string().optional(),
|
|
259
|
+
release: z.string().optional()
|
|
260
|
+
})
|
|
261
|
+
.strict();
|
|
262
|
+
const supportRequestsQueryInput = listInput
|
|
263
|
+
.extend({
|
|
264
|
+
...timeRangeInput,
|
|
265
|
+
status: z.string().optional(),
|
|
266
|
+
sourceKind: z.string().optional(),
|
|
267
|
+
agentName: z.string().optional(),
|
|
268
|
+
conversationId: z.string().optional(),
|
|
269
|
+
runId: z.string().optional(),
|
|
270
|
+
userId: z.string().optional(),
|
|
271
|
+
organizationId: z.string().optional(),
|
|
272
|
+
surface: z.string().optional(),
|
|
273
|
+
route: z.string().optional(),
|
|
274
|
+
search: z.string().optional()
|
|
275
|
+
})
|
|
276
|
+
.strict();
|
|
277
|
+
const supportRequestDetailInput = z.object({ id: z.string().min(1) }).strict();
|
|
278
|
+
const agentReadScopes = ["operator:agent:read"];
|
|
279
|
+
const sensitiveKeyPattern = /password|token|secret|accessToken|refreshToken|idToken|codeHash|tokenHash|verification|publicKey|credentialID|credentialId|authorization/i;
|
|
280
|
+
const maxStringLength = 2000;
|
|
281
|
+
function asRecord(value) {
|
|
282
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
283
|
+
}
|
|
284
|
+
function parseJson(value) {
|
|
285
|
+
if (typeof value !== "string" || !value.trim())
|
|
286
|
+
return value;
|
|
287
|
+
try {
|
|
288
|
+
return JSON.parse(value);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function parseRoles(value) {
|
|
295
|
+
if (Array.isArray(value))
|
|
296
|
+
return normalizeRoles(value.map(String));
|
|
297
|
+
if (typeof value === "string" && value.trim()) {
|
|
298
|
+
const parsed = parseJson(value);
|
|
299
|
+
if (Array.isArray(parsed))
|
|
300
|
+
return normalizeRoles(parsed.map(String));
|
|
301
|
+
return normalizeRoles(value.split(",").map((role) => role.trim()).filter(Boolean));
|
|
302
|
+
}
|
|
303
|
+
return ["user"];
|
|
304
|
+
}
|
|
305
|
+
function truncateString(value) {
|
|
306
|
+
if (value.length <= maxStringLength)
|
|
307
|
+
return { value, truncated: false };
|
|
308
|
+
return { value: `${value.slice(0, maxStringLength)}...[truncated]`, truncated: true };
|
|
309
|
+
}
|
|
310
|
+
function redactValue(key, value, depth = 0) {
|
|
311
|
+
if (sensitiveKeyPattern.test(key))
|
|
312
|
+
return { value: "[redacted]", redacted: true, truncated: false };
|
|
313
|
+
if (value === null || value === undefined || typeof value === "number" || typeof value === "boolean") {
|
|
314
|
+
return { value, redacted: false, truncated: false };
|
|
315
|
+
}
|
|
316
|
+
if (typeof value === "string") {
|
|
317
|
+
const truncated = truncateString(value);
|
|
318
|
+
return { value: truncated.value, redacted: false, truncated: truncated.truncated };
|
|
319
|
+
}
|
|
320
|
+
if (depth > 5)
|
|
321
|
+
return { value: "[truncated]", redacted: false, truncated: true };
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
let truncated = value.length > 50;
|
|
324
|
+
const items = value.slice(0, 50).map((item, index) => {
|
|
325
|
+
const result = redactValue(String(index), item, depth + 1);
|
|
326
|
+
truncated ||= result.truncated;
|
|
327
|
+
return result.value;
|
|
328
|
+
});
|
|
329
|
+
return { value: items, redacted: false, truncated };
|
|
330
|
+
}
|
|
331
|
+
const output = {};
|
|
332
|
+
let redacted = false;
|
|
333
|
+
let truncated = false;
|
|
334
|
+
for (const [childKey, childValue] of Object.entries(asRecord(value))) {
|
|
335
|
+
const result = redactValue(childKey, childValue, depth + 1);
|
|
336
|
+
output[childKey] = result.value;
|
|
337
|
+
redacted ||= result.redacted;
|
|
338
|
+
truncated ||= result.truncated;
|
|
339
|
+
}
|
|
340
|
+
return { value: output, redacted, truncated };
|
|
341
|
+
}
|
|
342
|
+
function redactRow(row) {
|
|
343
|
+
const output = {};
|
|
344
|
+
const redactedColumns = [];
|
|
345
|
+
const truncatedColumns = [];
|
|
346
|
+
let truncated = false;
|
|
347
|
+
for (const [key, value] of Object.entries(row)) {
|
|
348
|
+
const result = redactValue(key, parseJson(value));
|
|
349
|
+
output[key] = result.value;
|
|
350
|
+
if (result.redacted)
|
|
351
|
+
redactedColumns.push(key);
|
|
352
|
+
if (result.truncated)
|
|
353
|
+
truncatedColumns.push(key);
|
|
354
|
+
truncated ||= result.truncated;
|
|
355
|
+
}
|
|
356
|
+
return { row: output, redactedColumns, truncatedColumns, truncated };
|
|
357
|
+
}
|
|
358
|
+
function operatorBatchSummary(batch) {
|
|
359
|
+
const settled = batch.counts.total - batch.counts.pending;
|
|
360
|
+
return redactRow({
|
|
361
|
+
...batch,
|
|
362
|
+
progress: {
|
|
363
|
+
settled,
|
|
364
|
+
total: batch.counts.total,
|
|
365
|
+
percent: batch.counts.total ? Math.round((settled / batch.counts.total) * 100) : batch.status === "completed" ? 100 : 0
|
|
366
|
+
}
|
|
367
|
+
}).row;
|
|
368
|
+
}
|
|
369
|
+
function redactRows(rows) {
|
|
370
|
+
const redacted = rows.map(redactRow);
|
|
371
|
+
return {
|
|
372
|
+
rows: redacted.map((entry) => entry.row),
|
|
373
|
+
redactedColumns: [...new Set(redacted.flatMap((entry) => entry.redactedColumns))],
|
|
374
|
+
truncatedColumns: [...new Set(redacted.flatMap((entry) => entry.truncatedColumns))],
|
|
375
|
+
truncated: redacted.some((entry) => entry.truncated)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function normalizeSelectedAppRows(table, selectedColumns, rows) {
|
|
379
|
+
return rows.map((row) => {
|
|
380
|
+
const output = {};
|
|
381
|
+
for (const columnName of selectedColumns) {
|
|
382
|
+
const column = table.columns[columnName];
|
|
383
|
+
const value = row[columnName];
|
|
384
|
+
if (column?.kind === "secret") {
|
|
385
|
+
output[columnName] = redactSecretValue(value);
|
|
386
|
+
}
|
|
387
|
+
else if (column?.kind === "boolean" && value !== null && value !== undefined) {
|
|
388
|
+
output[columnName] = Boolean(value);
|
|
389
|
+
}
|
|
390
|
+
else if (column?.kind === "json") {
|
|
391
|
+
output[columnName] = parseJson(value);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
output[columnName] = value;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return output;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function operatorText(summary) {
|
|
401
|
+
return [{ type: "text", text: summary }];
|
|
402
|
+
}
|
|
403
|
+
function toolResult(summary, structuredContent, isError = false) {
|
|
404
|
+
return {
|
|
405
|
+
content: operatorText(summary),
|
|
406
|
+
structuredContent: {
|
|
407
|
+
generatedAt: nowIso(),
|
|
408
|
+
...structuredContent
|
|
409
|
+
},
|
|
410
|
+
isError
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function unavailableResult(provider, scope) {
|
|
414
|
+
return toolResult(`${provider} evidence is not installed for this Raybase app.`, {
|
|
415
|
+
unavailable: true,
|
|
416
|
+
provider,
|
|
417
|
+
requiredScope: scope,
|
|
418
|
+
appliedFilters: {},
|
|
419
|
+
rowCount: 0,
|
|
420
|
+
returned: 0,
|
|
421
|
+
truncated: false,
|
|
422
|
+
suggestedNextInspection: "Install the relevant evidence provider and register it with Operator MCP."
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function normalizeDateInput(value, label) {
|
|
426
|
+
if (!value)
|
|
427
|
+
return null;
|
|
428
|
+
const time = new Date(value).getTime();
|
|
429
|
+
if (!Number.isFinite(time))
|
|
430
|
+
throw new Error(`${label} must be an ISO timestamp`);
|
|
431
|
+
return new Date(time).toISOString();
|
|
432
|
+
}
|
|
433
|
+
function addTimeRange(where, params, column, input) {
|
|
434
|
+
const applied = {};
|
|
435
|
+
const from = normalizeDateInput(input.from, "from");
|
|
436
|
+
const to = normalizeDateInput(input.to, "to");
|
|
437
|
+
if (from) {
|
|
438
|
+
where.push(`${column} >= ?`);
|
|
439
|
+
params.push(from);
|
|
440
|
+
applied.from = from;
|
|
441
|
+
}
|
|
442
|
+
if (to) {
|
|
443
|
+
where.push(`${column} <= ?`);
|
|
444
|
+
params.push(to);
|
|
445
|
+
applied.to = to;
|
|
446
|
+
}
|
|
447
|
+
return applied;
|
|
448
|
+
}
|
|
449
|
+
function sqlString(value) {
|
|
450
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
451
|
+
}
|
|
452
|
+
async function tableExists(db, tableName) {
|
|
453
|
+
const row = await db.first(`SELECT "name" FROM "sqlite_master" WHERE "type" = 'table' AND "name" = ?`, [tableName]);
|
|
454
|
+
return Boolean(row);
|
|
455
|
+
}
|
|
456
|
+
function shortExcerpt(value, maxChars = 320) {
|
|
457
|
+
if (typeof value !== "string" || !value.trim())
|
|
458
|
+
return { value: null, truncated: false, redacted: false };
|
|
459
|
+
let redacted = false;
|
|
460
|
+
const safe = redactRow({ excerpt: value });
|
|
461
|
+
let text = String(safe.row.excerpt ?? "");
|
|
462
|
+
text = text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, () => {
|
|
463
|
+
redacted = true;
|
|
464
|
+
return "Bearer [redacted]";
|
|
465
|
+
});
|
|
466
|
+
text = text.replace(/\b(password|passwd|token|secret|authorization|auth|cookie|code|otp|api[-_]?key)=([^&\s]+)/gi, (_match, key) => {
|
|
467
|
+
redacted = true;
|
|
468
|
+
return `${key}=[redacted]`;
|
|
469
|
+
});
|
|
470
|
+
const truncated = text.length > maxChars || safe.truncated;
|
|
471
|
+
return {
|
|
472
|
+
value: text.length > maxChars ? `${text.slice(0, maxChars)}...[truncated]` : text,
|
|
473
|
+
truncated,
|
|
474
|
+
redacted: redacted || safe.redactedColumns.includes("excerpt")
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function supportRequestListRow(row) {
|
|
478
|
+
const bodyExcerpt = shortExcerpt(row.body);
|
|
479
|
+
return {
|
|
480
|
+
id: row.id,
|
|
481
|
+
status: row.status,
|
|
482
|
+
sourceKind: row.sourceKind,
|
|
483
|
+
sourceId: row.sourceId ?? null,
|
|
484
|
+
subject: row.subject,
|
|
485
|
+
category: row.category ?? null,
|
|
486
|
+
confidence: row.confidence ?? null,
|
|
487
|
+
principalKind: row.principalKind ?? null,
|
|
488
|
+
userId: row.userId ?? null,
|
|
489
|
+
organizationId: row.organizationId ?? null,
|
|
490
|
+
publicIdentity: row.publicIdentity ?? null,
|
|
491
|
+
surface: row.surface ?? null,
|
|
492
|
+
route: row.route ?? null,
|
|
493
|
+
agentName: row.agentName ?? null,
|
|
494
|
+
conversationId: row.conversationId ?? null,
|
|
495
|
+
runId: row.runId ?? null,
|
|
496
|
+
requestId: row.requestId ?? null,
|
|
497
|
+
toolCallId: row.toolCallId ?? null,
|
|
498
|
+
contactEmail: row.contactEmail ?? null,
|
|
499
|
+
bodyExcerpt: bodyExcerpt.value,
|
|
500
|
+
bodyExcerptTruncated: bodyExcerpt.truncated,
|
|
501
|
+
createdAt: row.createdAt,
|
|
502
|
+
updatedAt: row.updatedAt
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function supportRequestDetailRow(row, auditRows) {
|
|
506
|
+
const body = shortExcerpt(row.body, 4000);
|
|
507
|
+
const transcriptSummary = shortExcerpt(row.transcriptSummary, 2000);
|
|
508
|
+
const transcriptEvidence = shortExcerpt(row.transcriptEvidence, 4000);
|
|
509
|
+
const escalationReason = shortExcerpt(row.escalationReason, 2000);
|
|
510
|
+
const suggestedHumanReply = shortExcerpt(row.suggestedHumanReply, 4000);
|
|
511
|
+
const metadata = redactRow({
|
|
512
|
+
docsConsultedJson: parseJson(row.docsConsultedJson),
|
|
513
|
+
sourcesJson: parseJson(row.sourcesJson),
|
|
514
|
+
metadataJson: parseJson(row.metadataJson)
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
id: row.id,
|
|
518
|
+
status: row.status,
|
|
519
|
+
sourceKind: row.sourceKind,
|
|
520
|
+
sourceId: row.sourceId ?? null,
|
|
521
|
+
subject: row.subject,
|
|
522
|
+
category: row.category ?? null,
|
|
523
|
+
confidence: row.confidence ?? null,
|
|
524
|
+
principalKind: row.principalKind ?? null,
|
|
525
|
+
userId: row.userId ?? null,
|
|
526
|
+
organizationId: row.organizationId ?? null,
|
|
527
|
+
publicIdentity: row.publicIdentity ?? null,
|
|
528
|
+
surface: row.surface ?? null,
|
|
529
|
+
route: row.route ?? null,
|
|
530
|
+
agentName: row.agentName ?? null,
|
|
531
|
+
agentInstanceId: row.agentInstanceId ?? null,
|
|
532
|
+
conversationId: row.conversationId ?? null,
|
|
533
|
+
runId: row.runId ?? null,
|
|
534
|
+
requestId: row.requestId ?? null,
|
|
535
|
+
toolCallId: row.toolCallId ?? null,
|
|
536
|
+
createdMessageId: row.createdMessageId ?? null,
|
|
537
|
+
contactEmail: row.contactEmail ?? null,
|
|
538
|
+
body: body.value,
|
|
539
|
+
transcriptSummary: transcriptSummary.value,
|
|
540
|
+
transcriptEvidence: transcriptEvidence.value,
|
|
541
|
+
escalationReason: escalationReason.value,
|
|
542
|
+
suggestedHumanReply: suggestedHumanReply.value,
|
|
543
|
+
docsConsulted: metadata.row.docsConsultedJson ?? null,
|
|
544
|
+
sources: metadata.row.sourcesJson ?? null,
|
|
545
|
+
metadata: metadata.row.metadataJson ?? null,
|
|
546
|
+
redaction: {
|
|
547
|
+
truncated: body.truncated || transcriptSummary.truncated || transcriptEvidence.truncated || escalationReason.truncated || suggestedHumanReply.truncated || metadata.truncated,
|
|
548
|
+
redactedColumns: metadata.redactedColumns,
|
|
549
|
+
truncatedColumns: [
|
|
550
|
+
body.truncated ? "body" : null,
|
|
551
|
+
transcriptSummary.truncated ? "transcriptSummary" : null,
|
|
552
|
+
transcriptEvidence.truncated ? "transcriptEvidence" : null,
|
|
553
|
+
escalationReason.truncated ? "escalationReason" : null,
|
|
554
|
+
suggestedHumanReply.truncated ? "suggestedHumanReply" : null,
|
|
555
|
+
...metadata.truncatedColumns
|
|
556
|
+
].filter(Boolean)
|
|
557
|
+
},
|
|
558
|
+
adjacentEvidence: {
|
|
559
|
+
conversationId: row.conversationId ?? null,
|
|
560
|
+
runId: row.runId ?? null,
|
|
561
|
+
sourceKind: row.sourceKind,
|
|
562
|
+
route: row.route ?? null,
|
|
563
|
+
surface: row.surface ?? null,
|
|
564
|
+
recentAudit: auditRows
|
|
565
|
+
},
|
|
566
|
+
createdAt: row.createdAt,
|
|
567
|
+
updatedAt: row.updatedAt
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async function safeCount(db, tableName) {
|
|
571
|
+
try {
|
|
572
|
+
const row = await db.first(`SELECT COUNT(*) as "count" FROM ${q(tableName)}`);
|
|
573
|
+
return Number(row?.count ?? 0);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function singleColumnUniqueIndexes(db, tableName) {
|
|
580
|
+
const indexes = await db.all(`PRAGMA index_list(${sqlString(tableName)})`);
|
|
581
|
+
const uniques = new Map();
|
|
582
|
+
for (const index of indexes) {
|
|
583
|
+
if (!Boolean(index.unique) || String(index.origin ?? "") === "pk")
|
|
584
|
+
continue;
|
|
585
|
+
const indexName = String(index.name);
|
|
586
|
+
const indexedColumns = await db.all(`PRAGMA index_info(${sqlString(indexName)})`);
|
|
587
|
+
if (indexedColumns.length === 1)
|
|
588
|
+
uniques.set(String(indexedColumns[0].name), indexName);
|
|
589
|
+
}
|
|
590
|
+
return uniques;
|
|
591
|
+
}
|
|
592
|
+
async function databaseSchemaSummary(db, schema, systemTableNames, options = {}) {
|
|
593
|
+
const appTableNames = new Set(Object.values(schema.tables).map((table) => table.name));
|
|
594
|
+
const rows = (await db.all(`PRAGMA table_list`))
|
|
595
|
+
.filter((row) => row.schema === "main" && row.type === "table")
|
|
596
|
+
.filter((row) => !row.name.startsWith("sqlite_") && !row.name.startsWith("_cf_") && row.name !== "_do_migrations")
|
|
597
|
+
.filter((row) => !isRaybaseBackupOperationalTableName(row.name))
|
|
598
|
+
.filter((row) => options.appOnly ? appTableNames.has(row.name) : true)
|
|
599
|
+
.filter((row) => options.systemOnly ? systemTableNames.has(row.name) : true)
|
|
600
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
601
|
+
const tables = [];
|
|
602
|
+
for (const row of rows) {
|
|
603
|
+
const name = String(row.name);
|
|
604
|
+
const columns = await db.all(`PRAGMA table_info(${sqlString(name)})`);
|
|
605
|
+
const foreignKeys = await db.all(`PRAGMA foreign_key_list(${sqlString(name)})`);
|
|
606
|
+
const indexes = await db.all(`PRAGMA index_list(${sqlString(name)})`);
|
|
607
|
+
const uniqueColumns = await singleColumnUniqueIndexes(db, name);
|
|
608
|
+
tables.push({
|
|
609
|
+
name,
|
|
610
|
+
appDefined: appTableNames.has(name),
|
|
611
|
+
system: systemTableNames.has(name),
|
|
612
|
+
rowCount: await safeCount(db, name),
|
|
613
|
+
columns: columns.map((column) => {
|
|
614
|
+
const columnName = String(column.name);
|
|
615
|
+
const reference = foreignKeys.find((foreignKey) => String(foreignKey.from) === columnName);
|
|
616
|
+
return {
|
|
617
|
+
name: columnName,
|
|
618
|
+
type: String(column.type || "TEXT").toUpperCase(),
|
|
619
|
+
notNull: Boolean(column.notnull),
|
|
620
|
+
primaryKey: Boolean(column.pk),
|
|
621
|
+
unique: uniqueColumns.has(columnName),
|
|
622
|
+
defaultValue: column.dflt_value ?? null,
|
|
623
|
+
references: reference ? { table: String(reference.table), column: String(reference.to || "id") } : null
|
|
624
|
+
};
|
|
625
|
+
}),
|
|
626
|
+
indexes: indexes.map((index) => ({
|
|
627
|
+
name: String(index.name),
|
|
628
|
+
unique: Boolean(index.unique),
|
|
629
|
+
origin: String(index.origin ?? "")
|
|
630
|
+
}))
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return tables;
|
|
634
|
+
}
|
|
635
|
+
function safeAgentConversation(row, transcriptCount) {
|
|
636
|
+
const metadata = parseJson(row.metadataJson);
|
|
637
|
+
const safe = redactRow({ ...row, metadataJson: undefined, metadata });
|
|
638
|
+
return {
|
|
639
|
+
...safe.row,
|
|
640
|
+
principal: {
|
|
641
|
+
kind: row.principalKind,
|
|
642
|
+
userId: row.userId ?? null,
|
|
643
|
+
publicScopeHash: row.publicScopeHash ?? null,
|
|
644
|
+
organizationId: row.organizationId ?? null
|
|
645
|
+
},
|
|
646
|
+
archive: {
|
|
647
|
+
status: row.status === "archived" ? "archived" : row.status === "deleted" ? "deleted" : "hot",
|
|
648
|
+
deletedAt: row.deletedAt ?? null
|
|
649
|
+
},
|
|
650
|
+
transcriptCount: transcriptCount ?? null
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function agentRunListRow(row) {
|
|
654
|
+
const errorExcerpt = shortExcerpt(row.error, 2000);
|
|
655
|
+
const startedMs = new Date(String(row.startedAt ?? "")).getTime();
|
|
656
|
+
const finishedMs = typeof row.finishedAt === "string" && row.finishedAt ? new Date(row.finishedAt).getTime() : Number.NaN;
|
|
657
|
+
return {
|
|
658
|
+
runId: row.runId,
|
|
659
|
+
conversationId: row.conversationId,
|
|
660
|
+
agentName: row.agentName,
|
|
661
|
+
requestId: row.requestId,
|
|
662
|
+
triggerType: row.triggerType,
|
|
663
|
+
inputMessageId: row.inputMessageId ?? null,
|
|
664
|
+
status: row.status,
|
|
665
|
+
startedAt: row.startedAt,
|
|
666
|
+
finishedAt: row.finishedAt ?? null,
|
|
667
|
+
durationMs: Number.isFinite(startedMs) && Number.isFinite(finishedMs) ? Math.max(0, finishedMs - startedMs) : null,
|
|
668
|
+
error: errorExcerpt.value,
|
|
669
|
+
errorTruncated: errorExcerpt.truncated,
|
|
670
|
+
usageEventId: row.usageEventId ?? null,
|
|
671
|
+
auditEventId: row.auditEventId ?? null,
|
|
672
|
+
metadata: redactValue("metadata", parseJson(row.metadataJson)).value
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function aiUsageListRow(row) {
|
|
676
|
+
const errorExcerpt = shortExcerpt(row.error, 2000);
|
|
677
|
+
const metadata = redactValue("metadata", parseJson(row.metadata));
|
|
678
|
+
return {
|
|
679
|
+
id: row.id,
|
|
680
|
+
principalKind: row.principalKind ?? null,
|
|
681
|
+
userId: row.userId ?? null,
|
|
682
|
+
publicScopeHash: row.publicScopeHash ?? null,
|
|
683
|
+
serviceName: row.serviceName ?? null,
|
|
684
|
+
agent: row.agent ?? null,
|
|
685
|
+
conversationId: row.conversationId ?? null,
|
|
686
|
+
model: row.model,
|
|
687
|
+
requestKind: row.requestKind,
|
|
688
|
+
gatewayId: row.gatewayId ?? null,
|
|
689
|
+
gatewayLogId: row.gatewayLogId ?? null,
|
|
690
|
+
inputChars: Number(row.inputChars ?? 0),
|
|
691
|
+
floorChars: row.floorChars ?? null,
|
|
692
|
+
tailChars: row.tailChars ?? null,
|
|
693
|
+
outputChars: Number(row.outputChars ?? 0),
|
|
694
|
+
estimatedInputTokens: Number(row.estimatedInputTokens ?? 0),
|
|
695
|
+
estimatedOutputTokens: Number(row.estimatedOutputTokens ?? 0),
|
|
696
|
+
estimatedNeurons: row.estimatedNeurons ?? null,
|
|
697
|
+
modelParams: parseJson(row.modelParams),
|
|
698
|
+
metadata: metadata.value,
|
|
699
|
+
metadataTruncated: metadata.truncated,
|
|
700
|
+
status: row.status,
|
|
701
|
+
error: errorExcerpt.value,
|
|
702
|
+
errorTruncated: errorExcerpt.truncated,
|
|
703
|
+
durationMs: Number(row.durationMs ?? 0),
|
|
704
|
+
createdAt: row.createdAt
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function agentNamespace(env) {
|
|
708
|
+
const namespace = asRecord(env).RAYBASE_AI_CHAT_AGENT;
|
|
709
|
+
return namespace && typeof namespace === "object" && "idFromName" in namespace && "get" in namespace ? namespace : null;
|
|
710
|
+
}
|
|
711
|
+
async function readActiveAgentTranscript(ctx, instance, input) {
|
|
712
|
+
if (instance.status !== "active") {
|
|
713
|
+
return {
|
|
714
|
+
messages: [],
|
|
715
|
+
toolEvents: [],
|
|
716
|
+
sources: [],
|
|
717
|
+
citations: [],
|
|
718
|
+
supportMarkers: [],
|
|
719
|
+
rowCount: 0,
|
|
720
|
+
returned: 0,
|
|
721
|
+
truncated: false,
|
|
722
|
+
redacted: false,
|
|
723
|
+
appliedLimits: input
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const namespace = agentNamespace(ctx.env);
|
|
727
|
+
if (!namespace)
|
|
728
|
+
return { unavailable: true, error: "RAYBASE_AI_CHAT_AGENT Durable Object binding is missing." };
|
|
729
|
+
const token = await signAgentToken(authSecret(ctx.env), agentTokenClaims({
|
|
730
|
+
purpose: "agent-internal",
|
|
731
|
+
agent: { name: instance.agentName },
|
|
732
|
+
instance,
|
|
733
|
+
ttlSeconds: 60
|
|
734
|
+
}));
|
|
735
|
+
const response = await namespace.get(namespace.idFromName(raybaseAgentConversationName(ctx.env, {
|
|
736
|
+
instanceKey: instance.instanceKey,
|
|
737
|
+
agentName: instance.agentName,
|
|
738
|
+
conversationId: instance.conversationId,
|
|
739
|
+
// undefined keeps the environment-derived scope; a layered runtime's
|
|
740
|
+
// operator surface runs on an unscoped env and must supply it per call.
|
|
741
|
+
scope: ctx.agentConversationScope
|
|
742
|
+
}))).fetch(new Request(`https://raybase.local${raybaseAgentInternalReadPath}`, {
|
|
743
|
+
method: "POST",
|
|
744
|
+
headers: {
|
|
745
|
+
"content-type": "application/json",
|
|
746
|
+
"x-raybase-agent-context": token
|
|
747
|
+
},
|
|
748
|
+
body: JSON.stringify(input)
|
|
749
|
+
}));
|
|
750
|
+
if (!response.ok)
|
|
751
|
+
return { unavailable: true, error: await response.text() };
|
|
752
|
+
return await response.json();
|
|
753
|
+
}
|
|
754
|
+
function systemTableNames(options) {
|
|
755
|
+
return new Set([...controlTableNames, "_oauth_clients", "_oauth_authorization_codes", "_oauth_access_tokens", "_oauth_refresh_tokens", "_mcp_grants", "_mcp_grant_events", "_mcp_pending_authorizations", "_mcp_approval_tickets", ...(options.systemTables ?? []).map((table) => table.name)]);
|
|
756
|
+
}
|
|
757
|
+
function envRecord(env) {
|
|
758
|
+
return asRecord(env);
|
|
759
|
+
}
|
|
760
|
+
function operatorDb(ctx, options) {
|
|
761
|
+
return createRaybaseDb(ctx.env, options.schema, options.appMigrations);
|
|
762
|
+
}
|
|
763
|
+
function operatorMutationDb(ctx, options) {
|
|
764
|
+
const realtimeEnabled = options.realtime?.config.enabled === true;
|
|
765
|
+
const search = options.search;
|
|
766
|
+
const db = createRaybaseDb(ctx.env, options.schema, options.appMigrations, {
|
|
767
|
+
...(realtimeEnabled ? { realtime: options.realtime?.config } : {}),
|
|
768
|
+
...(search
|
|
769
|
+
? {
|
|
770
|
+
afterMutation: async (change) => {
|
|
771
|
+
const searchDb = createRaybaseDb(ctx.env, options.schema, options.appMigrations);
|
|
772
|
+
await updateSearchForDataChange({
|
|
773
|
+
schema: options.schema,
|
|
774
|
+
control: searchDb.control,
|
|
775
|
+
search,
|
|
776
|
+
change
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
: {})
|
|
781
|
+
});
|
|
782
|
+
return db.for({
|
|
783
|
+
user: ctx.user,
|
|
784
|
+
adminOverride: true,
|
|
785
|
+
source: { surface: "operator-mcp", requestId: ctx.request.headers.get("cf-ray") ?? undefined },
|
|
786
|
+
audit: (entry) => audit(db.control, ctx.user.id, entry.action, entry.target.type, entry.target.id ?? undefined, { ...redactRow(asRecord(entry.metadata)).row, ...ctx.mcp.auditMetadata }),
|
|
787
|
+
notify: realtimeEnabled
|
|
788
|
+
? async (change) => {
|
|
789
|
+
if (!("seq" in change))
|
|
790
|
+
return;
|
|
791
|
+
await dataStub(ctx.env).notifyDataChange({
|
|
792
|
+
change: change,
|
|
793
|
+
config: options.realtime.config,
|
|
794
|
+
schema: options.realtime.schema
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
: undefined,
|
|
798
|
+
realtime: options.realtime?.config
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
function mutationAuditMetadata(ctx, metadata = {}) {
|
|
802
|
+
return { ...metadata, ...ctx.mcp.auditMetadata };
|
|
803
|
+
}
|
|
804
|
+
function disableUserDisabledReason(actor, target) {
|
|
805
|
+
if (!isHostUserId(target.id) && normalizeStoredAdminRole(target.operatorRole) === "owner")
|
|
806
|
+
return "Owner accounts cannot be disabled.";
|
|
807
|
+
if (actor.id === String(target.id))
|
|
808
|
+
return "You cannot disable your own account.";
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
async function resolveUserTarget(control, input) {
|
|
812
|
+
const row = input.id
|
|
813
|
+
? await control.first(`SELECT "id", "email", "name", "emailVerified", "operatorRole", "appRoles", "banned", "banReason", "createdAt", "updatedAt" FROM "_auth_users" WHERE "id" = ?`, [input.id])
|
|
814
|
+
: await control.first(`SELECT "id", "email", "name", "emailVerified", "operatorRole", "appRoles", "banned", "banReason", "createdAt", "updatedAt" FROM "_auth_users" WHERE lower("email") = lower(?)`, [input.email ?? ""]);
|
|
815
|
+
if (!row)
|
|
816
|
+
throw new Error("User not found");
|
|
817
|
+
if (input.email && String(row.email).toLowerCase() !== input.email.toLowerCase())
|
|
818
|
+
throw new Error("User id and email do not refer to the same account");
|
|
819
|
+
return row;
|
|
820
|
+
}
|
|
821
|
+
async function activeSessionCount(control, userId) {
|
|
822
|
+
const row = await control.first(`SELECT COUNT(*) as "count" FROM "_auth_sessions" WHERE "userId" = ?`, [userId]);
|
|
823
|
+
return Number(row?.count ?? 0);
|
|
824
|
+
}
|
|
825
|
+
function operatorTool(input) {
|
|
826
|
+
return {
|
|
827
|
+
name: input.name,
|
|
828
|
+
description: input.description,
|
|
829
|
+
inputSchema: input.inputSchema,
|
|
830
|
+
annotations: input.annotations ?? { readOnlyHint: true },
|
|
831
|
+
scopes: [input.scope],
|
|
832
|
+
handler: input.handler
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
const agentSkillSourceInput = z.object({ kind: z.enum(["manifest-url", "website", "direct-url", "github", "upload", "upload-inline"]) }).passthrough();
|
|
836
|
+
const agentSkillIdentityInput = z.object({ agentName: z.string().min(1), name: z.string().min(1) }).strict();
|
|
837
|
+
function createAgentSkillOperatorTools(options) {
|
|
838
|
+
const db = async (ctx) => { await ensureManifestTables(ctx.control, raybaseAgentSkillSystemTables); return ctx.control; };
|
|
839
|
+
const source = (value) => agentSkillSourceInput.parse(value);
|
|
840
|
+
const noPackages = (value) => ({ ...value, candidates: value.candidates.map(({ package: _package, ...candidate }) => candidate) });
|
|
841
|
+
return [
|
|
842
|
+
operatorTool({
|
|
843
|
+
name: "agent_skills_list", description: "List database-backed Agent Skills with source, mode, enabled state, revision, sync status, and provenance summary.", scope: "operator:agent-skills:read",
|
|
844
|
+
inputSchema: z.object({ agentName: z.string().optional() }).strict(), handler: async (args, ctx) => { const rows = await listAgentSkills(await db(ctx), args.agentName); return toolResult(`Found ${rows.length} Agent Skill row(s).`, { skills: rows.map(skillOperatorProjection), rowCount: rows.length, returned: rows.length, truncated: false }); }
|
|
845
|
+
}),
|
|
846
|
+
operatorTool({
|
|
847
|
+
name: "agent_skills_read", description: "Read a database-backed Agent Skill, its immutable provenance, revision, supporting-file list, or one supporting file.", scope: "operator:agent-skills:read",
|
|
848
|
+
inputSchema: agentSkillIdentityInput.extend({ path: z.string().optional() }).strict(), handler: async (args, ctx) => { const control = await db(ctx); const row = await findAgentSkill(control, args.agentName, args.name); if (!row)
|
|
849
|
+
throw new Error("Agent Skill not found"); if (args.path) {
|
|
850
|
+
const file = await readAgentSkillFile(control, row.id, args.path);
|
|
851
|
+
if (!file)
|
|
852
|
+
throw new Error("Agent Skill file not found");
|
|
853
|
+
return toolResult(`Read ${args.name}/${args.path}.`, { skill: skillOperatorProjection(row), file, rowCount: 1, returned: 1, truncated: false });
|
|
854
|
+
} const files = await listAgentSkillFiles(control, row.id); return toolResult(`Read Agent Skill ${args.name}.`, { skill: { ...row, files }, rowCount: 1, returned: 1, truncated: false }); }
|
|
855
|
+
}),
|
|
856
|
+
operatorTool({
|
|
857
|
+
name: "agent_skills_sources_list", description: "List managed Agent Skill sources including sync state and artifact-origin policy.", scope: "operator:agent-skills:read",
|
|
858
|
+
inputSchema: z.object({ agentName: z.string().optional() }).strict(), handler: async (args, ctx) => { const control = await db(ctx); const rows = await control.all(`SELECT * FROM "_agent_skill_sources"${args.agentName ? ` WHERE "agentName"=?` : ""} ORDER BY "agentName","createdAt"`, args.agentName ? [args.agentName] : []); return toolResult(`Found ${rows.length} managed Agent Skill source(s).`, { sources: rows, rowCount: rows.length, returned: rows.length, truncated: false }); }
|
|
859
|
+
}),
|
|
860
|
+
operatorTool({
|
|
861
|
+
name: "agent_skills_discover", description: "Perform state-free, network-active Agent Skill discovery. website uses RFC 8615 origin-root discovery; GitHub root skills.json is a Raybase repository convention using the unchanged discovery v0.2.0 schema.", scope: "operator:agent-skills:read",
|
|
862
|
+
inputSchema: z.object({ agentName: z.string().min(1), source: agentSkillSourceInput, requestedNames: z.array(z.string()).optional() }).strict(), handler: async (args, ctx) => { const result = await discoverAgentSkills({ agentName: args.agentName, source: source(args.source), requestedNames: args.requestedNames, control: await db(ctx), fetcher: fetch }); return toolResult(`Skill discovery returned ${result.status}.`, { discovery: noPackages(result), rowCount: result.totalCandidates, returned: result.candidates.length, truncated: result.truncated }, result.status === "invalid"); }
|
|
863
|
+
}),
|
|
864
|
+
operatorTool({
|
|
865
|
+
name: "agent_skills_apply", description: "Atomically apply a prior digest-bound Agent Skill discovery selection.", scope: "operator:agent-skills:write",
|
|
866
|
+
inputSchema: z.object({ agentName: z.string().min(1), source: agentSkillSourceInput, expectedSourceDigest: z.string().min(1), requestedNames: z.array(z.string()).optional(), conflictPolicy: z.enum(["fail", "replace"]).optional(), expectedReplacedRevisions: z.record(z.string(), z.number().int()).optional(), mode: z.enum(["include", "available"]).optional(), enable: z.boolean().optional() }).strict(), annotations: { idempotentHint: true },
|
|
867
|
+
handler: async (args, ctx) => { const result = await applyAgentSkills({ agentName: args.agentName, source: source(args.source), expectedSourceDigest: args.expectedSourceDigest, requestedNames: args.requestedNames, conflictPolicy: args.conflictPolicy, expectedReplacedRevisions: args.expectedReplacedRevisions, mode: args.mode, enable: args.enable, control: await db(ctx), actorKind: "operator", actor: ctx.user.id, ownerKind: "operator", fetcher: fetch }); await audit(ctx.control, ctx.user.id, "agent.skills.apply", "_agent_skills", undefined, mutationAuditMetadata(ctx, { agentName: args.agentName, sourceKind: args.source.kind, sourceDigest: args.expectedSourceDigest, applied: result.applied.map((item) => item.name) })); return toolResult(result.ok ? `Applied ${result.applied.length} Agent Skill(s).` : `Agent Skill apply failed: ${result.error?.message}`, { ...result, discovery: noPackages(result.discovery), rowCount: result.applied.length, returned: result.applied.length, truncated: false }, !result.ok); }
|
|
868
|
+
}),
|
|
869
|
+
operatorTool({
|
|
870
|
+
name: "agent_skills_upload_create", description: "Create an explicit write-scoped short-lived Agent Skill upload session from a base64 payload (up to 1 MiB over MCP).", scope: "operator:agent-skills:write",
|
|
871
|
+
inputSchema: z.object({ agentName: z.string().min(1), filename: z.string().min(1), contentBase64: z.string().min(1) }).strict(), annotations: { idempotentHint: false }, handler: async (args, ctx) => { const bytes = base64Bytes(args.contentBase64); if (bytes.byteLength > 1024 * 1024)
|
|
872
|
+
throw new Error("MCP inline upload exceeds 1 MiB"); const result = await createAgentSkillUpload(await db(ctx), { agentName: args.agentName, filename: args.filename, bytes, createdByUserId: ctx.user.id }); await audit(ctx.control, ctx.user.id, "agent.skills.upload.create", "_agent_skill_uploads", result.uploadId, mutationAuditMetadata(ctx, { agentName: args.agentName, filename: args.filename, bytes: result.bytes, sha256: result.sha256 })); return toolResult("Created Agent Skill upload session.", { ...result, rowCount: 1, returned: 1, truncated: false }); }
|
|
873
|
+
}),
|
|
874
|
+
operatorTool({
|
|
875
|
+
name: "agent_skills_refresh", description: "Refresh an imported local Agent Skill from its immutable origin anchor with revision and local-modification guards.", scope: "operator:agent-skills:write",
|
|
876
|
+
inputSchema: agentSkillIdentityInput.extend({ expectedRevision: z.number().int(), overwriteLocalEdits: z.boolean().optional() }).strict(), annotations: { idempotentHint: true }, handler: async (args, ctx) => { const result = await refreshAgentSkill({ control: await db(ctx), agentName: args.agentName, name: args.name, expectedRevision: args.expectedRevision, overwriteLocalEdits: args.overwriteLocalEdits, actorKind: "operator", actor: ctx.user.id, fetcher: fetch }); await audit(ctx.control, ctx.user.id, "agent.skills.refresh", "_agent_skills", args.name, mutationAuditMetadata(ctx, { agentName: args.agentName, expectedRevision: args.expectedRevision, overwriteLocalEdits: args.overwriteLocalEdits === true })); return toolResult(result.ok ? `Refreshed Agent Skill ${args.name}.` : `Refresh failed: ${result.error?.message}`, { ...result, discovery: noPackages(result.discovery), rowCount: result.applied.length, returned: result.applied.length, truncated: false }, !result.ok); }
|
|
877
|
+
}),
|
|
878
|
+
operatorTool({
|
|
879
|
+
name: "agent_skills_edit", description: "CAS-edit an operator-visible local Agent Skill; code and managed guidance remain reconcile/sync owned.", scope: "operator:agent-skills:write",
|
|
880
|
+
inputSchema: agentSkillIdentityInput.extend({ expectedRevision: z.number().int(), edit: z.object({ title: z.string().nullable().optional(), description: z.string().optional(), mode: z.enum(["include", "available"]).optional(), ephemeral: z.boolean().optional(), replaceGuidance: z.string().optional(), findText: z.string().optional(), replaceWith: z.string().optional() }).strict() }).strict(), annotations: { idempotentHint: true }, handler: async (args, ctx) => { const row = await editAgentSkill(await db(ctx), { agentName: args.agentName, name: args.name, expectedRevision: args.expectedRevision, actorKind: "operator", actor: ctx.user.id, ...args.edit }); await audit(ctx.control, ctx.user.id, "agent.skills.edit", "_agent_skills", row.id, mutationAuditMetadata(ctx, { agentName: args.agentName, name: args.name, revision: row.revision, contentDigest: row.contentDigest })); return toolResult(`Edited Agent Skill ${args.name}.`, { skill: skillOperatorProjection(row), rowCount: 1, returned: 1, truncated: false }); }
|
|
881
|
+
}),
|
|
882
|
+
operatorTool({
|
|
883
|
+
name: "agent_skills_delete", description: "CAS-delete a local Agent Skill. Managed and code rows cannot be deleted directly.", scope: "operator:agent-skills:write",
|
|
884
|
+
inputSchema: agentSkillIdentityInput.extend({ expectedRevision: z.number().int() }).strict(), annotations: { destructiveHint: true, idempotentHint: false }, handler: async (args, ctx) => { await deleteAgentSkill(await db(ctx), { ...args, actorKind: "operator", actor: ctx.user.id }); await audit(ctx.control, ctx.user.id, "agent.skills.delete", "_agent_skills", args.name, mutationAuditMetadata(ctx, { agentName: args.agentName, name: args.name, expectedRevision: args.expectedRevision })); return toolResult(`Deleted Agent Skill ${args.name}.`, { deleted: true, name: args.name, rowCount: 1, returned: 0, truncated: false }); }
|
|
885
|
+
}),
|
|
886
|
+
operatorTool({
|
|
887
|
+
name: "agent_skills_set_enabled", description: "CAS-toggle any Agent Skill. Managed sync preserves this operator-owned enabled state.", scope: "operator:agent-skills:write",
|
|
888
|
+
inputSchema: agentSkillIdentityInput.extend({ enabled: z.boolean(), expectedRevision: z.number().int() }).strict(), annotations: { idempotentHint: true }, handler: async (args, ctx) => { const row = await setAgentSkillEnabled(await db(ctx), { ...args, actorKind: "operator", actor: ctx.user.id }); await audit(ctx.control, ctx.user.id, args.enabled ? "agent.skills.enable" : "agent.skills.disable", "_agent_skills", row.id, mutationAuditMetadata(ctx, { agentName: args.agentName, name: args.name, revision: row.revision })); return toolResult(`${args.enabled ? "Enabled" : "Disabled"} Agent Skill ${args.name}.`, { skill: skillOperatorProjection(row), rowCount: 1, returned: 1, truncated: false }); }
|
|
889
|
+
}),
|
|
890
|
+
operatorTool({
|
|
891
|
+
name: "agent_skills_source_register", description: "Register an operator-only website, exact manifest URL, or GitHub manifest as a managed source after digest-bound discovery, and apply its initial selection.", scope: "operator:agent-skills:write",
|
|
892
|
+
inputSchema: z.object({ agentName: z.string().min(1), source: agentSkillSourceInput, expectedSourceDigest: z.string().min(1), selection: z.union([z.literal("all"), z.array(z.string())]), syncEveryMs: z.number().int().min(900000).optional(), authSecretName: z.string().optional(), allowedArtifactOrigins: z.array(z.string()).max(8).optional(), artifactAuth: z.array(z.object({ origin: z.string(), secretName: z.string() })).optional() }).strict(), annotations: { idempotentHint: false }, handler: async (args, ctx) => { const control = await db(ctx); let descriptor = source(args.source); if (descriptor.kind !== "manifest-url" && descriptor.kind !== "website" && descriptor.kind !== "github")
|
|
893
|
+
throw new Error("Managed source must be manifest-url, website, or GitHub manifest"); if (descriptor.kind === "github" && (args.allowedArtifactOrigins?.length || args.artifactAuth?.length || args.authSecretName))
|
|
894
|
+
throw new Error("GitHub managed sources use the fixed public host set and do not accept custom credentials or origins"); if (descriptor.kind !== "github" && args.allowedArtifactOrigins?.length)
|
|
895
|
+
descriptor = { ...descriptor, allowedOrigins: args.allowedArtifactOrigins }; let requestedNames = args.selection === "all" ? undefined : args.selection; let preview = await discoverAgentSkills({ agentName: args.agentName, source: descriptor, requestedNames, control, fetcher: fetch, env: ctx.env, indexAuthSecretName: args.authSecretName, artifactAuth: args.artifactAuth }); if (args.selection === "all") {
|
|
896
|
+
requestedNames = preview.candidates.filter((candidate) => candidate.valid).map((candidate) => candidate.name);
|
|
897
|
+
if (requestedNames.length)
|
|
898
|
+
preview = await discoverAgentSkills({ agentName: args.agentName, source: descriptor, requestedNames, control, fetcher: fetch, env: ctx.env, indexAuthSecretName: args.authSecretName, artifactAuth: args.artifactAuth });
|
|
899
|
+
} if (preview.status !== "ready" || preview.sourceDigest !== args.expectedSourceDigest || preview.classification !== "managed-manifest")
|
|
900
|
+
throw new Error("Managed source discovery is not ready or digest changed"); const sourceId = `skill_source_${crypto.randomUUID().replace(/-/g, "")}`; const now = nowIso(); const url = preview.indexUrl ?? (preview.github ? `github:${preview.github.repo}@${preview.github.requestedRef ?? "default"}:${preview.github.manifestPath}` : ""); if (!control.runTransaction)
|
|
901
|
+
throw new Error("Managed source registration requires transaction support"); const statements = [{ sql: `INSERT INTO "_agent_skill_sources" ("id","agentName","sourceType","url","registeredBy","enabled","revision","syncEveryMs","authSecretName","allowedArtifactOriginsJson","artifactAuthJson","selectionMode","selectedNamesJson","repo","manifestPath","requestedRef","refKind","defaultBranch","resolvedCommit","createdAt","updatedAt") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, params: [sourceId, args.agentName, preview.github ? "github" : "http-index", url, "operator", 1, 1, args.syncEveryMs ?? 21600000, args.authSecretName ?? null, JSON.stringify(args.allowedArtifactOrigins ?? []), JSON.stringify(args.artifactAuth ?? []), args.selection === "all" ? "all" : "named", JSON.stringify(requestedNames ?? []), preview.github?.repo ?? null, preview.github?.manifestPath ?? null, preview.github?.requestedRef ?? null, preview.github?.refKind ?? null, preview.github?.defaultBranch ?? null, preview.resolvedCommit ?? null, now, now] }]; const transactionControl = { all: control.all.bind(control), first: control.first.bind(control), run: control.run.bind(control), runTransaction: async (batch) => { statements.push(...batch); } }; const result = await applyAgentSkills({ agentName: args.agentName, source: descriptor, requestedNames, expectedSourceDigest: args.expectedSourceDigest, control: transactionControl, actorKind: "operator", actor: ctx.user.id, managedSourceId: sourceId, fetcher: fetch, env: ctx.env, indexAuthSecretName: args.authSecretName, artifactAuth: args.artifactAuth }); if (!result.ok)
|
|
902
|
+
throw new Error(result.error?.message ?? "Managed source apply failed"); await control.runTransaction(statements); await audit(control, ctx.user.id, "agent.skills.source.register", "_agent_skill_sources", sourceId, mutationAuditMetadata(ctx, { agentName: args.agentName, sourceDigest: args.expectedSourceDigest, allowedArtifactOrigins: args.allowedArtifactOrigins ?? [], repo: preview.github?.repo, resolvedCommit: preview.resolvedCommit })); return toolResult("Registered managed Agent Skill source.", { sourceId, revision: 1, applied: result.applied, rowCount: result.applied.length, returned: result.applied.length, truncated: false }); }
|
|
903
|
+
}),
|
|
904
|
+
operatorTool({
|
|
905
|
+
name: "agent_skills_source_update", description: "Update operator-owned managed source scheduling, enablement, selection, and artifact-origin policy without fetching it.", scope: "operator:agent-skills:write",
|
|
906
|
+
inputSchema: z.object({ sourceId: z.string().min(1), expectedRevision: z.number().int(), enabled: z.boolean().optional(), syncEveryMs: z.number().int().min(900000).optional(), selection: z.union([z.literal("all"), z.array(z.string())]).optional(), authSecretName: z.string().nullable().optional(), allowedArtifactOrigins: z.array(z.string()).max(8).optional(), artifactAuth: z.array(z.object({ origin: z.string(), secretName: z.string() })).optional() }).strict(), annotations: { idempotentHint: true },
|
|
907
|
+
handler: async (args, ctx) => { const control = await db(ctx); const current = await control.first(`SELECT * FROM "_agent_skill_sources" WHERE "id"=? AND "registeredBy"='operator'`, [args.sourceId]); if (!current)
|
|
908
|
+
throw new Error("Operator-owned managed source not found"); const selectionMode = args.selection === undefined ? current.selectionMode : args.selection === "all" ? "all" : "named"; const selectedNames = args.selection === undefined ? current.selectedNamesJson : JSON.stringify(args.selection === "all" ? [] : args.selection); const allowedArtifactOrigins = args.allowedArtifactOrigins === undefined ? current.allowedArtifactOriginsJson : JSON.stringify(args.allowedArtifactOrigins.map(validateOutboundOrigin)); const artifactAuth = args.artifactAuth === undefined ? current.artifactAuthJson : JSON.stringify(args.artifactAuth.map((entry) => { const origin = validateOutboundOrigin(entry.origin); if (!/^[A-Z][A-Z0-9_]*$/.test(entry.secretName))
|
|
909
|
+
throw new Error("artifactAuth secretName must be an uppercase environment binding name"); return { origin, secretName: entry.secretName }; })); const updated = await control.first(`UPDATE "_agent_skill_sources" SET "enabled"=?,"revision"="revision"+1,"syncEveryMs"=?,"authSecretName"=?,"allowedArtifactOriginsJson"=?,"artifactAuthJson"=?,"selectionMode"=?,"selectedNamesJson"=?,"updatedAt"=? WHERE "id"=? AND "registeredBy"='operator' AND "revision"=? RETURNING *`, [args.enabled === undefined ? current.enabled : args.enabled ? 1 : 0, args.syncEveryMs ?? current.syncEveryMs, args.authSecretName === undefined ? current.authSecretName : args.authSecretName, allowedArtifactOrigins, artifactAuth, selectionMode, selectedNames, nowIso(), args.sourceId, args.expectedRevision]); if (!updated)
|
|
910
|
+
throw new Error("Managed source revision changed; refresh and retry"); await audit(control, ctx.user.id, "agent.skills.source.update", "_agent_skill_sources", args.sourceId, mutationAuditMetadata(ctx, { expectedRevision: args.expectedRevision, revision: updated.revision, enabled: args.enabled, syncEveryMs: args.syncEveryMs, selection: args.selection, allowedArtifactOrigins: args.allowedArtifactOrigins, artifactAuthOrigins: args.artifactAuth?.map((entry) => entry.origin) })); return toolResult("Updated managed Agent Skill source.", { source: updated, rowCount: 1, returned: 1, truncated: false }); }
|
|
911
|
+
}),
|
|
912
|
+
operatorTool({
|
|
913
|
+
name: "agent_skills_source_sync_now", description: "Synchronize one managed Agent Skill source immediately while retaining its last good snapshot on failure.", scope: "operator:agent-skills:write",
|
|
914
|
+
inputSchema: z.object({ sourceId: z.string().min(1) }).strict(), annotations: { idempotentHint: true },
|
|
915
|
+
handler: async (args, ctx) => { const result = await syncDueAgentSkillSources({ control: await db(ctx), fetcher: fetch, env: ctx.env, sourceId: args.sourceId }); await audit(ctx.control, ctx.user.id, "agent.skills.source.sync", "_agent_skill_sources", args.sourceId, mutationAuditMetadata(ctx, result)); return toolResult(result.failed.length ? "Managed Agent Skill source sync failed; the last good snapshot remains active." : "Synchronized managed Agent Skill source.", { ...result, rowCount: result.synced.length, returned: result.synced.length, truncated: false }, result.failed.length > 0); }
|
|
916
|
+
}),
|
|
917
|
+
operatorTool({
|
|
918
|
+
name: "agent_skills_source_remove", description: "Remove a managed Agent Skill source; by default its rows are retained disabled and orphaned.", scope: "operator:agent-skills:write",
|
|
919
|
+
inputSchema: z.object({ sourceId: z.string().min(1), expectedRevision: z.number().int(), deleteSkills: z.boolean().optional() }).strict(), annotations: { destructiveHint: true, idempotentHint: false }, handler: async (args, ctx) => { const control = await db(ctx); await removeManagedAgentSkillSource(control, { sourceId: args.sourceId, expectedRevision: args.expectedRevision, deleteSkills: args.deleteSkills }); await audit(control, ctx.user.id, "agent.skills.source.remove", "_agent_skill_sources", args.sourceId, mutationAuditMetadata(ctx, { expectedRevision: args.expectedRevision, deleteSkills: args.deleteSkills === true })); return toolResult("Removed managed Agent Skill source.", { sourceId: args.sourceId, deletedSkills: args.deleteSkills === true, rowCount: 1, returned: 0, truncated: false }); }
|
|
920
|
+
})
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Runtime-installable remote MCP servers. Install and rediscover reach the
|
|
925
|
+
* server once, on the operator's word; bind is a separate, closed allowlist per
|
|
926
|
+
* agent. No credential value ever passes through these tools — only a name.
|
|
927
|
+
*/
|
|
928
|
+
function createAgentRemoteMcpOperatorTools(options) {
|
|
929
|
+
const db = async (ctx) => { await ensureManifestTables(ctx.control, raybaseRemoteMcpSystemTables); return ctx.control; };
|
|
930
|
+
const accessInput = z.record(z.string(), z.string()).optional();
|
|
931
|
+
const access = (value) => value ? Object.fromEntries(Object.entries(value).map(([toolName, spec]) => [toolName, parseRemoteMcpAccessSpec(spec)])) : undefined;
|
|
932
|
+
return [
|
|
933
|
+
operatorTool({
|
|
934
|
+
name: "agent_remote_mcp_list", description: "List runtime-installed remote MCP servers, their pinned tools, agent bindings, and install findings. Credential references are names only.", scope: "operator:agent-remote-mcp:read",
|
|
935
|
+
inputSchema: z.object({}).strict(),
|
|
936
|
+
handler: async (_args, ctx) => { const control = await db(ctx); const overview = await installedRemoteMcpOverview(control); const findings = await remoteMcpInstallFindings({ control, env: ctx.env }); return toolResult(`Found ${overview.servers.length} installed remote MCP server(s).`, { ...overview, findings, rowCount: overview.servers.length, returned: overview.servers.length, truncated: false }); }
|
|
937
|
+
}),
|
|
938
|
+
operatorTool({
|
|
939
|
+
name: "agent_remote_mcp_install", description: "Install a remote MCP server with an explicit credential mode and transport. Secret values are never accepted by this model-visible tool.", scope: "operator:agent-remote-mcp:write",
|
|
940
|
+
inputSchema: z.object({ name: z.string().min(1), endpoint: z.string().min(1), credential: z.enum(["none", "secret", "stored", "oauth"]).optional(), secretName: z.string().optional(), scheme: z.enum(["bearer", "raw"]).optional(), transport: z.enum(["stateless", "sessioned"]).optional(), clientId: z.string().optional(), authorizationServer: z.string().optional(), access: accessInput }).strict(),
|
|
941
|
+
annotations: { idempotentHint: false },
|
|
942
|
+
handler: async (args, ctx) => {
|
|
943
|
+
const control = await db(ctx);
|
|
944
|
+
const mode = args.credential ?? "secret";
|
|
945
|
+
const oauthDiscovery = mode === "oauth" ? await discoverRemoteMcpOAuth({ endpoint: args.endpoint, clientId: args.clientId, authorizationServer: args.authorizationServer, fetcher: fetch }) : undefined;
|
|
946
|
+
const result = await installRemoteMcpServer({
|
|
947
|
+
control, name: args.name, endpoint: args.endpoint,
|
|
948
|
+
credential: mode === "none" ? { type: "none" } : mode === "secret" ? { type: "secret", name: args.secretName ?? remoteMcpSecretNameFor(args.name), scheme: args.scheme ?? "bearer" } : { type: "stored" },
|
|
949
|
+
transport: args.transport ?? "stateless",
|
|
950
|
+
oauthDiscovery,
|
|
951
|
+
access: access(args.access), reservedServerNames: options.remoteMcp?.serverNames ?? [],
|
|
952
|
+
actor: ctx.user.id, env: ctx.env, fetcher: fetch
|
|
953
|
+
});
|
|
954
|
+
await audit(control, ctx.user.id, "agent.remote_mcp.install", "_agent_remote_mcp_servers", result.row.id, mutationAuditMetadata(ctx, { serverName: args.name, endpointHost: result.server.endpointHost, secretName: result.secretName, toolNames: result.server.tools.map((tool) => tool.name), discoveryHash: result.row.discoveryHash }));
|
|
955
|
+
return toolResult(`Installed remote MCP server ${args.name} with ${result.server.tools.length} pinned tool(s).`, { server: result.server, secretName: result.secretName, rejected: result.discovery.rejected, rowCount: 1, returned: 1, truncated: false });
|
|
956
|
+
}
|
|
957
|
+
}),
|
|
958
|
+
operatorTool({
|
|
959
|
+
name: "agent_remote_mcp_credential_status", description: "Read safe runtime credential status. This never returns an encrypted envelope, token, verifier, code, or client secret.", scope: "operator:agent-remote-mcp:read",
|
|
960
|
+
inputSchema: z.object({ name: z.string().min(1) }).strict(),
|
|
961
|
+
handler: async (args, ctx) => { const control = await db(ctx); const status = await remoteMcpCredentialStatus(control, args.name); return toolResult(status ? `Credential for ${args.name} is ${status.state}.` : `No stored credential exists for ${args.name}.`, { credential: status, rowCount: status ? 1 : 0, returned: status ? 1 : 0, truncated: false }); }
|
|
962
|
+
}),
|
|
963
|
+
operatorTool({
|
|
964
|
+
name: "agent_remote_mcp_credential_set", description: "Issue a short-lived authenticated-admin upload link for a static credential. The token itself must never be passed to this tool.", scope: "operator:agent-remote-mcp:write",
|
|
965
|
+
inputSchema: z.object({ name: z.string().min(1), scheme: z.enum(["bearer", "raw"]).optional() }).strict(), annotations: { idempotentHint: false },
|
|
966
|
+
handler: async (args, ctx) => { const control = await db(ctx); if (!await findInstalledRemoteMcpServerRow(control, args.name))
|
|
967
|
+
throw new Error("Remote MCP server is not installed"); const issued = await new RemoteMcpCredentialStore(control, remoteMcpKeyFromEnv(ctx.env)).issueUploadTicket({ serverName: args.name, scheme: args.scheme ?? "bearer", actor: ctx.user.id }); const origin = new URL(ctx.request.url).origin; return toolResult(`Open the upload link while signed in as an operator. It expires at ${issued.expiresAt}.`, { uploadUrl: `${origin}/admin/agents#remote-mcp-credential=${encodeURIComponent(issued.ticket)}`, expiresAt: issued.expiresAt, rowCount: 1, returned: 1, truncated: false }); }
|
|
968
|
+
}),
|
|
969
|
+
operatorTool({
|
|
970
|
+
name: "agent_remote_mcp_credential_clear", description: "Clear a stored remote MCP credential and invalidate its generation.", scope: "operator:agent-remote-mcp:write",
|
|
971
|
+
inputSchema: z.object({ name: z.string().min(1) }).strict(), annotations: { destructiveHint: true, idempotentHint: true },
|
|
972
|
+
handler: async (args, ctx) => { const control = await db(ctx); const status = await new RemoteMcpCredentialStore(control, remoteMcpKeyFromEnv(ctx.env)).clear(args.name, ctx.user.id); await audit(control, ctx.user.id, "agent.remote_mcp.credential.clear", "_agent_remote_mcp_credentials", undefined, mutationAuditMetadata(ctx, { serverName: args.name, generation: status.generation })); return toolResult(`Cleared credential for ${args.name}.`, { credential: status, rowCount: 1, returned: 1, truncated: false }); }
|
|
973
|
+
}),
|
|
974
|
+
operatorTool({
|
|
975
|
+
name: "agent_remote_mcp_oauth_connect", description: "Begin the browser OAuth connection for an installed OAuth remote MCP server. Client secrets are not accepted by this tool.", scope: "operator:agent-remote-mcp:write",
|
|
976
|
+
inputSchema: z.object({ name: z.string().min(1), clientId: z.string().optional(), scopes: z.array(z.string()).max(64).optional() }).strict(), annotations: { idempotentHint: false },
|
|
977
|
+
handler: async (args, ctx) => { const control = await db(ctx); const row = await findInstalledRemoteMcpServerRow(control, args.name); if (!row?.oauthMetadataJson)
|
|
978
|
+
throw new Error("Remote MCP server is not configured for OAuth"); const result = await beginRemoteMcpOAuthConnect({ control, key: remoteMcpKeyFromEnv(ctx.env), serverName: args.name, discovery: JSON.parse(row.oauthMetadataJson), client: { ...(args.clientId ? { clientId: args.clientId } : {}), redirectUri: `${new URL(ctx.request.url).origin}/api/agents/remote-mcp/oauth/callback`, ...(args.scopes ? { scopes: args.scopes } : {}) }, actor: ctx.user.id, fetcher: fetch }); return toolResult("Open the authorization URL to connect the remote MCP server.", { authorizationUrl: result.authorizationUrl, expiresAt: result.expiresAt, rowCount: 1, returned: 1, truncated: false }); }
|
|
979
|
+
}),
|
|
980
|
+
operatorTool({
|
|
981
|
+
name: "agent_remote_mcp_oauth_disconnect", description: "Disconnect a remote MCP OAuth identity and invalidate its credential generation.", scope: "operator:agent-remote-mcp:write",
|
|
982
|
+
inputSchema: z.object({ name: z.string().min(1) }).strict(), annotations: { destructiveHint: true, idempotentHint: true },
|
|
983
|
+
handler: async (args, ctx) => { const control = await db(ctx); const status = await new RemoteMcpCredentialStore(control, remoteMcpKeyFromEnv(ctx.env)).clear(args.name, ctx.user.id); return toolResult(`Disconnected OAuth for ${args.name}.`, { credential: status, rowCount: 1, returned: 1, truncated: false }); }
|
|
984
|
+
}),
|
|
985
|
+
operatorTool({
|
|
986
|
+
name: "agent_remote_mcp_rediscover", description: "Re-read a server's tools/list and show the diff. Without confirm nothing changes; with confirm the new set is pinned and bindings to a tool that disappeared are disabled.", scope: "operator:agent-remote-mcp:write",
|
|
987
|
+
inputSchema: z.object({ name: z.string().min(1), confirm: z.boolean().optional() }).strict(), annotations: { idempotentHint: true },
|
|
988
|
+
handler: async (args, ctx) => {
|
|
989
|
+
const control = await db(ctx);
|
|
990
|
+
const result = await rediscoverRemoteMcpServer({ control, name: args.name, confirm: args.confirm === true, actor: ctx.user.id, env: ctx.env, fetcher: fetch });
|
|
991
|
+
if (result.applied)
|
|
992
|
+
await audit(control, ctx.user.id, "agent.remote_mcp.rediscover", "_agent_remote_mcp_servers", args.name, mutationAuditMetadata(ctx, { serverName: args.name, diff: result.diff, disabledBindings: result.disabledBindings.map((binding) => `${binding.agentName}/${binding.serverName}`) }));
|
|
993
|
+
return toolResult(result.applied ? `Pinned the new tool set for ${args.name}.` : `Previewed the tool diff for ${args.name}; pass confirm to pin it.`, { ...result, rowCount: 1, returned: 1, truncated: false });
|
|
994
|
+
}
|
|
995
|
+
}),
|
|
996
|
+
operatorTool({
|
|
997
|
+
name: "agent_remote_mcp_disable", description: "Disable or re-enable an installed remote MCP server. A disabled server offers no tools to any agent.", scope: "operator:agent-remote-mcp:write",
|
|
998
|
+
inputSchema: z.object({ name: z.string().min(1), enabled: z.boolean().optional() }).strict(), annotations: { idempotentHint: true },
|
|
999
|
+
handler: async (args, ctx) => {
|
|
1000
|
+
const control = await db(ctx);
|
|
1001
|
+
const status = args.enabled === true ? "installed" : "disabled";
|
|
1002
|
+
const row = await setInstalledRemoteMcpServerStatus({ control, name: args.name, status });
|
|
1003
|
+
await audit(control, ctx.user.id, status === "disabled" ? "agent.remote_mcp.disable" : "agent.remote_mcp.enable", "_agent_remote_mcp_servers", row.id, mutationAuditMetadata(ctx, { serverName: row.name }));
|
|
1004
|
+
return toolResult(`${status === "disabled" ? "Disabled" : "Enabled"} remote MCP server ${args.name}.`, { name: row.name, status: row.status, rowCount: 1, returned: 1, truncated: false });
|
|
1005
|
+
}
|
|
1006
|
+
}),
|
|
1007
|
+
operatorTool({
|
|
1008
|
+
name: "agent_remote_mcp_bind", description: "Bind pinned tools of an installed remote MCP server to one agent. The tool list is a closed allowlist; a duplicate model tool name refuses.", scope: "operator:agent-remote-mcp:write",
|
|
1009
|
+
inputSchema: z.object({ agentName: z.string().min(1), serverName: z.string().min(1), toolNames: z.array(z.string()).min(1) }).strict(), annotations: { idempotentHint: true },
|
|
1010
|
+
handler: async (args, ctx) => {
|
|
1011
|
+
const control = await db(ctx);
|
|
1012
|
+
const binding = await bindAgentRemoteMcpServer({ control, agentName: args.agentName, serverName: args.serverName, toolNames: args.toolNames, reservedModelToolNames: options.remoteMcp?.agentModelToolNames?.[args.agentName] ?? [], actor: ctx.user.id });
|
|
1013
|
+
await audit(control, ctx.user.id, "agent.remote_mcp.bind", "_agent_remote_mcp_bindings", binding.id, mutationAuditMetadata(ctx, { agentName: args.agentName, serverName: args.serverName, toolNames: args.toolNames }));
|
|
1014
|
+
return toolResult(`Bound ${args.serverName} to ${args.agentName}.`, { binding: remoteMcpBindingView(binding), rowCount: 1, returned: 1, truncated: false });
|
|
1015
|
+
}
|
|
1016
|
+
}),
|
|
1017
|
+
operatorTool({
|
|
1018
|
+
name: "agent_remote_mcp_unbind", description: "Remove one agent's binding to an installed remote MCP server.", scope: "operator:agent-remote-mcp:write",
|
|
1019
|
+
inputSchema: z.object({ agentName: z.string().min(1), serverName: z.string().min(1) }).strict(), annotations: { destructiveHint: true, idempotentHint: true },
|
|
1020
|
+
handler: async (args, ctx) => {
|
|
1021
|
+
const control = await db(ctx);
|
|
1022
|
+
const removed = await unbindAgentRemoteMcpServer({ control, agentName: args.agentName, serverName: args.serverName });
|
|
1023
|
+
if (removed)
|
|
1024
|
+
await audit(control, ctx.user.id, "agent.remote_mcp.unbind", "_agent_remote_mcp_bindings", undefined, mutationAuditMetadata(ctx, { agentName: args.agentName, serverName: args.serverName }));
|
|
1025
|
+
return toolResult(removed ? `Unbound ${args.serverName} from ${args.agentName}.` : `${args.agentName} was not bound to ${args.serverName}.`, { removed, rowCount: removed ? 1 : 0, returned: 0, truncated: false });
|
|
1026
|
+
}
|
|
1027
|
+
})
|
|
1028
|
+
];
|
|
1029
|
+
}
|
|
1030
|
+
function skillOperatorProjection(row) { return { name: row.name, agentName: row.agentName, title: row.title, description: row.description, source: row.source, mode: row.mode, ephemeral: row.ephemeral === 1, enabled: row.enabled === 1, revision: row.revision, contentDigest: row.contentDigest, lastImportedContentDigest: row.lastImportedContentDigest, ownerKind: row.ownerKind, ownerAgentName: row.ownerAgentName, provenance: { originKind: row.originKind, originUrl: row.originUrl, originRef: row.originRef, originRefKind: row.originRefKind, originCommit: row.originCommit, originRepo: row.originRepo, originManifestPath: row.originManifestPath, originSkillPath: row.originSkillPath, originDigest: row.originDigest, importedAt: row.importedAt }, sync: { sourceId: row.sourceId, remoteSha256: row.remoteSha256, syncedAt: row.syncedAt, status: row.syncStatus, error: row.syncError }, updatedAt: row.updatedAt }; }
|
|
1031
|
+
function base64Bytes(value) { const binary = atob(value); return Uint8Array.from(binary, (char) => char.charCodeAt(0)); }
|
|
1032
|
+
export function createCoreOperatorMcpTools(options) {
|
|
1033
|
+
const tools = [
|
|
1034
|
+
operatorTool({
|
|
1035
|
+
name: "app_health_summary",
|
|
1036
|
+
description: "Summarize Raybase app health, environment, storage, jobs, and recent failure evidence.",
|
|
1037
|
+
scope: "operator:health:read",
|
|
1038
|
+
inputSchema: z.object({}).strict(),
|
|
1039
|
+
handler: async (_args, ctx) => {
|
|
1040
|
+
const db = operatorDb(ctx, options);
|
|
1041
|
+
const env = envRecord(ctx.env);
|
|
1042
|
+
let databaseReachable = false;
|
|
1043
|
+
let databaseSizeBytes = null;
|
|
1044
|
+
try {
|
|
1045
|
+
await dataStub(ctx.env).ping();
|
|
1046
|
+
databaseReachable = true;
|
|
1047
|
+
databaseSizeBytes = await dataStub(ctx.env).databaseSize();
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
databaseReachable = false;
|
|
1051
|
+
}
|
|
1052
|
+
const recentErrors = await db.control.first(`SELECT COUNT(*) as "count" FROM "_audit" WHERE "createdAt" >= ? AND ("action" LIKE '%fail%' OR "action" LIKE '%deny%' OR "action" LIKE '%error%')`, [new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()]).catch(() => null);
|
|
1053
|
+
const jobFailures = await dataStub(ctx.env)
|
|
1054
|
+
.first(`SELECT COUNT(*) as "count" FROM "_job_runs" WHERE "status" IN ('failed','denied','expired')`)
|
|
1055
|
+
.catch(() => null);
|
|
1056
|
+
const runtimeErrors24h = await db.control.first(`SELECT COUNT(*) as "count" FROM ${q(raybaseRuntimeErrorEventsTableName)} WHERE "expiresAt" > ? AND "createdAt" >= ?`, [nowIso(), new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()]).catch(() => null);
|
|
1057
|
+
return toolResult(`App ${options.config.appName} is ${databaseReachable ? "reachable" : "not reachable"}.`, {
|
|
1058
|
+
appName: options.config.appName,
|
|
1059
|
+
environment: env.RAYBASE_ENV ?? "production",
|
|
1060
|
+
url: new URL(ctx.request.url).origin,
|
|
1061
|
+
coreVersion: options.coreVersion ?? "0.1.0",
|
|
1062
|
+
packageVersions: { "@raybase/core": options.coreVersion ?? "0.1.0" },
|
|
1063
|
+
database: { reachable: databaseReachable, sizeBytes: databaseSizeBytes },
|
|
1064
|
+
storage: { enabled: Boolean(options.storage), r2BindingPresent: Boolean(env.RAYBASE_FILES) },
|
|
1065
|
+
features: {
|
|
1066
|
+
aiEnabled: options.config.ai?.enabled === true,
|
|
1067
|
+
realtimeEnabled: options.config.realtime?.enabled === true,
|
|
1068
|
+
jobsEnabled: options.config.jobs?.enabled === true || Boolean(options.jobs && Object.keys(options.jobs).length)
|
|
1069
|
+
},
|
|
1070
|
+
recentFailureCounts: {
|
|
1071
|
+
audit24h: Number(recentErrors?.count ?? 0),
|
|
1072
|
+
jobs: Number(jobFailures?.count ?? 0),
|
|
1073
|
+
runtimeErrors24h: Number(runtimeErrors24h?.count ?? 0)
|
|
1074
|
+
},
|
|
1075
|
+
appliedFilters: { window: "24h" },
|
|
1076
|
+
rowCount: 1,
|
|
1077
|
+
returned: 1,
|
|
1078
|
+
truncated: false,
|
|
1079
|
+
suggestedNextInspection: "Use deployment_history, audit_query, and job_runs_query for recent failures."
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}),
|
|
1083
|
+
operatorTool({
|
|
1084
|
+
name: "deployment_history",
|
|
1085
|
+
description: "Inspect deployment and schema-version evidence without inventing missing deploy history.",
|
|
1086
|
+
scope: "operator:health:read",
|
|
1087
|
+
inputSchema: listInput,
|
|
1088
|
+
handler: async (args, ctx) => {
|
|
1089
|
+
const db = operatorDb(ctx, options);
|
|
1090
|
+
const schemaVersions = await db.control.all(`SELECT * FROM "_schema_versions" ORDER BY "updatedAt" DESC LIMIT ? OFFSET ?`, [args.limit, args.offset]).catch(() => []);
|
|
1091
|
+
const deployAudit = await db.control.all(`SELECT "id", "action", "targetType", "targetId", "metadata", "createdAt" FROM "_audit"
|
|
1092
|
+
WHERE "action" LIKE '%deploy%' OR "action" LIKE '%migration%' OR "action" LIKE '%schema%'
|
|
1093
|
+
ORDER BY "createdAt" DESC LIMIT ?`, [args.limit]).catch(() => []);
|
|
1094
|
+
return toolResult(schemaVersions.length || deployAudit.length ? "Deployment evidence found." : "No deploy ledger was found for this app.", {
|
|
1095
|
+
schemaVersions: schemaVersions.map((row) => redactRow(row).row),
|
|
1096
|
+
auditDeployMarkers: deployAudit.map((row) => ({ ...redactRow(row).row, metadata: parseJson(row.metadata) })),
|
|
1097
|
+
packageVersions: { "@raybase/core": options.coreVersion ?? "0.1.0" },
|
|
1098
|
+
appliedFilters: { limit: args.limit, offset: args.offset },
|
|
1099
|
+
rowCount: schemaVersions.length + deployAudit.length,
|
|
1100
|
+
returned: schemaVersions.length + deployAudit.length,
|
|
1101
|
+
truncated: false,
|
|
1102
|
+
suggestedNextInspection: "Use schema_summary and audit_query around deploy timestamps."
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}),
|
|
1106
|
+
operatorTool({
|
|
1107
|
+
name: "schema_summary",
|
|
1108
|
+
description: "Summarize app and Raybase system schema with columns, indexes, references, and row counts.",
|
|
1109
|
+
scope: "operator:schema:read",
|
|
1110
|
+
inputSchema: z.object({ includeSystem: z.boolean().default(true) }).strict(),
|
|
1111
|
+
handler: async (args, ctx) => {
|
|
1112
|
+
const db = operatorDb(ctx, options);
|
|
1113
|
+
const tables = await databaseSchemaSummary(db.control, options.schema, systemTableNames(options), { systemOnly: args.includeSystem ? undefined : false });
|
|
1114
|
+
const filtered = args.includeSystem ? tables : tables.filter((table) => table.appDefined);
|
|
1115
|
+
return toolResult(`Found ${filtered.length} schema table(s).`, {
|
|
1116
|
+
tables: filtered,
|
|
1117
|
+
appliedFilters: { includeSystem: args.includeSystem },
|
|
1118
|
+
rowCount: filtered.length,
|
|
1119
|
+
returned: filtered.length,
|
|
1120
|
+
truncated: false,
|
|
1121
|
+
suggestedNextInspection: "Use app_data_table_summary before querying app rows."
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}),
|
|
1125
|
+
operatorTool({
|
|
1126
|
+
name: "policy_summary",
|
|
1127
|
+
description: "Summarize app table policies from Raybase schema metadata.",
|
|
1128
|
+
scope: "operator:policy:read",
|
|
1129
|
+
inputSchema: z.object({}).strict(),
|
|
1130
|
+
handler: async () => {
|
|
1131
|
+
const actions = ["read", "create", "update", "delete"];
|
|
1132
|
+
const tables = Object.values(options.schema.tables).map((table) => ({
|
|
1133
|
+
name: table.name,
|
|
1134
|
+
label: table.label ?? null,
|
|
1135
|
+
policies: Object.fromEntries(actions.map((action) => [action, describePolicy(table.policies?.[action])])),
|
|
1136
|
+
missingActions: actions.filter((action) => !table.policies?.[action])
|
|
1137
|
+
}));
|
|
1138
|
+
return toolResult(`Summarized policies for ${tables.length} app table(s).`, {
|
|
1139
|
+
introspection: "partial",
|
|
1140
|
+
tables,
|
|
1141
|
+
appliedFilters: {},
|
|
1142
|
+
rowCount: tables.length,
|
|
1143
|
+
returned: tables.length,
|
|
1144
|
+
truncated: false,
|
|
1145
|
+
suggestedNextInspection: "Treat partial policy summaries as labels; inspect source policy definitions for full behavior."
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}),
|
|
1149
|
+
operatorTool({
|
|
1150
|
+
name: "users_summary",
|
|
1151
|
+
description: "Summarize users, statuses, operator roles, app roles, invites, and recent sessions.",
|
|
1152
|
+
scope: "operator:users:read",
|
|
1153
|
+
inputSchema: listInput.extend({ search: z.string().optional() }).strict(),
|
|
1154
|
+
handler: async (args, ctx) => {
|
|
1155
|
+
const db = operatorDb(ctx, options);
|
|
1156
|
+
const where = args.search ? `WHERE "email" LIKE ? OR "name" LIKE ?` : "";
|
|
1157
|
+
const params = args.search ? [`%${args.search}%`, `%${args.search}%`, args.limit, args.offset] : [args.limit, args.offset];
|
|
1158
|
+
const users = await db.control.all(`SELECT "id", "name", "email", "emailVerified", "operatorRole", "appRoles", "banned", "createdAt", "updatedAt"
|
|
1159
|
+
FROM "_auth_users" ${where} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`, params);
|
|
1160
|
+
const byOperatorRole = await db.control.all(`SELECT CASE WHEN "operatorRole" IS NULL OR "operatorRole" = '' THEN 'none' ELSE "operatorRole" END as "role", COUNT(*) as "count" FROM "_auth_users" GROUP BY CASE WHEN "operatorRole" IS NULL OR "operatorRole" = '' THEN 'none' ELSE "operatorRole" END`);
|
|
1161
|
+
const byStatus = await db.control.all(`SELECT CASE WHEN "banned" THEN 'disabled' ELSE 'active' END as "status", COUNT(*) as "count" FROM "_auth_users" GROUP BY CASE WHEN "banned" THEN 'disabled' ELSE 'active' END`);
|
|
1162
|
+
const invites = await db.control.all(`SELECT "status", COUNT(*) as "count" FROM "_invites" GROUP BY "status"`).catch(() => []);
|
|
1163
|
+
const recentSessions = await db.control.all(`SELECT s."id", s."userId", s."createdAt", s."updatedAt", s."expiresAt", u."email", u."name"
|
|
1164
|
+
FROM "_auth_sessions" s LEFT JOIN "_auth_users" u ON u."id" = s."userId"
|
|
1165
|
+
ORDER BY s."updatedAt" DESC LIMIT 20`).catch(() => []);
|
|
1166
|
+
const safeUsers = users.map((user) => ({
|
|
1167
|
+
id: user.id,
|
|
1168
|
+
name: user.name,
|
|
1169
|
+
email: user.email,
|
|
1170
|
+
emailVerified: Boolean(user.emailVerified),
|
|
1171
|
+
operatorRole: normalizeStoredAdminRole(user.operatorRole),
|
|
1172
|
+
appRoles: parseRoles(user.appRoles),
|
|
1173
|
+
disabled: Boolean(user.banned),
|
|
1174
|
+
createdAt: user.createdAt,
|
|
1175
|
+
updatedAt: user.updatedAt
|
|
1176
|
+
}));
|
|
1177
|
+
return toolResult(`Returned ${safeUsers.length} user(s).`, {
|
|
1178
|
+
counts: { byOperatorRole, byStatus, invitesByStatus: invites },
|
|
1179
|
+
recentSessions: recentSessions.map((session) => redactRow(session).row),
|
|
1180
|
+
users: safeUsers,
|
|
1181
|
+
appliedFilters: { search: args.search ?? null, limit: args.limit, offset: args.offset },
|
|
1182
|
+
rowCount: safeUsers.length,
|
|
1183
|
+
returned: safeUsers.length,
|
|
1184
|
+
truncated: false,
|
|
1185
|
+
suggestedNextInspection: "Use user_detail for one user and audit_query for user actions."
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}),
|
|
1189
|
+
operatorTool({
|
|
1190
|
+
name: "user_detail",
|
|
1191
|
+
description: "Inspect one user's roles, sessions, audit ids, and MCP grants without returning secrets.",
|
|
1192
|
+
scope: "operator:users:read",
|
|
1193
|
+
inputSchema: userDetailInput,
|
|
1194
|
+
handler: async (args, ctx) => {
|
|
1195
|
+
const db = operatorDb(ctx, options);
|
|
1196
|
+
const user = args.id
|
|
1197
|
+
? await db.control.first(`SELECT "id", "name", "email", "emailVerified", "operatorRole", "appRoles", "banned", "createdAt", "updatedAt" FROM "_auth_users" WHERE "id" = ?`, [args.id])
|
|
1198
|
+
: await db.control.first(`SELECT "id", "name", "email", "emailVerified", "operatorRole", "appRoles", "banned", "createdAt", "updatedAt" FROM "_auth_users" WHERE "email" = ?`, [args.email]);
|
|
1199
|
+
if (!user)
|
|
1200
|
+
return toolResult("User not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
1201
|
+
const sessionCount = await db.control.first(`SELECT COUNT(*) as "count" FROM "_auth_sessions" WHERE "userId" = ?`, [String(user.id)]);
|
|
1202
|
+
const recentAudit = await db.control.all(`SELECT "id", "action", "targetType", "targetId", "createdAt" FROM "_audit" WHERE "actorId" = ? ORDER BY "createdAt" DESC LIMIT 20`, [String(user.id)]);
|
|
1203
|
+
const grants = await db.control.all(`SELECT "id", "subjectType", "subjectId", "clientId", "clientName", "scopes", "resource", "createdAt", "expiresAt", "revokedAt", "lastUsedAt"
|
|
1204
|
+
FROM "_mcp_grants" WHERE "userId" = ? OR "subjectId" = ? ORDER BY "createdAt" DESC LIMIT 50`, [String(user.id), String(user.id)]).catch(() => []);
|
|
1205
|
+
return toolResult(`Loaded user ${String(user.email)}.`, {
|
|
1206
|
+
user: {
|
|
1207
|
+
id: user.id,
|
|
1208
|
+
name: user.name,
|
|
1209
|
+
email: user.email,
|
|
1210
|
+
emailVerified: Boolean(user.emailVerified),
|
|
1211
|
+
operatorRole: normalizeStoredAdminRole(user.operatorRole),
|
|
1212
|
+
appRoles: parseRoles(user.appRoles),
|
|
1213
|
+
disabled: Boolean(user.banned),
|
|
1214
|
+
createdAt: user.createdAt,
|
|
1215
|
+
updatedAt: user.updatedAt
|
|
1216
|
+
},
|
|
1217
|
+
sessionCount: Number(sessionCount?.count ?? 0),
|
|
1218
|
+
recentAudit,
|
|
1219
|
+
activeMcpGrants: grants.map((grant) => ({ ...grant, scopes: parseJson(grant.scopes), revokedAt: grant.revokedAt ?? null })),
|
|
1220
|
+
memberships: [],
|
|
1221
|
+
appliedFilters: args,
|
|
1222
|
+
rowCount: 1,
|
|
1223
|
+
returned: 1,
|
|
1224
|
+
truncated: false,
|
|
1225
|
+
suggestedNextInspection: "Use audit_query filtered by actorId for complete action history."
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}),
|
|
1229
|
+
operatorTool({
|
|
1230
|
+
name: "audit_query",
|
|
1231
|
+
description: "Query hot Raybase audit rows by safe filters with parsed metadata.",
|
|
1232
|
+
scope: "operator:audit:read",
|
|
1233
|
+
inputSchema: auditQueryInput,
|
|
1234
|
+
handler: async (args, ctx) => {
|
|
1235
|
+
const db = operatorDb(ctx, options);
|
|
1236
|
+
const where = [];
|
|
1237
|
+
const params = [];
|
|
1238
|
+
const applied = { ...addTimeRange(where, params, `a."createdAt"`, args), limit: args.limit, offset: args.offset };
|
|
1239
|
+
if (args.action) {
|
|
1240
|
+
where.push(`a."action" = ?`);
|
|
1241
|
+
params.push(args.action);
|
|
1242
|
+
applied.action = args.action;
|
|
1243
|
+
}
|
|
1244
|
+
if (args.targetType) {
|
|
1245
|
+
where.push(`a."targetType" = ?`);
|
|
1246
|
+
params.push(args.targetType);
|
|
1247
|
+
applied.targetType = args.targetType;
|
|
1248
|
+
}
|
|
1249
|
+
if (args.targetId) {
|
|
1250
|
+
where.push(`a."targetId" = ?`);
|
|
1251
|
+
params.push(args.targetId);
|
|
1252
|
+
applied.targetId = args.targetId;
|
|
1253
|
+
}
|
|
1254
|
+
if (args.actorId) {
|
|
1255
|
+
where.push(`a."actorId" = ?`);
|
|
1256
|
+
params.push(args.actorId);
|
|
1257
|
+
applied.actorId = args.actorId;
|
|
1258
|
+
}
|
|
1259
|
+
if (args.actorEmail) {
|
|
1260
|
+
where.push(`u."email" = ?`);
|
|
1261
|
+
params.push(args.actorEmail);
|
|
1262
|
+
applied.actorEmail = args.actorEmail;
|
|
1263
|
+
}
|
|
1264
|
+
if (args.search) {
|
|
1265
|
+
where.push(`(a."action" LIKE ? OR a."targetType" LIKE ? OR a."targetId" LIKE ?)`);
|
|
1266
|
+
params.push(`%${args.search}%`, `%${args.search}%`, `%${args.search}%`);
|
|
1267
|
+
applied.search = args.search;
|
|
1268
|
+
}
|
|
1269
|
+
params.push(args.limit, args.offset);
|
|
1270
|
+
const rows = await db.control.all(`SELECT a.*, u."name" AS "actorName", u."email" AS "actorEmail"
|
|
1271
|
+
FROM "_audit" a LEFT JOIN "_auth_users" u ON u."id" = a."actorId"
|
|
1272
|
+
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
1273
|
+
ORDER BY a."createdAt" DESC LIMIT ? OFFSET ?`, params);
|
|
1274
|
+
return toolResult(`Returned ${rows.length} audit row(s).`, {
|
|
1275
|
+
rows: rows.map((row) => ({ ...redactRow(row).row, metadata: parseJson(row.metadata) })),
|
|
1276
|
+
appliedFilters: applied,
|
|
1277
|
+
rowCount: rows.length,
|
|
1278
|
+
returned: rows.length,
|
|
1279
|
+
truncated: false,
|
|
1280
|
+
suggestedNextInspection: "Use targetType/targetId filters or user_detail for actor-focused evidence."
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}),
|
|
1284
|
+
operatorTool({
|
|
1285
|
+
name: "audit_archive_status",
|
|
1286
|
+
description: "Inspect audit archive configuration, manifests, retention, and missing binding warnings.",
|
|
1287
|
+
scope: "operator:audit:read",
|
|
1288
|
+
inputSchema: z.object({ limit: z.coerce.number().int().min(1).max(100).default(20) }).strict(),
|
|
1289
|
+
handler: async (args, ctx) => {
|
|
1290
|
+
const db = operatorDb(ctx, options);
|
|
1291
|
+
const env = envRecord(ctx.env);
|
|
1292
|
+
const manifests = await db.control.all(`SELECT * FROM "_audit_archives" ORDER BY "windowStart" DESC LIMIT ?`, [args.limit]).catch(() => []);
|
|
1293
|
+
return toolResult(`Audit archive is ${auditArchiveEnabled(String(env.RAYBASE_AUDIT_ARCHIVE_ENABLED ?? "")) ? "enabled" : "disabled"}.`, {
|
|
1294
|
+
enabled: auditArchiveEnabled(String(env.RAYBASE_AUDIT_ARCHIVE_ENABLED ?? "")),
|
|
1295
|
+
hotRetentionDays: parseAuditHotDays(typeof env.RAYBASE_AUDIT_HOT_DAYS === "string" ? env.RAYBASE_AUDIT_HOT_DAYS : undefined),
|
|
1296
|
+
r2BindingPresent: Boolean(env.RAYBASE_AUDIT_ARCHIVE),
|
|
1297
|
+
manifests,
|
|
1298
|
+
lastArchivedWindow: manifests[0] ?? null,
|
|
1299
|
+
warnings: [env.RAYBASE_AUDIT_ARCHIVE ? null : "RAYBASE_AUDIT_ARCHIVE binding is missing"].filter(Boolean),
|
|
1300
|
+
appliedFilters: { limit: args.limit },
|
|
1301
|
+
rowCount: manifests.length,
|
|
1302
|
+
returned: manifests.length,
|
|
1303
|
+
truncated: false,
|
|
1304
|
+
suggestedNextInspection: "Use audit_query for hot rows; v1 does not download archive objects."
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}),
|
|
1308
|
+
operatorTool({
|
|
1309
|
+
name: "import_runs_query",
|
|
1310
|
+
description: "Query admin CSV import runs with row counts, actor, file metadata, status, and suggested inspection.",
|
|
1311
|
+
scope: "operator:imports:read",
|
|
1312
|
+
inputSchema: importRunsQueryInput,
|
|
1313
|
+
handler: async (args, ctx) => {
|
|
1314
|
+
const db = operatorDb(ctx, options);
|
|
1315
|
+
if (!(await tableExists(db.control, importRunsTableName))) {
|
|
1316
|
+
return toolResult("CSV import history is not installed for this app.", {
|
|
1317
|
+
unavailable: true,
|
|
1318
|
+
appliedFilters: args,
|
|
1319
|
+
rowCount: 0,
|
|
1320
|
+
returned: 0,
|
|
1321
|
+
truncated: false,
|
|
1322
|
+
suggestedNextInspection: "Open /admin/data once after deploying the import feature so the Durable Object bootstrap can create import tables."
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
const where = [];
|
|
1326
|
+
const params = [];
|
|
1327
|
+
const applied = { ...addTimeRange(where, params, `"createdAt"`, args), limit: args.limit, offset: args.offset };
|
|
1328
|
+
if (args.table) {
|
|
1329
|
+
where.push(`"tableName" = ?`);
|
|
1330
|
+
params.push(args.table);
|
|
1331
|
+
applied.table = args.table;
|
|
1332
|
+
}
|
|
1333
|
+
if (args.status) {
|
|
1334
|
+
where.push(`"status" = ?`);
|
|
1335
|
+
params.push(args.status);
|
|
1336
|
+
applied.status = args.status;
|
|
1337
|
+
}
|
|
1338
|
+
if (args.actorId) {
|
|
1339
|
+
where.push(`"actorId" = ?`);
|
|
1340
|
+
params.push(args.actorId);
|
|
1341
|
+
applied.actorId = args.actorId;
|
|
1342
|
+
}
|
|
1343
|
+
if (args.runId) {
|
|
1344
|
+
where.push(`"id" = ?`);
|
|
1345
|
+
params.push(args.runId);
|
|
1346
|
+
applied.runId = args.runId;
|
|
1347
|
+
}
|
|
1348
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
1349
|
+
const count = await db.control.first(`SELECT COUNT(*) as "count" FROM ${q(importRunsTableName)} ${whereSql}`, params);
|
|
1350
|
+
const runs = await db.control.all(`SELECT * FROM ${q(importRunsTableName)} ${whereSql} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`, [...params, args.limit, args.offset]);
|
|
1351
|
+
const failedExamples = await db.control.all(`SELECT "runId", "rowNumber", "status", "targetId", "errorJson", "createdAt"
|
|
1352
|
+
FROM ${q(importRowsTableName)}
|
|
1353
|
+
WHERE "status" IN ('invalid','failed')
|
|
1354
|
+
ORDER BY "createdAt" DESC LIMIT 20`).catch(() => []);
|
|
1355
|
+
return toolResult(`Returned ${runs.length} import run(s).`, {
|
|
1356
|
+
runs: runs.map((run) => ({
|
|
1357
|
+
...redactRow(run).row,
|
|
1358
|
+
mappingJson: parseJson(run.mappingJson),
|
|
1359
|
+
optionsJson: parseJson(run.optionsJson),
|
|
1360
|
+
summaryJson: parseJson(run.summaryJson),
|
|
1361
|
+
errorJson: parseJson(run.errorJson)
|
|
1362
|
+
})),
|
|
1363
|
+
failedRowExamples: failedExamples.map((row) => ({ ...redactRow(row).row, errorJson: parseJson(row.errorJson) })),
|
|
1364
|
+
appliedFilters: applied,
|
|
1365
|
+
rowCount: Number(count?.count ?? runs.length),
|
|
1366
|
+
returned: runs.length,
|
|
1367
|
+
truncated: args.offset + runs.length < Number(count?.count ?? runs.length),
|
|
1368
|
+
suggestedNextInspection: "Use import_run_detail with one run id, then audit_query for admin.import.* audit events."
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}),
|
|
1372
|
+
operatorTool({
|
|
1373
|
+
name: "import_run_detail",
|
|
1374
|
+
description: "Inspect one admin CSV import run with bounded failed row examples and revert status.",
|
|
1375
|
+
scope: "operator:imports:read",
|
|
1376
|
+
inputSchema: importRunDetailInput,
|
|
1377
|
+
handler: async (args, ctx) => {
|
|
1378
|
+
const db = operatorDb(ctx, options);
|
|
1379
|
+
if (!(await tableExists(db.control, importRunsTableName))) {
|
|
1380
|
+
return toolResult("CSV import history is not installed for this app.", {
|
|
1381
|
+
unavailable: true,
|
|
1382
|
+
appliedFilters: args,
|
|
1383
|
+
rowCount: 0,
|
|
1384
|
+
returned: 0,
|
|
1385
|
+
truncated: false,
|
|
1386
|
+
suggestedNextInspection: "Open /admin/data once after deploying the import feature so the Durable Object bootstrap can create import tables."
|
|
1387
|
+
}, true);
|
|
1388
|
+
}
|
|
1389
|
+
const run = await db.control.first(`SELECT * FROM ${q(importRunsTableName)} WHERE "id" = ?`, [args.runId]);
|
|
1390
|
+
if (!run)
|
|
1391
|
+
return toolResult("Import run not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
1392
|
+
const rows = await db.control.all(`SELECT "id", "runId", "rowNumber", "status", "targetId", "inputJson", "errorJson", "createdAt"
|
|
1393
|
+
FROM ${q(importRowsTableName)}
|
|
1394
|
+
WHERE "runId" = ?
|
|
1395
|
+
ORDER BY "rowNumber" ASC, "createdAt" ASC LIMIT 500`, [args.runId]).catch(() => []);
|
|
1396
|
+
const auditRows = await db.control.all(`SELECT "id", "action", "targetType", "targetId", "metadata", "createdAt"
|
|
1397
|
+
FROM "_audit"
|
|
1398
|
+
WHERE "targetId" = ? OR "metadata" LIKE ?
|
|
1399
|
+
ORDER BY "createdAt" DESC LIMIT 50`, [args.runId, `%${args.runId}%`]).catch(() => []);
|
|
1400
|
+
return toolResult(`Loaded import run ${args.runId}.`, {
|
|
1401
|
+
run: {
|
|
1402
|
+
...redactRow(run).row,
|
|
1403
|
+
mappingJson: parseJson(run.mappingJson),
|
|
1404
|
+
optionsJson: parseJson(run.optionsJson),
|
|
1405
|
+
summaryJson: parseJson(run.summaryJson),
|
|
1406
|
+
errorJson: parseJson(run.errorJson)
|
|
1407
|
+
},
|
|
1408
|
+
rows: rows.map((row) => ({
|
|
1409
|
+
...redactRow({
|
|
1410
|
+
...row,
|
|
1411
|
+
inputJson: parseJson(row.inputJson),
|
|
1412
|
+
errorJson: parseJson(row.errorJson)
|
|
1413
|
+
}).row
|
|
1414
|
+
})),
|
|
1415
|
+
recentAudit: auditRows.map((row) => ({ ...redactRow(row).row, metadata: parseJson(row.metadata) })),
|
|
1416
|
+
revert: {
|
|
1417
|
+
status: run.status === "reverted" ? "reverted" : Number(run.insertedRows ?? 0) > Number(run.revertedRows ?? 0) ? "available_in_admin" : "not_available",
|
|
1418
|
+
insertedRows: Number(run.insertedRows ?? 0),
|
|
1419
|
+
revertedRows: Number(run.revertedRows ?? 0)
|
|
1420
|
+
},
|
|
1421
|
+
appliedFilters: args,
|
|
1422
|
+
rowCount: Number(run.totalRows ?? rows.length),
|
|
1423
|
+
returned: rows.length,
|
|
1424
|
+
truncated: rows.length >= 500,
|
|
1425
|
+
suggestedNextInspection: "Use app_record_detail for inserted target ids or audit_query for admin.import.* events."
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}),
|
|
1429
|
+
operatorTool({
|
|
1430
|
+
name: "job_runs_query",
|
|
1431
|
+
description: "Query Raybase job runs, registered job names, status counts, and failure summaries.",
|
|
1432
|
+
scope: "operator:jobs:read",
|
|
1433
|
+
inputSchema: jobRunsQueryInput,
|
|
1434
|
+
handler: async (args, ctx) => {
|
|
1435
|
+
const stub = dataStub(ctx.env);
|
|
1436
|
+
const where = [];
|
|
1437
|
+
const params = [];
|
|
1438
|
+
const applied = { ...addTimeRange(where, params, `"createdAt"`, args), limit: args.limit, offset: args.offset };
|
|
1439
|
+
if (args.status) {
|
|
1440
|
+
where.push(`"status" = ?`);
|
|
1441
|
+
params.push(args.status);
|
|
1442
|
+
applied.status = args.status;
|
|
1443
|
+
}
|
|
1444
|
+
if (args.jobName) {
|
|
1445
|
+
where.push(`"jobName" = ?`);
|
|
1446
|
+
params.push(args.jobName);
|
|
1447
|
+
applied.jobName = args.jobName;
|
|
1448
|
+
}
|
|
1449
|
+
if (args.actorId) {
|
|
1450
|
+
where.push(`"actorId" = ?`);
|
|
1451
|
+
params.push(args.actorId);
|
|
1452
|
+
applied.actorId = args.actorId;
|
|
1453
|
+
}
|
|
1454
|
+
if (args.runId) {
|
|
1455
|
+
where.push(`"id" = ?`);
|
|
1456
|
+
params.push(args.runId);
|
|
1457
|
+
applied.runId = args.runId;
|
|
1458
|
+
}
|
|
1459
|
+
if (args.batchId) {
|
|
1460
|
+
where.push(`"batchId" = ?`);
|
|
1461
|
+
params.push(args.batchId);
|
|
1462
|
+
applied.batchId = args.batchId;
|
|
1463
|
+
}
|
|
1464
|
+
params.push(args.limit, args.offset);
|
|
1465
|
+
const runs = await stub.all(`SELECT * FROM "_job_runs" ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`, params).catch(() => []);
|
|
1466
|
+
const counts = await stub.all(`SELECT "status", COUNT(*) as "count" FROM "_job_runs" GROUP BY "status" ORDER BY "status"`).catch(() => []);
|
|
1467
|
+
const failures = runs.filter((run) => ["failed", "denied", "expired"].includes(run.status)).slice(0, 20);
|
|
1468
|
+
return toolResult(`Returned ${runs.length} job run(s).`, {
|
|
1469
|
+
runs: runs.map((run) => redactRow(run).row),
|
|
1470
|
+
statusCounts: counts,
|
|
1471
|
+
registeredJobs: Object.keys(options.jobs ?? {}),
|
|
1472
|
+
databaseSizeBytes: await stub.databaseSize().catch(() => null),
|
|
1473
|
+
failureSummaries: failures.map((run) => ({ id: run.id, jobName: run.jobName, status: run.status, endedAt: run.endedAt, error: parseJson(run.errorJson) })),
|
|
1474
|
+
appliedFilters: applied,
|
|
1475
|
+
rowCount: runs.length,
|
|
1476
|
+
returned: runs.length,
|
|
1477
|
+
truncated: false,
|
|
1478
|
+
suggestedNextInspection: "Use job_run_detail for one failed run."
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}),
|
|
1482
|
+
operatorTool({
|
|
1483
|
+
name: "job_run_detail",
|
|
1484
|
+
description: "Inspect one job run with compact event history and redacted input/result/error fields.",
|
|
1485
|
+
scope: "operator:jobs:read",
|
|
1486
|
+
inputSchema: jobRunDetailInput,
|
|
1487
|
+
handler: async (args, ctx) => {
|
|
1488
|
+
const stub = dataStub(ctx.env);
|
|
1489
|
+
const run = await stub.first(`SELECT * FROM "_job_runs" WHERE "id" = ?`, [args.runId]).catch(() => null);
|
|
1490
|
+
if (!run)
|
|
1491
|
+
return toolResult("Job run not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
1492
|
+
const events = await stub.all(`SELECT * FROM "_job_events" WHERE "runId" = ? ORDER BY "createdAt" ASC LIMIT 500`, [run.id]).catch(() => []);
|
|
1493
|
+
const safeRun = redactRow({
|
|
1494
|
+
...run,
|
|
1495
|
+
inputJson: parseJson(run.inputJson),
|
|
1496
|
+
resultJson: parseJson(run.resultJson),
|
|
1497
|
+
errorJson: parseJson(run.errorJson),
|
|
1498
|
+
artifactJson: parseJson(run.artifactJson),
|
|
1499
|
+
progressJson: parseJson(run.progressJson)
|
|
1500
|
+
});
|
|
1501
|
+
return toolResult(`Loaded job run ${run.id}.`, {
|
|
1502
|
+
run: safeRun.row,
|
|
1503
|
+
events: events.map((event) => redactRow({ ...event, dataJson: parseJson(event.dataJson) }).row),
|
|
1504
|
+
lineage: { retryOfRunId: run.retryOfRunId, batchId: run.batchId ?? null, continuationOfBatchId: run.continuationOfBatchId ?? null },
|
|
1505
|
+
appliedFilters: args,
|
|
1506
|
+
rowCount: 1,
|
|
1507
|
+
returned: 1,
|
|
1508
|
+
truncated: safeRun.truncated,
|
|
1509
|
+
suggestedNextInspection: run.batchId || run.continuationOfBatchId
|
|
1510
|
+
? "Use job_batch_detail with the batch id for siblings and the continuation run."
|
|
1511
|
+
: "Use audit_query with targetId equal to this run id."
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}),
|
|
1515
|
+
operatorTool({
|
|
1516
|
+
name: "job_batches_query",
|
|
1517
|
+
description: "Query Raybase job batches (fan-out and join) with child counts, progress, and continuation run status.",
|
|
1518
|
+
scope: "operator:jobs:read",
|
|
1519
|
+
inputSchema: jobBatchesQueryInput,
|
|
1520
|
+
handler: async (args, ctx) => {
|
|
1521
|
+
const stub = dataStub(ctx.env);
|
|
1522
|
+
const result = await stub.listJobBatches({ status: args.status, name: args.name, key: args.key, limit: args.limit })
|
|
1523
|
+
.catch(() => ({ batches: [], counts: [] }));
|
|
1524
|
+
return toolResult(`Returned ${result.batches.length} job batch(es).`, {
|
|
1525
|
+
batches: result.batches.map(operatorBatchSummary),
|
|
1526
|
+
statusCounts: result.counts,
|
|
1527
|
+
appliedFilters: args,
|
|
1528
|
+
rowCount: result.batches.length,
|
|
1529
|
+
returned: result.batches.length,
|
|
1530
|
+
truncated: result.batches.length >= args.limit,
|
|
1531
|
+
suggestedNextInspection: "Use job_batch_detail for one batch's children and continuation run."
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}),
|
|
1535
|
+
operatorTool({
|
|
1536
|
+
name: "job_batch_detail",
|
|
1537
|
+
description: "Inspect one job batch: counts, a cursor-paginated page of child runs with redacted results and errors, and the continuation run.",
|
|
1538
|
+
scope: "operator:jobs:read",
|
|
1539
|
+
inputSchema: jobBatchDetailInput,
|
|
1540
|
+
handler: async (args, ctx) => {
|
|
1541
|
+
const stub = dataStub(ctx.env);
|
|
1542
|
+
const batch = await stub.getJobBatch(args.batchId).catch(() => null);
|
|
1543
|
+
if (!batch)
|
|
1544
|
+
return toolResult("Job batch not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
1545
|
+
const page = await stub.listJobBatchChildren({ batchId: batch.id, status: args.status, cursor: args.cursor ?? null, limit: args.limit });
|
|
1546
|
+
const continuationRun = batch.continuationRunId
|
|
1547
|
+
? await stub.first(`SELECT * FROM "_job_runs" WHERE "id" = ?`, [batch.continuationRunId]).catch(() => null)
|
|
1548
|
+
: null;
|
|
1549
|
+
return toolResult(`Loaded job batch ${batch.key}.`, {
|
|
1550
|
+
batch: operatorBatchSummary(batch),
|
|
1551
|
+
children: page.children.map((child) => redactRow(child).row),
|
|
1552
|
+
nextCursor: page.cursor,
|
|
1553
|
+
continuationRun: continuationRun
|
|
1554
|
+
? redactRow({ ...continuationRun, inputJson: parseJson(continuationRun.inputJson), resultJson: parseJson(continuationRun.resultJson), errorJson: parseJson(continuationRun.errorJson) }).row
|
|
1555
|
+
: null,
|
|
1556
|
+
appliedFilters: args,
|
|
1557
|
+
rowCount: batch.counts.total,
|
|
1558
|
+
returned: page.children.length,
|
|
1559
|
+
truncated: page.cursor !== null,
|
|
1560
|
+
suggestedNextInspection: page.cursor
|
|
1561
|
+
? "Call job_batch_detail again with cursor set to nextCursor for more children."
|
|
1562
|
+
: "Use job_run_detail for one child or the continuation run."
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}),
|
|
1566
|
+
operatorTool({
|
|
1567
|
+
name: "retrieval_indexes_summary",
|
|
1568
|
+
description: "Summarize declared Raybase retrieval indexes, backend configuration, chunk counts, and last reindex evidence.",
|
|
1569
|
+
scope: "operator:retrieval:read",
|
|
1570
|
+
inputSchema: z.object({}).strict(),
|
|
1571
|
+
handler: async (_args, ctx) => {
|
|
1572
|
+
const db = operatorDb(ctx, options);
|
|
1573
|
+
const summary = await retrievalIndexesSummary({
|
|
1574
|
+
control: db.control,
|
|
1575
|
+
retrieval: options.retrieval,
|
|
1576
|
+
env: ctx.env,
|
|
1577
|
+
appId: options.config.appName
|
|
1578
|
+
});
|
|
1579
|
+
return toolResult(`Returned ${summary.indexes.length} retrieval index(es).`, {
|
|
1580
|
+
...summary,
|
|
1581
|
+
rowCount: summary.indexes.length,
|
|
1582
|
+
returned: summary.indexes.length,
|
|
1583
|
+
truncated: false,
|
|
1584
|
+
suggestedNextInspection: "Use retrieval_index_detail with an indexName for recent errors and reindex runs."
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
}),
|
|
1588
|
+
operatorTool({
|
|
1589
|
+
name: "retrieval_index_detail",
|
|
1590
|
+
description: "Inspect one retrieval index with redacted chunk error summaries and recent reindex runs.",
|
|
1591
|
+
scope: "operator:retrieval:read",
|
|
1592
|
+
inputSchema: retrievalIndexDetailInput,
|
|
1593
|
+
handler: async (args, ctx) => {
|
|
1594
|
+
const db = operatorDb(ctx, options);
|
|
1595
|
+
const detail = await retrievalIndexDetail({
|
|
1596
|
+
control: db.control,
|
|
1597
|
+
retrieval: options.retrieval,
|
|
1598
|
+
indexName: args.indexName,
|
|
1599
|
+
env: ctx.env,
|
|
1600
|
+
appId: options.config.appName
|
|
1601
|
+
});
|
|
1602
|
+
return toolResult(`Loaded retrieval index ${args.indexName}.`, {
|
|
1603
|
+
...detail,
|
|
1604
|
+
recentErrors: detail.recentErrors.map((row) => redactRow(row).row),
|
|
1605
|
+
rowCount: 1,
|
|
1606
|
+
returned: 1,
|
|
1607
|
+
truncated: false,
|
|
1608
|
+
suggestedNextInspection: "Use retrieval_reindex_runs_query for broader reindex history."
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}),
|
|
1612
|
+
operatorTool({
|
|
1613
|
+
name: "retrieval_reindex_runs_query",
|
|
1614
|
+
description: "Query retrieval reindex run history without exposing raw vector ids, values, or metadata.",
|
|
1615
|
+
scope: "operator:retrieval:read",
|
|
1616
|
+
inputSchema: retrievalRunsQueryInput,
|
|
1617
|
+
handler: async (args, ctx) => {
|
|
1618
|
+
const db = operatorDb(ctx, options);
|
|
1619
|
+
const result = await retrievalReindexRunsQuery({
|
|
1620
|
+
control: db.control,
|
|
1621
|
+
indexName: args.indexName,
|
|
1622
|
+
backend: args.backend,
|
|
1623
|
+
namespace: args.namespace,
|
|
1624
|
+
limit: args.limit,
|
|
1625
|
+
offset: args.offset
|
|
1626
|
+
});
|
|
1627
|
+
return toolResult(`Returned ${result.runs.length} retrieval reindex run(s).`, {
|
|
1628
|
+
runs: result.runs.map((row) => ({ ...redactRow(row).row, summaryJson: parseJson(row.summaryJson) })),
|
|
1629
|
+
appliedFilters: args,
|
|
1630
|
+
rowCount: result.runs.length,
|
|
1631
|
+
returned: result.runs.length,
|
|
1632
|
+
truncated: result.runs.length >= args.limit,
|
|
1633
|
+
suggestedNextInspection: "Use retrieval_index_detail for current chunk status counts."
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
}),
|
|
1637
|
+
operatorTool({
|
|
1638
|
+
name: "search_indexes_summary",
|
|
1639
|
+
description: "Summarize SQL full-text search indexes, source tables, document counts, and last reindex evidence.",
|
|
1640
|
+
scope: "operator:search:read",
|
|
1641
|
+
inputSchema: z.object({}).strict(),
|
|
1642
|
+
handler: async (_args, ctx) => {
|
|
1643
|
+
const db = operatorDb(ctx, options);
|
|
1644
|
+
const result = await searchIndexesSummary({ control: db.control, search: options.search });
|
|
1645
|
+
return toolResult(`Found ${result.indexes.length} search index(es).`, {
|
|
1646
|
+
...result,
|
|
1647
|
+
rowCount: result.indexes.length,
|
|
1648
|
+
returned: result.indexes.length,
|
|
1649
|
+
truncated: false,
|
|
1650
|
+
suggestedNextInspection: "Use search_index_detail with an indexName for recent errors and reindex runs."
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}),
|
|
1654
|
+
operatorTool({
|
|
1655
|
+
name: "search_index_detail",
|
|
1656
|
+
description: "Inspect one SQL full-text search index, including source coverage, recent errors, and reindex runs.",
|
|
1657
|
+
scope: "operator:search:read",
|
|
1658
|
+
inputSchema: searchIndexDetailInput,
|
|
1659
|
+
handler: async (args, ctx) => {
|
|
1660
|
+
const db = operatorDb(ctx, options);
|
|
1661
|
+
const detail = await searchIndexDetail({ control: db.control, search: options.search, indexName: args.indexName });
|
|
1662
|
+
return toolResult(`Loaded search index ${args.indexName}.`, {
|
|
1663
|
+
...detail,
|
|
1664
|
+
recentErrors: detail.recentErrors.map((row) => redactRow(row).row),
|
|
1665
|
+
rowCount: 1,
|
|
1666
|
+
returned: 1,
|
|
1667
|
+
truncated: false,
|
|
1668
|
+
suggestedNextInspection: "Use app_data_search with a bounded query to inspect policy-hydrated result snippets."
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}),
|
|
1672
|
+
operatorTool({
|
|
1673
|
+
name: "search_reindex_runs_query",
|
|
1674
|
+
description: "Query SQL full-text search reindex run history without exposing raw document bodies.",
|
|
1675
|
+
scope: "operator:search:read",
|
|
1676
|
+
inputSchema: searchRunsQueryInput,
|
|
1677
|
+
handler: async (args, ctx) => {
|
|
1678
|
+
const db = operatorDb(ctx, options);
|
|
1679
|
+
const result = await searchReindexRunsQuery({
|
|
1680
|
+
control: db.control,
|
|
1681
|
+
indexName: args.indexName,
|
|
1682
|
+
status: args.status,
|
|
1683
|
+
limit: args.limit,
|
|
1684
|
+
offset: args.offset
|
|
1685
|
+
});
|
|
1686
|
+
return toolResult(`Returned ${result.runs.length} search reindex run(s).`, {
|
|
1687
|
+
runs: result.runs.map((row) => ({ ...redactRow(row).row, summaryJson: parseJson(row.summaryJson) })),
|
|
1688
|
+
appliedFilters: args,
|
|
1689
|
+
rowCount: result.runs.length,
|
|
1690
|
+
returned: result.runs.length,
|
|
1691
|
+
truncated: result.runs.length >= args.limit,
|
|
1692
|
+
suggestedNextInspection: "Use search_index_detail for current document status counts."
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
}),
|
|
1696
|
+
operatorTool({
|
|
1697
|
+
name: "mcp_grants_summary",
|
|
1698
|
+
description: "Summarize app-user and operator MCP grants plus recent grant events.",
|
|
1699
|
+
scope: "operator:mcp-grants:read",
|
|
1700
|
+
inputSchema: listInput,
|
|
1701
|
+
handler: async (args, ctx) => {
|
|
1702
|
+
const db = operatorDb(ctx, options);
|
|
1703
|
+
const grants = (await listMcpGrants(db.control)).slice(args.offset, args.offset + args.limit);
|
|
1704
|
+
const events = await db.control.all(`SELECT * FROM "_mcp_grant_events" ORDER BY "createdAt" DESC LIMIT ?`, [Math.min(args.limit, 100)]).catch(() => []);
|
|
1705
|
+
return toolResult(`Returned ${grants.length} MCP grant(s).`, {
|
|
1706
|
+
grants: grants.map((grant) => ({ ...grant, scopes: parseJson(grant.scopes), surface: String(grant.subjectType).includes("operator") ? "operator" : "app" })),
|
|
1707
|
+
counts: {
|
|
1708
|
+
active: grants.filter((grant) => !grant.revokedAt).length,
|
|
1709
|
+
revoked: grants.filter((grant) => grant.revokedAt).length,
|
|
1710
|
+
operator: grants.filter((grant) => String(grant.subjectType).includes("operator")).length,
|
|
1711
|
+
app: grants.filter((grant) => !String(grant.subjectType).includes("operator")).length
|
|
1712
|
+
},
|
|
1713
|
+
recentEvents: events.map((event) => ({ ...redactRow(event).row, metadata: parseJson(event.metadata) })),
|
|
1714
|
+
appliedFilters: { limit: args.limit, offset: args.offset },
|
|
1715
|
+
rowCount: grants.length,
|
|
1716
|
+
returned: grants.length,
|
|
1717
|
+
truncated: false,
|
|
1718
|
+
suggestedNextInspection: "Use audit_query action operator_mcp.tool.call for operator activity."
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}),
|
|
1722
|
+
operatorTool({
|
|
1723
|
+
name: "backup_status",
|
|
1724
|
+
description: "Inspect backup, PITR, audit archive, and storage metadata caveats without creating or downloading backups.",
|
|
1725
|
+
scope: "operator:backups:read",
|
|
1726
|
+
inputSchema: z.object({}).strict(),
|
|
1727
|
+
handler: async (_args, ctx) => {
|
|
1728
|
+
const db = operatorDb(ctx, options);
|
|
1729
|
+
const env = envRecord(ctx.env);
|
|
1730
|
+
const now = new Date();
|
|
1731
|
+
const auditEvents = await db.control.all(`SELECT "id", "action", "targetType", "targetId", "createdAt" FROM "_audit"
|
|
1732
|
+
WHERE "action" IN ('database.backup','database.restore','database.pitr.restore')
|
|
1733
|
+
ORDER BY "createdAt" DESC LIMIT 20`).catch(() => []);
|
|
1734
|
+
return toolResult("Backup status inspected without creating a backup.", {
|
|
1735
|
+
encryptedBackup: {
|
|
1736
|
+
availableFromAdmin: true,
|
|
1737
|
+
archiveFormat: 2,
|
|
1738
|
+
encrypted: true,
|
|
1739
|
+
caveat: options.storage ? "Encrypted backups include file metadata only, not R2 file bytes." : null
|
|
1740
|
+
},
|
|
1741
|
+
pitr: {
|
|
1742
|
+
supported: !pitrUnavailableReason(ctx.env),
|
|
1743
|
+
reason: pitrUnavailableReason(ctx.env),
|
|
1744
|
+
earliestRestoreTime: new Date(now.getTime() - pitrRetentionMs).toISOString()
|
|
1745
|
+
},
|
|
1746
|
+
auditArchive: {
|
|
1747
|
+
enabled: auditArchiveEnabled(String(env.RAYBASE_AUDIT_ARCHIVE_ENABLED ?? "")),
|
|
1748
|
+
r2BindingPresent: Boolean(env.RAYBASE_AUDIT_ARCHIVE)
|
|
1749
|
+
},
|
|
1750
|
+
recentBackupRestoreAuditEvents: auditEvents,
|
|
1751
|
+
appliedFilters: {},
|
|
1752
|
+
rowCount: auditEvents.length,
|
|
1753
|
+
returned: auditEvents.length,
|
|
1754
|
+
truncated: false,
|
|
1755
|
+
suggestedNextInspection: "Use audit_archive_status for archive manifests."
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}),
|
|
1759
|
+
operatorTool({
|
|
1760
|
+
name: "storage_usage_summary",
|
|
1761
|
+
description: "Summarize file metadata counts, aggregate sizes, configured buckets, and storage warnings without downloading bytes.",
|
|
1762
|
+
scope: "operator:storage:read",
|
|
1763
|
+
inputSchema: z.object({}).strict(),
|
|
1764
|
+
handler: async (_args, ctx) => {
|
|
1765
|
+
const db = operatorDb(ctx, options);
|
|
1766
|
+
const exists = await tableExists(db.control, raybaseFilesTableName);
|
|
1767
|
+
const aggregates = exists
|
|
1768
|
+
? await db.control.all(`SELECT "bucket", "status", COUNT(*) as "count", COALESCE(SUM("size"), 0) as "sizeBytes"
|
|
1769
|
+
FROM "_files" GROUP BY "bucket", "status" ORDER BY "bucket", "status"`)
|
|
1770
|
+
: [];
|
|
1771
|
+
const orphanErrors = exists
|
|
1772
|
+
? await db.control.all(`SELECT "bucket", COUNT(*) as "count" FROM "_files"
|
|
1773
|
+
WHERE "deleteError" IS NOT NULL OR "status" IN ('uploading','deleting','quarantined')
|
|
1774
|
+
GROUP BY "bucket"`)
|
|
1775
|
+
: [];
|
|
1776
|
+
return toolResult(`Storage metadata has ${aggregates.length} aggregate row(s).`, {
|
|
1777
|
+
enabled: Boolean(options.storage),
|
|
1778
|
+
r2BindingPresent: Boolean(envRecord(ctx.env).RAYBASE_FILES),
|
|
1779
|
+
buckets: Object.values(options.storage?.buckets ?? {}).map((bucket) => ({
|
|
1780
|
+
name: bucket.name,
|
|
1781
|
+
label: bucket.label ?? null,
|
|
1782
|
+
maxSizeBytes: bucket.maxSizeBytes ?? null,
|
|
1783
|
+
accept: bucket.accept ?? []
|
|
1784
|
+
})),
|
|
1785
|
+
metadataTablePresent: exists,
|
|
1786
|
+
aggregates,
|
|
1787
|
+
indicators: { orphanOrErrorCounts: orphanErrors },
|
|
1788
|
+
appliedFilters: {},
|
|
1789
|
+
rowCount: aggregates.length,
|
|
1790
|
+
returned: aggregates.length,
|
|
1791
|
+
truncated: false,
|
|
1792
|
+
suggestedNextInspection: "Use app audit/file metadata admin views for one file id; v1 does not download bytes."
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
}),
|
|
1796
|
+
operatorTool({
|
|
1797
|
+
name: "app_data_table_summary",
|
|
1798
|
+
description: "List app-defined tables, counts, columns, labels, references, and Operator MCP query safety.",
|
|
1799
|
+
scope: "operator:app-data:read",
|
|
1800
|
+
inputSchema: z.object({}).strict(),
|
|
1801
|
+
handler: async (_args, ctx) => {
|
|
1802
|
+
const db = operatorDb(ctx, options);
|
|
1803
|
+
const tables = await Promise.all(Object.entries(options.schema.tables).map(async ([key, table]) => ({
|
|
1804
|
+
key,
|
|
1805
|
+
name: table.name,
|
|
1806
|
+
label: table.label ?? null,
|
|
1807
|
+
rowCount: await safeCount(db.control, table.name),
|
|
1808
|
+
safeToQuery: true,
|
|
1809
|
+
columns: Object.entries(table.columns).map(([name, column]) => ({
|
|
1810
|
+
name,
|
|
1811
|
+
kind: column.kind,
|
|
1812
|
+
required: Boolean(column.required),
|
|
1813
|
+
primaryKey: Boolean(column.primaryKey),
|
|
1814
|
+
label: column.label ?? null,
|
|
1815
|
+
references: getReferenceName(column)
|
|
1816
|
+
}))
|
|
1817
|
+
})));
|
|
1818
|
+
return toolResult(`Found ${tables.length} app data table(s).`, {
|
|
1819
|
+
tables,
|
|
1820
|
+
appliedFilters: {},
|
|
1821
|
+
rowCount: tables.length,
|
|
1822
|
+
returned: tables.length,
|
|
1823
|
+
truncated: false,
|
|
1824
|
+
suggestedNextInspection: "Use app_data_query with { from, select, where, limit } for bounded relational evidence queries."
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}),
|
|
1828
|
+
operatorTool({
|
|
1829
|
+
name: "app_data_query",
|
|
1830
|
+
description: "Read bounded app table rows through a schema-validated relational JSON AST compiled to parameterized SELECT SQL.",
|
|
1831
|
+
scope: "operator:app-data:read",
|
|
1832
|
+
inputSchema: operatorAppDataQueryInput,
|
|
1833
|
+
handler: async (args, ctx) => {
|
|
1834
|
+
const compiled = compileOperatorAppDataQuery({
|
|
1835
|
+
schema: options.schema,
|
|
1836
|
+
systemTableNames: systemTableNames(options)
|
|
1837
|
+
}, args);
|
|
1838
|
+
const { table } = tableByInput(options.schema, compiled.appliedQuery.from);
|
|
1839
|
+
const db = operatorDb(ctx, options);
|
|
1840
|
+
const [rows, countRow] = await Promise.all([
|
|
1841
|
+
db.control.all(compiled.sql, compiled.params),
|
|
1842
|
+
db.control.first(compiled.countSql, compiled.countParams)
|
|
1843
|
+
]);
|
|
1844
|
+
const safe = redactRows(normalizeSelectedAppRows(table, compiled.selectedColumns, rows));
|
|
1845
|
+
const rowIds = safe.rows.map((row) => row.id).filter(Boolean);
|
|
1846
|
+
return toolResult(`Returned ${safe.rows.length} row(s) from ${table.name}.`, {
|
|
1847
|
+
table: table.name,
|
|
1848
|
+
tablesTouched: compiled.tablesTouched,
|
|
1849
|
+
rows: safe.rows,
|
|
1850
|
+
rowIds,
|
|
1851
|
+
sql: compiled.sql,
|
|
1852
|
+
params: redactCompiledQueryParams(compiled),
|
|
1853
|
+
appliedQuery: compiled.appliedQuery,
|
|
1854
|
+
redactedColumns: safe.redactedColumns,
|
|
1855
|
+
truncatedColumns: safe.truncatedColumns,
|
|
1856
|
+
referenceValidation: compiled.referenceValidation,
|
|
1857
|
+
rowCount: Number(countRow?.count ?? safe.rows.length),
|
|
1858
|
+
returned: safe.rows.length,
|
|
1859
|
+
truncated: safe.truncated || rows.length >= compiled.appliedQuery.limit,
|
|
1860
|
+
suggestedNextInspection: rowIds.length
|
|
1861
|
+
? "Use app_record_detail with one row id for linked evidence."
|
|
1862
|
+
: "Use app_data_table_summary or a narrower app_data_query for additional evidence."
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}),
|
|
1866
|
+
operatorTool({
|
|
1867
|
+
name: "app_data_search",
|
|
1868
|
+
description: "Search app data through Raybase SQL FTS and return bounded, redacted snippets hydrated through app policies.",
|
|
1869
|
+
scope: "operator:app-data:read",
|
|
1870
|
+
inputSchema: appDataSearchInput,
|
|
1871
|
+
handler: async (args, ctx) => {
|
|
1872
|
+
const db = operatorDb(ctx, options);
|
|
1873
|
+
const table = args.table ? tableByInput(options.schema, args.table).table : null;
|
|
1874
|
+
const client = db.for({ user: ctx.user, adminOverride: true });
|
|
1875
|
+
const result = await searchIndex({
|
|
1876
|
+
schema: options.schema,
|
|
1877
|
+
db: client,
|
|
1878
|
+
control: db.control,
|
|
1879
|
+
search: options.search,
|
|
1880
|
+
indexName: args.indexName,
|
|
1881
|
+
query: args.query,
|
|
1882
|
+
limit: args.limit,
|
|
1883
|
+
sourceTables: table ? [table.name] : undefined
|
|
1884
|
+
});
|
|
1885
|
+
const safeResults = result.results.map((entry) => {
|
|
1886
|
+
const safe = redactRow({
|
|
1887
|
+
title: entry.title,
|
|
1888
|
+
url: entry.url,
|
|
1889
|
+
snippet: entry.snippet,
|
|
1890
|
+
metadata: entry.metadata
|
|
1891
|
+
});
|
|
1892
|
+
return {
|
|
1893
|
+
...entry,
|
|
1894
|
+
title: safe.row.title,
|
|
1895
|
+
url: safe.row.url,
|
|
1896
|
+
snippet: safe.row.snippet,
|
|
1897
|
+
metadata: safe.row.metadata
|
|
1898
|
+
};
|
|
1899
|
+
});
|
|
1900
|
+
return toolResult(`Returned ${safeResults.length} app data search result(s).`, {
|
|
1901
|
+
results: safeResults,
|
|
1902
|
+
appliedFilters: { indexName: args.indexName, table: table?.name ?? null, query: args.query, limit: args.limit, offset: args.offset },
|
|
1903
|
+
rowCount: safeResults.length,
|
|
1904
|
+
returned: safeResults.length,
|
|
1905
|
+
truncated: safeResults.length >= args.limit,
|
|
1906
|
+
suggestedNextInspection: "Use app_record_detail with one returned sourceId for full redacted row context."
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}),
|
|
1910
|
+
operatorTool({
|
|
1911
|
+
name: "app_record_detail",
|
|
1912
|
+
description: "Read one app table row by id with redaction, reference labels, and recent audit ids.",
|
|
1913
|
+
scope: "operator:app-data:read",
|
|
1914
|
+
inputSchema: appRecordDetailInput,
|
|
1915
|
+
handler: async (args, ctx) => {
|
|
1916
|
+
const { key, table } = tableByInput(options.schema, args.table);
|
|
1917
|
+
const db = operatorDb(ctx, options);
|
|
1918
|
+
const client = db.for({ user: ctx.user, adminOverride: true });
|
|
1919
|
+
const row = await client.get(key, args.id);
|
|
1920
|
+
if (!row)
|
|
1921
|
+
return toolResult("App record not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
1922
|
+
const safe = redactRow(row);
|
|
1923
|
+
const auditIds = await db.control.all(`SELECT "id", "action", "createdAt" FROM "_audit" WHERE "targetType" = ? AND "targetId" = ? ORDER BY "createdAt" DESC LIMIT 20`, [table.name, args.id]).catch(() => []);
|
|
1924
|
+
return toolResult(`Loaded ${table.name} ${args.id}.`, {
|
|
1925
|
+
table: table.name,
|
|
1926
|
+
row: safe.row,
|
|
1927
|
+
redactedColumns: safe.redactedColumns,
|
|
1928
|
+
truncatedColumns: safe.truncatedColumns,
|
|
1929
|
+
references: Object.entries(table.columns)
|
|
1930
|
+
.map(([name, column]) => ({ column: name, references: getReferenceName(column), value: row[name] }))
|
|
1931
|
+
.filter((entry) => entry.references),
|
|
1932
|
+
recentAudit: auditIds,
|
|
1933
|
+
appliedFilters: args,
|
|
1934
|
+
rowCount: 1,
|
|
1935
|
+
returned: 1,
|
|
1936
|
+
truncated: safe.truncated,
|
|
1937
|
+
suggestedNextInspection: "Use audit_query for the returned recent audit ids."
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}),
|
|
1941
|
+
operatorTool({
|
|
1942
|
+
name: "app_data_create",
|
|
1943
|
+
description: "Create one app-defined table row through schema validation, policies, invariants, realtime/search hooks, and audited admin metadata.",
|
|
1944
|
+
scope: "operator:app-data:write",
|
|
1945
|
+
inputSchema: appDataCreateInput,
|
|
1946
|
+
annotations: { idempotentHint: false },
|
|
1947
|
+
handler: async (args, ctx) => {
|
|
1948
|
+
const { key, table } = tableByInput(options.schema, args.table);
|
|
1949
|
+
const values = inputSchemaForTable(table, "create").parse(args.values);
|
|
1950
|
+
const row = await operatorMutationDb(ctx, options).create(key, values);
|
|
1951
|
+
const safe = redactRow(row);
|
|
1952
|
+
return toolResult(`Created ${table.name} ${String(row.id)}.`, {
|
|
1953
|
+
operation: "create",
|
|
1954
|
+
table: table.name,
|
|
1955
|
+
id: row.id,
|
|
1956
|
+
row: safe.row,
|
|
1957
|
+
redactedColumns: safe.redactedColumns,
|
|
1958
|
+
truncatedColumns: safe.truncatedColumns,
|
|
1959
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
1960
|
+
appliedFilters: { table: table.name },
|
|
1961
|
+
rowCount: 1,
|
|
1962
|
+
returned: 1,
|
|
1963
|
+
truncated: safe.truncated
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
}),
|
|
1967
|
+
operatorTool({
|
|
1968
|
+
name: "app_data_update",
|
|
1969
|
+
description: "Update one app-defined table row through schema validation, policies, invariants, realtime/search hooks, and audited admin metadata.",
|
|
1970
|
+
scope: "operator:app-data:write",
|
|
1971
|
+
inputSchema: appDataUpdateInput,
|
|
1972
|
+
annotations: { idempotentHint: true },
|
|
1973
|
+
handler: async (args, ctx) => {
|
|
1974
|
+
const { key, table } = tableByInput(options.schema, args.table);
|
|
1975
|
+
const patch = inputSchemaForTable(table, "update").partial().parse(args.patch);
|
|
1976
|
+
const row = await operatorMutationDb(ctx, options).update(key, args.id, patch);
|
|
1977
|
+
const safe = redactRow(row);
|
|
1978
|
+
return toolResult(`Updated ${table.name} ${String(row.id)}.`, {
|
|
1979
|
+
operation: "update",
|
|
1980
|
+
table: table.name,
|
|
1981
|
+
id: row.id,
|
|
1982
|
+
row: safe.row,
|
|
1983
|
+
redactedColumns: safe.redactedColumns,
|
|
1984
|
+
truncatedColumns: safe.truncatedColumns,
|
|
1985
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
1986
|
+
appliedFilters: { table: table.name, id: args.id },
|
|
1987
|
+
rowCount: 1,
|
|
1988
|
+
returned: 1,
|
|
1989
|
+
truncated: safe.truncated
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
}),
|
|
1993
|
+
operatorTool({
|
|
1994
|
+
name: "app_data_delete",
|
|
1995
|
+
description: "Delete one app-defined table row after explicit confirmation through policies, realtime/search hooks, and audited admin metadata.",
|
|
1996
|
+
scope: "operator:app-data:delete",
|
|
1997
|
+
inputSchema: appDataDeleteInput,
|
|
1998
|
+
annotations: { destructiveHint: true, idempotentHint: false },
|
|
1999
|
+
handler: async (args, ctx) => {
|
|
2000
|
+
const { key, table } = tableByInput(options.schema, args.table);
|
|
2001
|
+
await operatorMutationDb(ctx, options).delete(key, args.id);
|
|
2002
|
+
return toolResult(`Deleted ${table.name} ${args.id}.`, {
|
|
2003
|
+
operation: "delete",
|
|
2004
|
+
table: table.name,
|
|
2005
|
+
id: args.id,
|
|
2006
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2007
|
+
appliedFilters: { table: table.name, id: args.id, confirm: true },
|
|
2008
|
+
rowCount: 1,
|
|
2009
|
+
returned: 0,
|
|
2010
|
+
truncated: false
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
}),
|
|
2014
|
+
operatorTool({
|
|
2015
|
+
name: "app_setting_set",
|
|
2016
|
+
description: "Set one editable app-scoped Raybase setting override with existing setting validation and audited metadata.",
|
|
2017
|
+
scope: "operator:settings:write",
|
|
2018
|
+
inputSchema: appSettingSetInput,
|
|
2019
|
+
annotations: { idempotentHint: true },
|
|
2020
|
+
handler: async (args, ctx) => {
|
|
2021
|
+
const definitions = options.settings ?? {};
|
|
2022
|
+
if (!Object.keys(definitions).length)
|
|
2023
|
+
throw new Error("No Raybase settings definitions are registered for this app");
|
|
2024
|
+
const definition = definitions[args.key];
|
|
2025
|
+
if (!definition)
|
|
2026
|
+
throw new Error(`Unknown setting: ${args.key}`);
|
|
2027
|
+
if (!canWriteCoreSetting(ctx.user, definition, "app"))
|
|
2028
|
+
throw new Error("Setting is read-only");
|
|
2029
|
+
const settingsAppId = raybaseRuntimeSettingsAppId(ctx.env);
|
|
2030
|
+
const result = await setRaybaseSettingOverride({
|
|
2031
|
+
env: ctx.env,
|
|
2032
|
+
definitions,
|
|
2033
|
+
config: options.config,
|
|
2034
|
+
storage: createSqlSettingsStorage(ctx.control),
|
|
2035
|
+
user: ctx.user,
|
|
2036
|
+
appId: settingsAppId,
|
|
2037
|
+
key: args.key,
|
|
2038
|
+
scope: "app",
|
|
2039
|
+
subjectId: appSettingsSubjectId,
|
|
2040
|
+
value: args.value,
|
|
2041
|
+
updatedBy: ctx.user.id
|
|
2042
|
+
});
|
|
2043
|
+
const metadata = settingAuditMetadata({
|
|
2044
|
+
key: args.key,
|
|
2045
|
+
appId: settingsAppId,
|
|
2046
|
+
scope: "app",
|
|
2047
|
+
subjectId: appSettingsSubjectId,
|
|
2048
|
+
type: result.after.type,
|
|
2049
|
+
sourceBefore: result.before.source,
|
|
2050
|
+
sourceAfter: result.after.source,
|
|
2051
|
+
valueChanged: settingValueChanged(result.before, result.after)
|
|
2052
|
+
});
|
|
2053
|
+
await audit(ctx.control, ctx.user.id, "setting.override.set", "_settings", args.key, mutationAuditMetadata(ctx, metadata));
|
|
2054
|
+
return toolResult(`Set app setting ${args.key}.`, {
|
|
2055
|
+
operation: "set",
|
|
2056
|
+
key: args.key,
|
|
2057
|
+
type: result.after.type,
|
|
2058
|
+
sourceBefore: result.before.source,
|
|
2059
|
+
sourceAfter: result.after.source,
|
|
2060
|
+
valueChanged: metadata.valueChanged,
|
|
2061
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2062
|
+
appliedFilters: { key: args.key, scope: "app" },
|
|
2063
|
+
rowCount: 1,
|
|
2064
|
+
returned: 1,
|
|
2065
|
+
truncated: false
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
}),
|
|
2069
|
+
operatorTool({
|
|
2070
|
+
name: "app_setting_reset",
|
|
2071
|
+
description: "Reset one editable app-scoped Raybase setting override to its default or inherited source with audited metadata.",
|
|
2072
|
+
scope: "operator:settings:write",
|
|
2073
|
+
inputSchema: appSettingResetInput,
|
|
2074
|
+
annotations: { idempotentHint: true },
|
|
2075
|
+
handler: async (args, ctx) => {
|
|
2076
|
+
const definitions = options.settings ?? {};
|
|
2077
|
+
if (!Object.keys(definitions).length)
|
|
2078
|
+
throw new Error("No Raybase settings definitions are registered for this app");
|
|
2079
|
+
const definition = definitions[args.key];
|
|
2080
|
+
if (!definition)
|
|
2081
|
+
throw new Error(`Unknown setting: ${args.key}`);
|
|
2082
|
+
if (!canWriteCoreSetting(ctx.user, definition, "app"))
|
|
2083
|
+
throw new Error("Setting is read-only");
|
|
2084
|
+
const settingsAppId = raybaseRuntimeSettingsAppId(ctx.env);
|
|
2085
|
+
const result = await resetRaybaseSettingOverride({
|
|
2086
|
+
env: ctx.env,
|
|
2087
|
+
definitions,
|
|
2088
|
+
config: options.config,
|
|
2089
|
+
storage: createSqlSettingsStorage(ctx.control),
|
|
2090
|
+
user: ctx.user,
|
|
2091
|
+
appId: settingsAppId,
|
|
2092
|
+
key: args.key,
|
|
2093
|
+
scope: "app",
|
|
2094
|
+
subjectId: appSettingsSubjectId
|
|
2095
|
+
});
|
|
2096
|
+
const metadata = settingAuditMetadata({
|
|
2097
|
+
key: args.key,
|
|
2098
|
+
appId: settingsAppId,
|
|
2099
|
+
scope: "app",
|
|
2100
|
+
subjectId: appSettingsSubjectId,
|
|
2101
|
+
type: result.after.type,
|
|
2102
|
+
sourceBefore: result.before.source,
|
|
2103
|
+
sourceAfter: result.after.source,
|
|
2104
|
+
valueChanged: settingValueChanged(result.before, result.after)
|
|
2105
|
+
});
|
|
2106
|
+
await audit(ctx.control, ctx.user.id, "setting.override.reset", "_settings", args.key, mutationAuditMetadata(ctx, metadata));
|
|
2107
|
+
return toolResult(`Reset app setting ${args.key}.`, {
|
|
2108
|
+
operation: "reset",
|
|
2109
|
+
key: args.key,
|
|
2110
|
+
type: result.after.type,
|
|
2111
|
+
sourceBefore: result.before.source,
|
|
2112
|
+
sourceAfter: result.after.source,
|
|
2113
|
+
valueChanged: metadata.valueChanged,
|
|
2114
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2115
|
+
appliedFilters: { key: args.key, scope: "app" },
|
|
2116
|
+
rowCount: 1,
|
|
2117
|
+
returned: 1,
|
|
2118
|
+
truncated: false
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
}),
|
|
2122
|
+
operatorTool({
|
|
2123
|
+
name: "user_invite_create",
|
|
2124
|
+
description: "Create a normal app-user invite with app roles only; operator roles cannot be assigned and the operation is audited.",
|
|
2125
|
+
scope: "operator:users:invite",
|
|
2126
|
+
inputSchema: userInviteCreateInput,
|
|
2127
|
+
annotations: { idempotentHint: false },
|
|
2128
|
+
handler: async (args, ctx) => {
|
|
2129
|
+
const email = args.email.trim().toLowerCase();
|
|
2130
|
+
const roles = normalizeRoles(args.appRoles?.length ? args.appRoles.map(String) : ["user"]);
|
|
2131
|
+
const invite = await createInvite(ctx.control, authSecret(ctx.env), {
|
|
2132
|
+
email,
|
|
2133
|
+
role: roles.find((role) => role !== "user") || roles[0],
|
|
2134
|
+
roles,
|
|
2135
|
+
operatorRole: null,
|
|
2136
|
+
createdBy: ctx.user.id
|
|
2137
|
+
});
|
|
2138
|
+
const url = new URL(`/invite/${invite.token}`, new URL(ctx.request.url).origin).toString();
|
|
2139
|
+
await audit(ctx.control, ctx.user.id, "invite.create", "_invites", undefined, mutationAuditMetadata(ctx, {
|
|
2140
|
+
email,
|
|
2141
|
+
appRoles: roles,
|
|
2142
|
+
emailQueued: false
|
|
2143
|
+
}));
|
|
2144
|
+
return toolResult(`Created invite for ${email}.`, {
|
|
2145
|
+
operation: "invite",
|
|
2146
|
+
email,
|
|
2147
|
+
appRoles: roles,
|
|
2148
|
+
url,
|
|
2149
|
+
expiresAt: invite.expiresAt,
|
|
2150
|
+
emailQueued: false,
|
|
2151
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2152
|
+
appliedFilters: { email },
|
|
2153
|
+
rowCount: 1,
|
|
2154
|
+
returned: 1,
|
|
2155
|
+
truncated: false
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}),
|
|
2159
|
+
operatorTool({
|
|
2160
|
+
name: "user_deactivate",
|
|
2161
|
+
description: "Deactivate one non-owner user after explicit confirmation, delete their sessions, and audit the lifecycle operation.",
|
|
2162
|
+
scope: "operator:users:lifecycle",
|
|
2163
|
+
inputSchema: userDeactivateInput,
|
|
2164
|
+
annotations: { destructiveHint: true, idempotentHint: false },
|
|
2165
|
+
handler: async (args, ctx) => {
|
|
2166
|
+
const target = await resolveUserTarget(ctx.control, args);
|
|
2167
|
+
const disabledReason = disableUserDisabledReason(ctx.user, target);
|
|
2168
|
+
if (disabledReason)
|
|
2169
|
+
throw new Error(disabledReason);
|
|
2170
|
+
const targetId = String(target.id);
|
|
2171
|
+
const sessionsBefore = await activeSessionCount(ctx.control, targetId);
|
|
2172
|
+
const reason = args.reason?.trim() || "Disabled by Raybase Operator MCP";
|
|
2173
|
+
await ctx.control.run(`UPDATE "_auth_users" SET "banned" = 1, "banReason" = ?, "updatedAt" = ? WHERE "id" = ?`, [reason, Date.now(), targetId]);
|
|
2174
|
+
await ctx.control.run(`DELETE FROM "_auth_sessions" WHERE "userId" = ?`, [targetId]);
|
|
2175
|
+
await audit(ctx.control, ctx.user.id, "user.disable", "user", targetId, mutationAuditMetadata(ctx, {
|
|
2176
|
+
reason,
|
|
2177
|
+
sessionsRevoked: sessionsBefore
|
|
2178
|
+
}));
|
|
2179
|
+
return toolResult(`Deactivated user ${String(target.email)}.`, {
|
|
2180
|
+
operation: "deactivate",
|
|
2181
|
+
user: { id: targetId, email: target.email, disabled: true },
|
|
2182
|
+
sessionsRevoked: sessionsBefore,
|
|
2183
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2184
|
+
appliedFilters: { id: args.id ?? null, email: args.email ?? null, confirm: true },
|
|
2185
|
+
rowCount: 1,
|
|
2186
|
+
returned: 1,
|
|
2187
|
+
truncated: false
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
}),
|
|
2191
|
+
operatorTool({
|
|
2192
|
+
name: "user_reactivate",
|
|
2193
|
+
description: "Reactivate one user account by clearing disabled fields and audit the lifecycle operation.",
|
|
2194
|
+
scope: "operator:users:lifecycle",
|
|
2195
|
+
inputSchema: userReactivateInput,
|
|
2196
|
+
annotations: { idempotentHint: true },
|
|
2197
|
+
handler: async (args, ctx) => {
|
|
2198
|
+
const target = await resolveUserTarget(ctx.control, args);
|
|
2199
|
+
const targetId = String(target.id);
|
|
2200
|
+
await ctx.control.run(`UPDATE "_auth_users" SET "banned" = 0, "banReason" = NULL, "banExpires" = NULL, "updatedAt" = ? WHERE "id" = ?`, [Date.now(), targetId]);
|
|
2201
|
+
await audit(ctx.control, ctx.user.id, "user.enable", "user", targetId, mutationAuditMetadata(ctx));
|
|
2202
|
+
return toolResult(`Reactivated user ${String(target.email)}.`, {
|
|
2203
|
+
operation: "reactivate",
|
|
2204
|
+
user: { id: targetId, email: target.email, disabled: false },
|
|
2205
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2206
|
+
appliedFilters: { id: args.id ?? null, email: args.email ?? null },
|
|
2207
|
+
rowCount: 1,
|
|
2208
|
+
returned: 1,
|
|
2209
|
+
truncated: false
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}),
|
|
2213
|
+
operatorTool({
|
|
2214
|
+
name: "user_sessions_revoke",
|
|
2215
|
+
description: "Revoke sessions for one user after explicit confirmation and audit the lifecycle operation.",
|
|
2216
|
+
scope: "operator:users:lifecycle",
|
|
2217
|
+
inputSchema: userSessionsRevokeInput,
|
|
2218
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
2219
|
+
handler: async (args, ctx) => {
|
|
2220
|
+
const target = await resolveUserTarget(ctx.control, args);
|
|
2221
|
+
const targetId = String(target.id);
|
|
2222
|
+
const sessionsBefore = await activeSessionCount(ctx.control, targetId);
|
|
2223
|
+
await ctx.control.run(`DELETE FROM "_auth_sessions" WHERE "userId" = ?`, [targetId]);
|
|
2224
|
+
await audit(ctx.control, ctx.user.id, "user.sessions.revoke", "user", targetId, mutationAuditMetadata(ctx, {
|
|
2225
|
+
reason: args.reason?.trim() || null,
|
|
2226
|
+
sessionsRevoked: sessionsBefore
|
|
2227
|
+
}));
|
|
2228
|
+
return toolResult(`Revoked ${sessionsBefore} session(s) for ${String(target.email)}.`, {
|
|
2229
|
+
operation: "sessions_revoke",
|
|
2230
|
+
user: { id: targetId, email: target.email },
|
|
2231
|
+
sessionsRevoked: sessionsBefore,
|
|
2232
|
+
audit: { mcpGrantId: ctx.mcp.grantId, mcpClientId: ctx.mcp.clientId },
|
|
2233
|
+
appliedFilters: { id: args.id ?? null, email: args.email ?? null, confirm: true },
|
|
2234
|
+
rowCount: 1,
|
|
2235
|
+
returned: 1,
|
|
2236
|
+
truncated: false
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}),
|
|
2240
|
+
{
|
|
2241
|
+
name: "agent_conversations_query",
|
|
2242
|
+
description: "Query Raybase Agent conversation catalog rows with lifecycle, principal, route, surface, and organization filters.",
|
|
2243
|
+
inputSchema: agentConversationsQueryInput,
|
|
2244
|
+
annotations: { readOnlyHint: true },
|
|
2245
|
+
scopes: [...agentReadScopes],
|
|
2246
|
+
handler: async (rawArgs, ctx) => {
|
|
2247
|
+
const args = rawArgs;
|
|
2248
|
+
// ctx.control so layered runtimes (Pro) can swap in an org-scoped executor.
|
|
2249
|
+
const control = ctx.control;
|
|
2250
|
+
if (!(await tableExists(control, "_agent_instances"))) {
|
|
2251
|
+
return toolResult("Agent conversation catalog is not installed for this app.", {
|
|
2252
|
+
unavailable: true,
|
|
2253
|
+
provider: "Agent conversations",
|
|
2254
|
+
requiredScope: "operator:agent:read",
|
|
2255
|
+
appliedFilters: args,
|
|
2256
|
+
rowCount: 0,
|
|
2257
|
+
returned: 0,
|
|
2258
|
+
truncated: false,
|
|
2259
|
+
suggestedNextInspection: "Enable a Raybase Agent such as Guide so _agent_instances is registered."
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
const where = [];
|
|
2263
|
+
const params = [];
|
|
2264
|
+
const applied = { ...addTimeRange(where, params, `"createdAt"`, args), limit: args.limit, offset: args.offset };
|
|
2265
|
+
if (args.agentName) {
|
|
2266
|
+
where.push(`"agentName" = ?`);
|
|
2267
|
+
params.push(args.agentName);
|
|
2268
|
+
applied.agentName = args.agentName;
|
|
2269
|
+
}
|
|
2270
|
+
if (args.runtime) {
|
|
2271
|
+
where.push(`"runtime" = ?`);
|
|
2272
|
+
params.push(args.runtime);
|
|
2273
|
+
applied.runtime = args.runtime;
|
|
2274
|
+
}
|
|
2275
|
+
if (args.triggerType) {
|
|
2276
|
+
where.push(`"triggerType" = ?`);
|
|
2277
|
+
params.push(args.triggerType);
|
|
2278
|
+
applied.triggerType = args.triggerType;
|
|
2279
|
+
}
|
|
2280
|
+
if (args.status) {
|
|
2281
|
+
where.push(`"status" = ?`);
|
|
2282
|
+
params.push(args.status);
|
|
2283
|
+
applied.status = args.status;
|
|
2284
|
+
}
|
|
2285
|
+
if (args.lifecycleState) {
|
|
2286
|
+
if (args.lifecycleState === "cleanup")
|
|
2287
|
+
where.push(`"status" IN ('cleanupRequested', 'deleting', 'cleanupError')`);
|
|
2288
|
+
else
|
|
2289
|
+
where.push(`"status" = ?`);
|
|
2290
|
+
if (args.lifecycleState !== "cleanup")
|
|
2291
|
+
params.push(args.lifecycleState);
|
|
2292
|
+
applied.lifecycleState = args.lifecycleState;
|
|
2293
|
+
}
|
|
2294
|
+
if (args.principalKind) {
|
|
2295
|
+
where.push(`"principalKind" = ?`);
|
|
2296
|
+
params.push(args.principalKind);
|
|
2297
|
+
applied.principalKind = args.principalKind;
|
|
2298
|
+
}
|
|
2299
|
+
if (args.userId) {
|
|
2300
|
+
where.push(`"userId" = ?`);
|
|
2301
|
+
params.push(args.userId);
|
|
2302
|
+
applied.userId = args.userId;
|
|
2303
|
+
}
|
|
2304
|
+
if (args.publicScopeHash) {
|
|
2305
|
+
where.push(`"publicScopeHash" = ?`);
|
|
2306
|
+
params.push(args.publicScopeHash);
|
|
2307
|
+
applied.publicScopeHash = args.publicScopeHash;
|
|
2308
|
+
}
|
|
2309
|
+
if (args.organizationId) {
|
|
2310
|
+
where.push(`"organizationId" = ?`);
|
|
2311
|
+
params.push(args.organizationId);
|
|
2312
|
+
applied.organizationId = args.organizationId;
|
|
2313
|
+
}
|
|
2314
|
+
if (args.surface) {
|
|
2315
|
+
where.push(`"surface" = ?`);
|
|
2316
|
+
params.push(args.surface);
|
|
2317
|
+
applied.surface = args.surface;
|
|
2318
|
+
}
|
|
2319
|
+
if (args.route) {
|
|
2320
|
+
where.push(`"route" = ?`);
|
|
2321
|
+
params.push(args.route);
|
|
2322
|
+
applied.route = args.route;
|
|
2323
|
+
}
|
|
2324
|
+
if (args.search) {
|
|
2325
|
+
where.push(`("conversationId" = ? OR "agentTitle" LIKE ? OR "route" LIKE ? OR "metadataJson" LIKE ? OR "lastError" LIKE ?)`);
|
|
2326
|
+
params.push(args.search, `%${args.search}%`, `%${args.search}%`, `%${args.search}%`, `%${args.search}%`);
|
|
2327
|
+
applied.search = args.search;
|
|
2328
|
+
}
|
|
2329
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
2330
|
+
const count = await control.first(`SELECT COUNT(*) as "count" FROM ${q("_agent_instances")} ${whereSql}`, params);
|
|
2331
|
+
const rows = await control.all(`SELECT * FROM ${q("_agent_instances")} ${whereSql}
|
|
2332
|
+
ORDER BY "updatedAt" DESC LIMIT ? OFFSET ?`, [...params, args.limit, args.offset]);
|
|
2333
|
+
return toolResult(`Returned ${rows.length} Agent conversation(s).`, {
|
|
2334
|
+
conversations: rows.map((row) => safeAgentConversation(row)),
|
|
2335
|
+
appliedFilters: applied,
|
|
2336
|
+
rowCount: Number(count?.count ?? rows.length),
|
|
2337
|
+
returned: rows.length,
|
|
2338
|
+
truncated: args.offset + rows.length < Number(count?.count ?? rows.length),
|
|
2339
|
+
suggestedNextInspection: "Use agent_runs_query with a conversationId for per-turn status and error evidence, or agent_conversation_read for bounded active transcript content."
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "agent_conversation_read",
|
|
2345
|
+
description: "Read one Raybase Agent conversation catalog row plus bounded redacted active transcript content.",
|
|
2346
|
+
inputSchema: agentConversationReadInput,
|
|
2347
|
+
annotations: { readOnlyHint: true },
|
|
2348
|
+
scopes: [...agentReadScopes],
|
|
2349
|
+
handler: async (rawArgs, ctx) => {
|
|
2350
|
+
const args = rawArgs;
|
|
2351
|
+
// ctx.control so layered runtimes (Pro) can swap in an org-scoped executor.
|
|
2352
|
+
const control = ctx.control;
|
|
2353
|
+
if (!(await tableExists(control, "_agent_instances"))) {
|
|
2354
|
+
return toolResult("Agent conversation catalog is not installed for this app.", {
|
|
2355
|
+
unavailable: true,
|
|
2356
|
+
provider: "Agent conversations",
|
|
2357
|
+
requiredScope: "operator:agent:read",
|
|
2358
|
+
appliedFilters: args,
|
|
2359
|
+
rowCount: 0,
|
|
2360
|
+
returned: 0,
|
|
2361
|
+
truncated: false,
|
|
2362
|
+
suggestedNextInspection: "Enable a Raybase Agent such as Guide so _agent_instances is registered."
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
const params = [args.conversationId];
|
|
2366
|
+
const agentWhere = args.agentName ? ` AND "agentName" = ?` : "";
|
|
2367
|
+
if (args.agentName)
|
|
2368
|
+
params.push(args.agentName);
|
|
2369
|
+
const instance = await control.first(`SELECT * FROM ${q("_agent_instances")} WHERE "conversationId" = ?${agentWhere} LIMIT 1`, params);
|
|
2370
|
+
if (!instance)
|
|
2371
|
+
return toolResult("Agent conversation not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
2372
|
+
const scopedOrganizationId = ctx.agentConversationScope?.organizationId;
|
|
2373
|
+
if (scopedOrganizationId && instance.organizationId && instance.organizationId !== scopedOrganizationId) {
|
|
2374
|
+
throw new Error("Conversation does not belong to the requested organization");
|
|
2375
|
+
}
|
|
2376
|
+
const transcript = await readActiveAgentTranscript(ctx, instance, { limit: args.limit, offset: args.offset });
|
|
2377
|
+
if (transcript.unavailable) {
|
|
2378
|
+
return toolResult("Agent transcript storage is unavailable.", {
|
|
2379
|
+
conversation: safeAgentConversation(instance),
|
|
2380
|
+
unavailable: true,
|
|
2381
|
+
error: transcript.error,
|
|
2382
|
+
appliedFilters: args,
|
|
2383
|
+
rowCount: 1,
|
|
2384
|
+
returned: 1,
|
|
2385
|
+
truncated: false,
|
|
2386
|
+
suggestedNextInspection: "Check RAYBASE_AI_CHAT_AGENT binding and Agent Durable Object health."
|
|
2387
|
+
}, true);
|
|
2388
|
+
}
|
|
2389
|
+
return toolResult(`Loaded Agent conversation ${instance.conversationId}.`, {
|
|
2390
|
+
conversation: safeAgentConversation(instance, transcript.rowCount ?? null),
|
|
2391
|
+
messages: transcript.messages ?? [],
|
|
2392
|
+
toolEvents: transcript.toolEvents ?? [],
|
|
2393
|
+
sources: transcript.sources ?? [],
|
|
2394
|
+
citations: transcript.citations ?? [],
|
|
2395
|
+
supportMarkers: transcript.supportMarkers ?? [],
|
|
2396
|
+
redaction: {
|
|
2397
|
+
redacted: transcript.redacted === true,
|
|
2398
|
+
truncated: transcript.truncated === true
|
|
2399
|
+
},
|
|
2400
|
+
archiveStatus: instance.status === "archived" ? "archived" : instance.status === "deleted" ? "deleted" : "hot",
|
|
2401
|
+
appliedFilters: args,
|
|
2402
|
+
rowCount: transcript.rowCount ?? 0,
|
|
2403
|
+
returned: transcript.returned ?? 0,
|
|
2404
|
+
truncated: transcript.truncated === true,
|
|
2405
|
+
suggestedNextInspection: "Use agent_runs_query with this conversationId for per-turn status, error, and timing evidence, or audit_query around the conversation updatedAt timestamp for adjacent runtime events."
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
name: "agent_runs_query",
|
|
2411
|
+
description: "Query Raybase Agent run history with per-turn status, error, timing, and trigger evidence.",
|
|
2412
|
+
inputSchema: agentRunsQueryInput,
|
|
2413
|
+
annotations: { readOnlyHint: true },
|
|
2414
|
+
scopes: [...agentReadScopes],
|
|
2415
|
+
handler: async (rawArgs, ctx) => {
|
|
2416
|
+
const args = rawArgs;
|
|
2417
|
+
// ctx.control so layered runtimes (Pro) can swap in an org-scoped executor.
|
|
2418
|
+
const control = ctx.control;
|
|
2419
|
+
if (!(await tableExists(control, "_agent_runs"))) {
|
|
2420
|
+
return toolResult("Agent run history is not installed for this app.", {
|
|
2421
|
+
unavailable: true,
|
|
2422
|
+
provider: "Agent runs",
|
|
2423
|
+
requiredScope: "operator:agent:read",
|
|
2424
|
+
appliedFilters: args,
|
|
2425
|
+
rowCount: 0,
|
|
2426
|
+
returned: 0,
|
|
2427
|
+
truncated: false,
|
|
2428
|
+
suggestedNextInspection: "Enable a Raybase Agent such as Guide so _agent_runs is registered."
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
const where = [];
|
|
2432
|
+
const params = [];
|
|
2433
|
+
const applied = { ...addTimeRange(where, params, `"startedAt"`, args), limit: args.limit, offset: args.offset };
|
|
2434
|
+
for (const [key, column] of Object.entries({
|
|
2435
|
+
conversationId: "conversationId",
|
|
2436
|
+
agentName: "agentName",
|
|
2437
|
+
status: "status",
|
|
2438
|
+
triggerType: "triggerType",
|
|
2439
|
+
runId: "runId"
|
|
2440
|
+
})) {
|
|
2441
|
+
const value = args[key];
|
|
2442
|
+
if (typeof value === "string" && value) {
|
|
2443
|
+
where.push(`${q(column)} = ?`);
|
|
2444
|
+
params.push(value);
|
|
2445
|
+
applied[key] = value;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
2449
|
+
const count = await control.first(`SELECT COUNT(*) as "count" FROM ${q("_agent_runs")} ${whereSql}`, params);
|
|
2450
|
+
const rows = await control.all(`SELECT * FROM ${q("_agent_runs")} ${whereSql} ORDER BY "startedAt" DESC LIMIT ? OFFSET ?`, [...params, args.limit, args.offset]);
|
|
2451
|
+
return toolResult(`Returned ${rows.length} Agent run(s).`, {
|
|
2452
|
+
runs: rows.map(agentRunListRow),
|
|
2453
|
+
appliedFilters: applied,
|
|
2454
|
+
rowCount: Number(count?.count ?? rows.length),
|
|
2455
|
+
returned: rows.length,
|
|
2456
|
+
truncated: args.offset + rows.length < Number(count?.count ?? rows.length),
|
|
2457
|
+
suggestedNextInspection: "Use ai_usage_query with the conversationId and the run's startedAt/finishedAt window for per-step model evidence, or agent_conversation_read for transcript content."
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2461
|
+
operatorTool({
|
|
2462
|
+
name: "ai_usage_query",
|
|
2463
|
+
description: "Query content-free AI usage events (model, request kind, sizes with the agent prompt floor/tail split, timing, status, gateway log ids) without message text.",
|
|
2464
|
+
scope: "operator:ai:read",
|
|
2465
|
+
inputSchema: aiUsageQueryInput,
|
|
2466
|
+
handler: async (args, ctx) => {
|
|
2467
|
+
// ctx.control so layered runtimes (Pro) can swap in an org-scoped executor.
|
|
2468
|
+
const control = ctx.control;
|
|
2469
|
+
if (!(await tableExists(control, "_ai_usage_events"))) {
|
|
2470
|
+
return toolResult("AI usage evidence is not installed for this app.", {
|
|
2471
|
+
unavailable: true,
|
|
2472
|
+
provider: "AI usage",
|
|
2473
|
+
requiredScope: "operator:ai:read",
|
|
2474
|
+
appliedFilters: args,
|
|
2475
|
+
rowCount: 0,
|
|
2476
|
+
returned: 0,
|
|
2477
|
+
truncated: false,
|
|
2478
|
+
suggestedNextInspection: "Enable Raybase AI (config.ai.enabled) or deploy a version that includes _ai_usage_events."
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
const where = [];
|
|
2482
|
+
const params = [];
|
|
2483
|
+
const applied = { ...addTimeRange(where, params, `"createdAt"`, args), limit: args.limit, offset: args.offset };
|
|
2484
|
+
for (const [key, column] of Object.entries({
|
|
2485
|
+
conversationId: "conversationId",
|
|
2486
|
+
agent: "agent",
|
|
2487
|
+
model: "model",
|
|
2488
|
+
requestKind: "requestKind",
|
|
2489
|
+
status: "status",
|
|
2490
|
+
principalKind: "principalKind",
|
|
2491
|
+
serviceName: "serviceName",
|
|
2492
|
+
userId: "userId"
|
|
2493
|
+
})) {
|
|
2494
|
+
const value = args[key];
|
|
2495
|
+
if (typeof value === "string" && value) {
|
|
2496
|
+
where.push(`${q(column)} = ?`);
|
|
2497
|
+
params.push(value);
|
|
2498
|
+
applied[key] = value;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
for (const [key, path] of Object.entries({ jobName: "$.jobName", jobRunId: "$.jobRunId" })) {
|
|
2502
|
+
const value = args[key];
|
|
2503
|
+
if (typeof value === "string" && value) {
|
|
2504
|
+
where.push(`json_extract(${q("metadata")}, ?) = ?`);
|
|
2505
|
+
params.push(path, value);
|
|
2506
|
+
applied[key] = value;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
2510
|
+
const count = await control.first(`SELECT COUNT(*) as "count" FROM ${q("_ai_usage_events")} ${whereSql}`, params);
|
|
2511
|
+
const rows = await control.all(`SELECT * FROM ${q("_ai_usage_events")} ${whereSql} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`, [...params, args.limit, args.offset]);
|
|
2512
|
+
return toolResult(`Returned ${rows.length} AI usage event(s).`, {
|
|
2513
|
+
events: rows.map(aiUsageListRow),
|
|
2514
|
+
appliedFilters: applied,
|
|
2515
|
+
rowCount: Number(count?.count ?? rows.length),
|
|
2516
|
+
returned: rows.length,
|
|
2517
|
+
truncated: args.offset + rows.length < Number(count?.count ?? rows.length),
|
|
2518
|
+
suggestedNextInspection: "Use a returned gatewayLogId with your AI gateway or provider generation API for effective request parameters and finish reason, or agent_runs_query for run-level status."
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
}),
|
|
2522
|
+
operatorTool({
|
|
2523
|
+
name: "runtime_errors_summary",
|
|
2524
|
+
description: "Group short-retention server and opt-in client runtime error events by fingerprint, source, route, and release.",
|
|
2525
|
+
scope: "operator:health:read",
|
|
2526
|
+
inputSchema: runtimeErrorsSummaryInput,
|
|
2527
|
+
handler: async (args, ctx) => {
|
|
2528
|
+
// ctx.control so layered runtimes (Pro) can swap in an org-scoped executor.
|
|
2529
|
+
const control = ctx.control;
|
|
2530
|
+
if (!(await tableExists(control, raybaseRuntimeErrorEventsTableName))) {
|
|
2531
|
+
return toolResult("Runtime error evidence is not installed for this app.", {
|
|
2532
|
+
unavailable: true,
|
|
2533
|
+
provider: "Runtime errors",
|
|
2534
|
+
requiredScope: "operator:health:read",
|
|
2535
|
+
appliedFilters: args,
|
|
2536
|
+
rowCount: 0,
|
|
2537
|
+
returned: 0,
|
|
2538
|
+
truncated: false,
|
|
2539
|
+
retentionDays: runtimeErrorRetentionDays(options.config),
|
|
2540
|
+
suggestedNextInspection: "Enable Core runtime error evidence or deploy a version that includes _runtime_error_events."
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
const result = await runtimeErrorSummaryQuery(control, args, options.config);
|
|
2544
|
+
return toolResult(`Returned ${result.groups.length} runtime error group(s).`, {
|
|
2545
|
+
groups: result.groups,
|
|
2546
|
+
appliedFilters: result.appliedFilters,
|
|
2547
|
+
rowCount: result.rowCount,
|
|
2548
|
+
returned: result.returned,
|
|
2549
|
+
truncated: result.truncated,
|
|
2550
|
+
retentionDays: result.retentionDays,
|
|
2551
|
+
suggestedNextInspection: "Use runtime_error_detail with a fingerprint, then inspect audit_query, job_run_detail, support_request_detail, or agent_conversation_read for adjacent evidence."
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
}),
|
|
2555
|
+
operatorTool({
|
|
2556
|
+
name: "runtime_error_detail",
|
|
2557
|
+
description: "Read bounded redacted event samples for one runtime error fingerprint.",
|
|
2558
|
+
scope: "operator:health:read",
|
|
2559
|
+
inputSchema: runtimeErrorDetailInput,
|
|
2560
|
+
handler: async (args, ctx) => {
|
|
2561
|
+
// ctx.control so layered runtimes (Pro) can swap in an org-scoped executor.
|
|
2562
|
+
const control = ctx.control;
|
|
2563
|
+
if (!(await tableExists(control, raybaseRuntimeErrorEventsTableName))) {
|
|
2564
|
+
return toolResult("Runtime error evidence is not installed for this app.", {
|
|
2565
|
+
unavailable: true,
|
|
2566
|
+
provider: "Runtime errors",
|
|
2567
|
+
requiredScope: "operator:health:read",
|
|
2568
|
+
appliedFilters: args,
|
|
2569
|
+
rowCount: 0,
|
|
2570
|
+
returned: 0,
|
|
2571
|
+
truncated: false,
|
|
2572
|
+
suggestedNextInspection: "Enable Core runtime error evidence or deploy a version that includes _runtime_error_events."
|
|
2573
|
+
}, true);
|
|
2574
|
+
}
|
|
2575
|
+
const result = await runtimeErrorDetailQuery(control, args);
|
|
2576
|
+
return toolResult(`Returned ${result.rows.length} runtime error event sample(s).`, {
|
|
2577
|
+
events: result.rows,
|
|
2578
|
+
redaction: {
|
|
2579
|
+
redacted: true,
|
|
2580
|
+
truncated: result.truncated,
|
|
2581
|
+
notes: ["Messages, stack samples, routes, user agents, and metadata are sanitized at insert time."]
|
|
2582
|
+
},
|
|
2583
|
+
appliedFilters: result.appliedFilters,
|
|
2584
|
+
rowCount: result.rowCount,
|
|
2585
|
+
returned: result.returned,
|
|
2586
|
+
truncated: result.truncated,
|
|
2587
|
+
suggestedNextInspection: "Inspect audit_query around event timestamps; use job_run_detail for job routes, support_request_detail for user escalation context, or agent_conversation_read when conversationId metadata is present."
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
}),
|
|
2591
|
+
operatorTool({
|
|
2592
|
+
name: "support_requests_query",
|
|
2593
|
+
description: "Query persisted support request rows with bounded excerpts and conversation/run pointers.",
|
|
2594
|
+
scope: "operator:support:read",
|
|
2595
|
+
inputSchema: supportRequestsQueryInput,
|
|
2596
|
+
handler: async (args, ctx) => {
|
|
2597
|
+
const db = operatorDb(ctx, options);
|
|
2598
|
+
if (!(await tableExists(db.control, raybaseSupportRequestsTableName))) {
|
|
2599
|
+
return toolResult("Support request evidence is not installed for this Raybase app.", {
|
|
2600
|
+
unavailable: true,
|
|
2601
|
+
provider: "Support requests",
|
|
2602
|
+
requiredScope: "operator:support:read",
|
|
2603
|
+
appliedFilters: args,
|
|
2604
|
+
rowCount: 0,
|
|
2605
|
+
returned: 0,
|
|
2606
|
+
truncated: false,
|
|
2607
|
+
suggestedNextInspection: "Enable the Core support module or deploy a version that includes _support_requests."
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
const where = [];
|
|
2611
|
+
const params = [];
|
|
2612
|
+
const applied = { ...addTimeRange(where, params, `"createdAt"`, args), limit: args.limit, offset: args.offset };
|
|
2613
|
+
for (const [key, column] of Object.entries({
|
|
2614
|
+
status: "status",
|
|
2615
|
+
sourceKind: "sourceKind",
|
|
2616
|
+
agentName: "agentName",
|
|
2617
|
+
conversationId: "conversationId",
|
|
2618
|
+
runId: "runId",
|
|
2619
|
+
userId: "userId",
|
|
2620
|
+
organizationId: "organizationId",
|
|
2621
|
+
surface: "surface",
|
|
2622
|
+
route: "route"
|
|
2623
|
+
})) {
|
|
2624
|
+
const value = args[key];
|
|
2625
|
+
if (typeof value === "string" && value) {
|
|
2626
|
+
where.push(`${q(column)} = ?`);
|
|
2627
|
+
params.push(value);
|
|
2628
|
+
applied[key] = value;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (args.search) {
|
|
2632
|
+
where.push(`("subject" LIKE ? OR "body" LIKE ? OR "transcriptSummary" LIKE ? OR "category" LIKE ?)`);
|
|
2633
|
+
params.push(`%${args.search}%`, `%${args.search}%`, `%${args.search}%`, `%${args.search}%`);
|
|
2634
|
+
applied.search = args.search;
|
|
2635
|
+
}
|
|
2636
|
+
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
2637
|
+
const count = await db.control.first(`SELECT COUNT(*) as "count" FROM ${q(raybaseSupportRequestsTableName)} ${whereSql}`, params);
|
|
2638
|
+
const rows = await db.control.all(`SELECT "id", "status", "sourceKind", "sourceId", "conversationId", "agentName", "agentInstanceId", "runId", "requestId", "toolCallId",
|
|
2639
|
+
"userId", "organizationId", "principalKind", "publicIdentity", "surface", "route", "subject", "body", "category", "confidence",
|
|
2640
|
+
"contactEmail", "createdAt", "updatedAt"
|
|
2641
|
+
FROM ${q(raybaseSupportRequestsTableName)}
|
|
2642
|
+
${whereSql}
|
|
2643
|
+
ORDER BY "updatedAt" DESC, "createdAt" DESC
|
|
2644
|
+
LIMIT ? OFFSET ?`, [...params, args.limit, args.offset]);
|
|
2645
|
+
return toolResult(`Returned ${rows.length} support request(s).`, {
|
|
2646
|
+
requests: rows.map(supportRequestListRow),
|
|
2647
|
+
appliedFilters: applied,
|
|
2648
|
+
rowCount: Number(count?.count ?? rows.length),
|
|
2649
|
+
returned: rows.length,
|
|
2650
|
+
truncated: args.offset + rows.length < Number(count?.count ?? rows.length),
|
|
2651
|
+
suggestedNextInspection: "Use support_request_detail with an id, then agent_conversation_read or audit_query for adjacent evidence."
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
}),
|
|
2655
|
+
operatorTool({
|
|
2656
|
+
name: "support_request_detail",
|
|
2657
|
+
description: "Read one persisted support request with bounded redacted body, transcript evidence, sources, and adjacent evidence pointers.",
|
|
2658
|
+
scope: "operator:support:read",
|
|
2659
|
+
inputSchema: supportRequestDetailInput,
|
|
2660
|
+
handler: async (args, ctx) => {
|
|
2661
|
+
const db = operatorDb(ctx, options);
|
|
2662
|
+
if (!(await tableExists(db.control, raybaseSupportRequestsTableName))) {
|
|
2663
|
+
return toolResult("Support request evidence is not installed for this Raybase app.", {
|
|
2664
|
+
unavailable: true,
|
|
2665
|
+
provider: "Support requests",
|
|
2666
|
+
requiredScope: "operator:support:read",
|
|
2667
|
+
appliedFilters: args,
|
|
2668
|
+
rowCount: 0,
|
|
2669
|
+
returned: 0,
|
|
2670
|
+
truncated: false,
|
|
2671
|
+
suggestedNextInspection: "Enable the Core support module or deploy a version that includes _support_requests."
|
|
2672
|
+
}, true);
|
|
2673
|
+
}
|
|
2674
|
+
const row = await db.control.first(`SELECT * FROM ${q(raybaseSupportRequestsTableName)} WHERE "id" = ?`, [args.id]);
|
|
2675
|
+
if (!row)
|
|
2676
|
+
return toolResult("Support request not found.", { appliedFilters: args, rowCount: 0, returned: 0, truncated: false }, true);
|
|
2677
|
+
const auditRows = await db.control.all(`SELECT "id", "action", "targetType", "targetId", "metadata", "createdAt"
|
|
2678
|
+
FROM "_audit"
|
|
2679
|
+
WHERE ("targetType" = ? AND "targetId" = ?) OR "metadata" LIKE ?
|
|
2680
|
+
ORDER BY "createdAt" DESC LIMIT 20`, [raybaseSupportRequestsTableName, args.id, `%${args.id}%`]).catch(() => []);
|
|
2681
|
+
return toolResult(`Loaded support request ${args.id}.`, {
|
|
2682
|
+
supportRequest: supportRequestDetailRow(row, auditRows.map((auditRow) => ({ ...redactRow(auditRow).row, metadata: parseJson(auditRow.metadata) }))),
|
|
2683
|
+
appliedFilters: args,
|
|
2684
|
+
rowCount: 1,
|
|
2685
|
+
returned: 1,
|
|
2686
|
+
truncated: false,
|
|
2687
|
+
suggestedNextInspection: "Use agent_conversation_read for conversationId/run context or audit_query action support.request.create."
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
}),
|
|
2691
|
+
operatorTool({
|
|
2692
|
+
name: "improvement_inbox_query",
|
|
2693
|
+
description: "Query Improvement Inbox findings when the provider is installed; otherwise explain unavailability.",
|
|
2694
|
+
scope: "operator:findings:read",
|
|
2695
|
+
inputSchema: findingQueryInput,
|
|
2696
|
+
handler: async () => unavailableResult("Improvement Inbox", "operator:findings:read")
|
|
2697
|
+
}),
|
|
2698
|
+
operatorTool({
|
|
2699
|
+
name: "finding_detail",
|
|
2700
|
+
description: "Read one Improvement Inbox finding when the provider is installed; otherwise explain unavailability.",
|
|
2701
|
+
scope: "operator:findings:read",
|
|
2702
|
+
inputSchema: findingDetailInput,
|
|
2703
|
+
handler: async () => unavailableResult("Improvement Inbox", "operator:findings:read")
|
|
2704
|
+
})
|
|
2705
|
+
];
|
|
2706
|
+
const providerTools = (options.providers ?? []).flatMap((provider) => provider.tools);
|
|
2707
|
+
const providerToolNames = new Set(providerTools.map((tool) => tool.name));
|
|
2708
|
+
const skillTools = createAgentSkillOperatorTools(options);
|
|
2709
|
+
const remoteMcpTools = createAgentRemoteMcpOperatorTools(options);
|
|
2710
|
+
return [...tools.filter((tool) => !providerToolNames.has(tool.name)), ...skillTools, ...remoteMcpTools, ...providerTools];
|
|
2711
|
+
}
|
|
2712
|
+
function embeddedJson(uri, result) {
|
|
2713
|
+
return {
|
|
2714
|
+
uri,
|
|
2715
|
+
mimeType: "application/json",
|
|
2716
|
+
text: JSON.stringify(result.structuredContent ?? {}, null, 2)
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
function findTool(tools, name) {
|
|
2720
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
2721
|
+
if (!tool)
|
|
2722
|
+
throw new Error(`Operator tool ${name} is not registered`);
|
|
2723
|
+
return tool;
|
|
2724
|
+
}
|
|
2725
|
+
export function createCoreOperatorMcpResources(options, tools) {
|
|
2726
|
+
const resources = [
|
|
2727
|
+
{
|
|
2728
|
+
uri: "raybase://operator/summary",
|
|
2729
|
+
name: "Operator summary",
|
|
2730
|
+
description: "Compact Raybase Operator MCP health summary.",
|
|
2731
|
+
mimeType: "application/json",
|
|
2732
|
+
scopes: ["operator:health:read"],
|
|
2733
|
+
read: async (ctx) => embeddedJson("raybase://operator/summary", await findTool(tools, "app_health_summary").handler({}, ctx))
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
uri: "raybase://schema/summary",
|
|
2737
|
+
name: "Schema summary",
|
|
2738
|
+
description: "Raybase app and system schema summary.",
|
|
2739
|
+
mimeType: "application/json",
|
|
2740
|
+
scopes: ["operator:schema:read"],
|
|
2741
|
+
read: async (ctx) => embeddedJson("raybase://schema/summary", await findTool(tools, "schema_summary").handler({ includeSystem: true }, ctx))
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
uri: "raybase://audit/recent",
|
|
2745
|
+
name: "Recent audit",
|
|
2746
|
+
description: "Recent Raybase audit rows.",
|
|
2747
|
+
mimeType: "application/json",
|
|
2748
|
+
scopes: ["operator:audit:read"],
|
|
2749
|
+
read: async (ctx) => embeddedJson("raybase://audit/recent", await findTool(tools, "audit_query").handler({ limit: 50, offset: 0 }, ctx))
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
uri: "raybase://imports/recent",
|
|
2753
|
+
name: "Recent imports",
|
|
2754
|
+
description: "Recent admin CSV import runs and failed row examples.",
|
|
2755
|
+
mimeType: "application/json",
|
|
2756
|
+
scopes: ["operator:imports:read"],
|
|
2757
|
+
read: async (ctx) => embeddedJson("raybase://imports/recent", await findTool(tools, "import_runs_query").handler({ limit: 50, offset: 0 }, ctx))
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
uri: "raybase://jobs/failures",
|
|
2761
|
+
name: "Job failures",
|
|
2762
|
+
description: "Recent failed, denied, or expired job runs.",
|
|
2763
|
+
mimeType: "application/json",
|
|
2764
|
+
scopes: ["operator:jobs:read"],
|
|
2765
|
+
read: async (ctx) => embeddedJson("raybase://jobs/failures", await findTool(tools, "job_runs_query").handler({ limit: 50, offset: 0, status: "failed" }, ctx))
|
|
2766
|
+
},
|
|
2767
|
+
{
|
|
2768
|
+
uri: "raybase://retrieval/summary",
|
|
2769
|
+
name: "Retrieval summary",
|
|
2770
|
+
description: "Raybase semantic retrieval index status and last reindex evidence.",
|
|
2771
|
+
mimeType: "application/json",
|
|
2772
|
+
scopes: ["operator:retrieval:read"],
|
|
2773
|
+
read: async (ctx) => embeddedJson("raybase://retrieval/summary", await findTool(tools, "retrieval_indexes_summary").handler({}, ctx))
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
uri: "raybase://search/summary",
|
|
2777
|
+
name: "Search summary",
|
|
2778
|
+
description: "Raybase SQL full-text search index status and last reindex evidence.",
|
|
2779
|
+
mimeType: "application/json",
|
|
2780
|
+
scopes: ["operator:search:read"],
|
|
2781
|
+
read: async (ctx) => embeddedJson("raybase://search/summary", await findTool(tools, "search_indexes_summary").handler({}, ctx))
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
uri: "raybase://mcp/grants",
|
|
2785
|
+
name: "MCP grants",
|
|
2786
|
+
description: "Recent app and operator MCP grants.",
|
|
2787
|
+
mimeType: "application/json",
|
|
2788
|
+
scopes: ["operator:mcp-grants:read"],
|
|
2789
|
+
read: async (ctx) => embeddedJson("raybase://mcp/grants", await findTool(tools, "mcp_grants_summary").handler({ limit: 50, offset: 0 }, ctx))
|
|
2790
|
+
},
|
|
2791
|
+
{
|
|
2792
|
+
uri: "raybase://agents/conversations/recent",
|
|
2793
|
+
name: "Recent Agent conversations",
|
|
2794
|
+
description: "Recent Raybase Agent conversation catalog rows with lifecycle metadata.",
|
|
2795
|
+
mimeType: "application/json",
|
|
2796
|
+
scopes: [...agentReadScopes],
|
|
2797
|
+
read: async (ctx) => embeddedJson("raybase://agents/conversations/recent", await findTool(tools, "agent_conversations_query").handler({ limit: 50, offset: 0 }, ctx))
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
uri: "raybase://runtime-errors/recent",
|
|
2801
|
+
name: "Recent runtime errors",
|
|
2802
|
+
description: "Short-retention server and opt-in client runtime error groups.",
|
|
2803
|
+
mimeType: "application/json",
|
|
2804
|
+
scopes: ["operator:health:read"],
|
|
2805
|
+
read: async (ctx) => embeddedJson("raybase://runtime-errors/recent", await findTool(tools, "runtime_errors_summary").handler({ limit: 50, offset: 0 }, ctx))
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
uri: "raybase://support/requests/open",
|
|
2809
|
+
name: "Open support requests",
|
|
2810
|
+
description: "Persisted open support requests with bounded excerpts.",
|
|
2811
|
+
mimeType: "application/json",
|
|
2812
|
+
scopes: ["operator:support:read"],
|
|
2813
|
+
read: async (ctx) => embeddedJson("raybase://support/requests/open", await findTool(tools, "support_requests_query").handler({ limit: 50, offset: 0, status: "open" }, ctx))
|
|
2814
|
+
},
|
|
2815
|
+
{
|
|
2816
|
+
uri: "raybase://findings/open",
|
|
2817
|
+
name: "Open findings",
|
|
2818
|
+
description: "Open Improvement Inbox findings when the provider is installed.",
|
|
2819
|
+
mimeType: "application/json",
|
|
2820
|
+
scopes: ["operator:findings:read"],
|
|
2821
|
+
read: async (ctx) => embeddedJson("raybase://findings/open", await findTool(tools, "improvement_inbox_query").handler({ limit: 50, offset: 0, status: "open" }, ctx))
|
|
2822
|
+
}
|
|
2823
|
+
];
|
|
2824
|
+
const providerResources = (options.providers ?? []).flatMap((provider) => provider.resources ?? []);
|
|
2825
|
+
const providerResourceUris = new Set(providerResources.map((resource) => resource.uri));
|
|
2826
|
+
return [...resources.filter((resource) => !providerResourceUris.has(resource.uri)), ...providerResources];
|
|
2827
|
+
}
|
|
2828
|
+
//# sourceMappingURL=operator-mcp.js.map
|