@opengeni/api-router 0.30.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +3 -0
- package/dist/admission-trace.d.ts +8 -0
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.d.ts +50 -2
- package/dist/app.js +12 -1
- package/dist/artifact-catalog-cursor.d.ts +27 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
- package/dist/auth/managed-auth.d.ts +102 -3
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/organization-user-setup.d.ts +80 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-6TZ5LG3O.js +123948 -0
- package/dist/chunk-6TZ5LG3O.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-subscription-account-conflicts.d.ts +1 -0
- package/dist/claude-subscription-account-usage.d.ts +39 -0
- package/dist/claude-subscription-oauth.d.ts +25 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +44 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +32 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-ownership.d.ts +54 -0
- package/dist/controller-data-plane.d.ts +32 -0
- package/dist/editable-artifact-office-import.d.ts +7 -4
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +10 -18
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/github-access.d.ts +31 -2
- package/dist/http/acting-person.d.ts +24 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +27 -1
- package/dist/http/cors.d.ts +17 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/keyed-admission.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/path-identifier.d.ts +17 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +52 -0
- package/dist/http/sse.d.ts +17 -6
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1129 -166
- package/dist/index.js.map +1 -1
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +29 -3
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +16 -2
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +116 -0
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/integrations/google-drive.d.ts +25 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +2691 -0
- package/dist/integrations/oauth-client.d.ts +178 -18
- package/dist/integrations/oauth-profiles.d.ts +222 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github-repositories.d.ts +84 -0
- package/dist/integrations/personal-github.d.ts +42 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +19 -1
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +367 -12
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/slack-interactions.d.ts +113 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/social-oauth.d.ts +7 -0
- package/dist/interaction-agent-access.d.ts +15 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/action-catalog-types.d.ts +9 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/contract-input.d.ts +11 -0
- package/dist/mcp/documents.d.ts +7 -5
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-input.d.ts +1189 -0
- package/dist/mcp/scheduled-task-view.d.ts +22 -4
- package/dist/mcp/server.d.ts +255 -3
- package/dist/mcp/session-event-view.d.ts +94 -0
- package/dist/mcp/session-view.d.ts +66 -2
- package/dist/mcp/session-wait.d.ts +174 -0
- package/dist/mcp-oauth.d.ts +52 -0
- package/dist/model-catalog.d.ts +11 -31
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +52 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +25 -0
- package/dist/routes/api-keys.d.ts +17 -0
- package/dist/routes/artifact-catalog.d.ts +6 -0
- package/dist/routes/automations.d.ts +19 -0
- package/dist/routes/billing.d.ts +49 -0
- package/dist/routes/browser-sessions.d.ts +71 -3
- package/dist/routes/claude-subscription-accounts.d.ts +3 -0
- package/dist/routes/claude-subscription-oauth.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +146 -0
- package/dist/routes/codex.d.ts +76 -2
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/computer-sessions.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/editable-artifacts.d.ts +2 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/feedback.d.ts +3 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/github-browser-pages.d.ts +12 -0
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/insights-response-cache.d.ts +10 -0
- package/dist/routes/insights-usage.d.ts +12 -0
- package/dist/routes/insights.d.ts +30 -1
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/managed-auth-session-sets.d.ts +26 -0
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-connection-access.d.ts +3 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/organization-integrations.d.ts +6 -0
- package/dist/routes/organization-mcp-connections.d.ts +3 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/organization-sessions.d.ts +26 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +9 -0
- package/dist/routes/sessions.d.ts +44 -6
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/routes/subscription-account-pools.d.ts +34 -0
- package/dist/routes/subscription-pool-access.d.ts +16 -0
- package/dist/routes/supergrok.d.ts +6 -1
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +29 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +19 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +9 -1
- package/dist/sandbox/machines.d.ts +5 -2
- package/dist/sandbox/metrics-ingestion.d.ts +28 -20
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
- package/dist/sandbox/viewer.d.ts +26 -1
- package/dist/sandbox-file-artifacts.d.ts +50 -0
- package/dist/scheduled-task-deletion.d.ts +13 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-MPS2OZD3.js +292 -0
- package/dist/session-event-view-MPS2OZD3.js.map +1 -0
- package/dist/site-session-origin.d.ts +13 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +123 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/package.json +23 -19
- package/src/access-grant-rls.ts +75 -0
- package/src/admission-trace.ts +14 -0
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +1974 -256
- package/src/artifact-catalog-cursor.ts +60 -0
- package/src/auth/managed-auth-attempt-context.ts +70 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +222 -0
- package/src/auth/managed-auth.ts +536 -60
- package/src/auth/managed-email.ts +174 -0
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/auth/new-signups-gate.ts +49 -0
- package/src/auth/organization-user-setup.ts +307 -0
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-subscription-account-conflicts.ts +12 -0
- package/src/claude-subscription-account-usage.ts +48 -0
- package/src/claude-subscription-oauth.ts +362 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +97 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +194 -30
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-ownership.ts +199 -0
- package/src/controller-data-plane.ts +81 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +77 -9
- package/src/editable-artifact-production.ts +23 -7
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +27 -21
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/fatal-process-boundary.ts +246 -0
- package/src/gateway-realtime.ts +1 -1
- package/src/github-access.ts +147 -5
- package/src/http/acting-person.ts +71 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +115 -1
- package/src/http/auth.ts +48 -0
- package/src/http/cors.ts +67 -0
- package/src/http/deprecation.ts +118 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/keyed-admission.ts +29 -0
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/path-identifier.ts +54 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +178 -0
- package/src/http/sse.ts +486 -132
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +157 -52
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +98 -11
- package/src/integrations/atlassian.ts +474 -136
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +354 -64
- package/src/integrations/github-app-connect.ts +435 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +76 -0
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/integrations/google-drive.ts +820 -222
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +48 -0
- package/src/integrations/oauth-client.ts +1356 -459
- package/src/integrations/oauth-profiles.ts +537 -0
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github-repositories.ts +674 -0
- package/src/integrations/personal-github.ts +825 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +250 -22
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +963 -239
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/slack-interactions.ts +3170 -474
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/social-oauth.ts +213 -15
- package/src/interaction-agent-access.ts +48 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/action-catalog-types.ts +9 -0
- package/src/mcp/action-catalog.gen.ts +6696 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/company-profile-agent-admin.ts +198 -0
- package/src/mcp/contract-input.ts +141 -0
- package/src/mcp/documents.ts +65 -377
- package/src/mcp/editable-artifacts.ts +12 -6
- package/src/mcp/files.ts +80 -38
- package/src/mcp/first-party-tool-permissions.ts +305 -0
- package/src/mcp/knowledge-entries.ts +322 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-input.ts +76 -0
- package/src/mcp/scheduled-task-view.ts +44 -3
- package/src/mcp/server.ts +3030 -1267
- package/src/mcp/session-event-view.ts +383 -0
- package/src/mcp/session-view.ts +84 -3
- package/src/mcp/session-wait.ts +665 -0
- package/src/mcp-oauth.ts +1123 -0
- package/src/model-catalog.ts +91 -333
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +397 -0
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +364 -74
- package/src/routes/api-keys.ts +336 -10
- package/src/routes/artifact-catalog.ts +279 -0
- package/src/routes/automations.ts +768 -0
- package/src/routes/billing.ts +461 -46
- package/src/routes/browser-sessions.ts +839 -131
- package/src/routes/canonical-human-identities.ts +90 -14
- package/src/routes/capabilities.ts +107 -6
- package/src/routes/channels.ts +20 -3
- package/src/routes/claude-subscription-accounts.ts +334 -0
- package/src/routes/claude-subscription-oauth.ts +142 -0
- package/src/routes/client-errors.ts +362 -0
- package/src/routes/codex.ts +833 -116
- package/src/routes/company-brain.ts +322 -0
- package/src/routes/company-profile.ts +65 -1
- package/src/routes/computer-sessions.ts +557 -106
- package/src/routes/connect.ts +1938 -0
- package/src/routes/connections.ts +1040 -349
- package/src/routes/documents.ts +488 -287
- package/src/routes/editable-artifacts.ts +79 -9
- package/src/routes/enrollments.ts +135 -39
- package/src/routes/environments.ts +154 -58
- package/src/routes/external-identity-links.ts +201 -0
- package/src/routes/feedback.ts +113 -0
- package/src/routes/files.ts +337 -39
- package/src/routes/github-browser-pages.ts +162 -0
- package/src/routes/github.ts +446 -144
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +153 -19
- package/src/routes/install.ts +133 -38
- package/src/routes/integration-facets.ts +107 -33
- package/src/routes/interaction-resources.ts +30 -3
- package/src/routes/knowledge.ts +488 -0
- package/src/routes/machines.ts +313 -15
- package/src/routes/managed-auth-session-sets.ts +1218 -0
- package/src/routes/managed-onboarding.ts +337 -0
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-connection-access.ts +208 -0
- package/src/routes/organization-integration-policy.ts +116 -0
- package/src/routes/organization-integrations.ts +326 -0
- package/src/routes/organization-mcp-connections.ts +178 -0
- package/src/routes/organization-memberships.ts +1019 -27
- package/src/routes/organization-model-providers.ts +293 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +350 -0
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +370 -0
- package/src/routes/plugins.ts +251 -22
- package/src/routes/pr-review-github.ts +881 -0
- package/src/routes/pr-review.ts +622 -0
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/rigs.ts +167 -50
- package/src/routes/scheduled-tasks.ts +346 -41
- package/src/routes/session-artifact-associations.ts +87 -0
- package/src/routes/session-history-imports.ts +280 -0
- package/src/routes/sessions.ts +2306 -264
- package/src/routes/skill-content.ts +297 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +62 -9
- package/src/routes/social.ts +46 -18
- package/src/routes/subscription-account-pools.ts +436 -0
- package/src/routes/subscription-pool-access.ts +126 -0
- package/src/routes/supergrok.ts +219 -303
- package/src/routes/transcription-recordings.ts +38 -3
- package/src/routes/transcriptions.ts +4 -0
- package/src/routes/usage-allowances.ts +378 -0
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/video-generation.ts +14 -10
- package/src/routes/workspace-artifacts.ts +275 -69
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-learning.ts +159 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +38 -25
- package/src/routes/workspaces.ts +1004 -127
- package/src/sandbox/auth-callout.ts +96 -19
- package/src/sandbox/channel-a.ts +323 -24
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +81 -6
- package/src/sandbox/machines.ts +218 -76
- package/src/sandbox/metrics-ingestion.ts +411 -62
- package/src/sandbox/rematerialize.ts +121 -19
- package/src/sandbox/session-attach-variable-sets.ts +131 -0
- package/src/sandbox/viewer.ts +280 -25
- package/src/sandbox-file-artifacts.ts +413 -0
- package/src/scheduled-task-deletion.ts +138 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +6 -3
- package/src/site-session-origin.ts +48 -0
- package/src/site-uploads.ts +224 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/segmenter.ts +8 -2
- package/src/transcription/service.ts +43 -6
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-artifact-content.ts +144 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/src/workspace-deletion.ts +73 -0
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +870 -0
- package/src/workspace-webhook-dispatch.ts +190 -0
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-2PQMSRCB.js +0 -49759
- package/dist/chunk-2PQMSRCB.js.map +0 -1
- package/dist/routes/packs.d.ts +0 -3
- package/src/routes/packs.ts +0 -729
package/src/routes/sessions.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
|
|
2
|
+
import { UnsupportedLatencyModeError } from "@opengeni/config";
|
|
3
|
+
import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
|
|
4
|
+
import { listSessionEventSlices } from "@opengeni/db/session-event-slices";
|
|
5
|
+
import * as sessionPreviewSchema from "@opengeni/db/schema";
|
|
6
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
7
|
+
import { SessionMessageSearchRequest } from "@opengeni/contracts";
|
|
8
|
+
import { scheduledSessionIds } from "@opengeni/db";
|
|
9
|
+
import { isVerifiedDelegatedHumanAuthorization, withSiteSessionOrigin } from "@opengeni/core";
|
|
10
|
+
import { freezeSessionRealtimeConnectionAccounts } from "@opengeni/core";
|
|
11
|
+
import { resolveSiteSessionOrigin, withOptionalSiteCommandOrigin } from "../site-session-origin";
|
|
12
|
+
import { SandboxRecoveryRequest } from "@opengeni/contracts";
|
|
13
|
+
import { getManagedHumanSandboxRecovery, consentManagedHumanSandboxRecovery } from "@opengeni/core";
|
|
14
|
+
import { SandboxRecoveryConflictError } from "@opengeni/db";
|
|
15
|
+
import { codexAccountJson } from "./codex";
|
|
16
|
+
import { getSessionCodexAccounts } from "@opengeni/db";
|
|
1
17
|
import {
|
|
2
18
|
AcknowledgeStreamRequest,
|
|
3
19
|
ApplySessionGoalRevisionRequest,
|
|
20
|
+
ListSessionGoalRevisionsQuery,
|
|
21
|
+
RejectSessionGoalRevisionRequest,
|
|
22
|
+
RollbackSessionGoalRevisionRequest,
|
|
4
23
|
ActivateCodexRealtimeConnectionRequest,
|
|
5
24
|
AttachViewerRequest,
|
|
6
25
|
BeginSessionRealtimeRequest,
|
|
@@ -9,6 +28,7 @@ import {
|
|
|
9
28
|
GatewayRealtimeConnectRequest,
|
|
10
29
|
ClientSessionEvent,
|
|
11
30
|
CompactSessionContextRequest,
|
|
31
|
+
CreateSessionRequest,
|
|
12
32
|
DeleteSessionQueueItemRequest,
|
|
13
33
|
EditSessionQueueItemRequest,
|
|
14
34
|
EndSessionRealtimeRequest,
|
|
@@ -19,6 +39,7 @@ import {
|
|
|
19
39
|
FsMoveRequest,
|
|
20
40
|
FsReadRequest,
|
|
21
41
|
FsWriteRequest,
|
|
42
|
+
ForkSessionRequest,
|
|
22
43
|
HumanInputRequestStatus,
|
|
23
44
|
GitDiffRequest,
|
|
24
45
|
GitReadBatchRequest,
|
|
@@ -30,38 +51,60 @@ import {
|
|
|
30
51
|
PtyOpenRequest,
|
|
31
52
|
PtyResizeRequest,
|
|
32
53
|
PtyWriteRequest,
|
|
54
|
+
PublishSandboxFileArtifactRequest,
|
|
33
55
|
RenewSessionRealtimeRequest,
|
|
34
56
|
SyncSessionRealtimeLedgerRequest,
|
|
35
57
|
SessionControlRequest,
|
|
58
|
+
SessionRetryRequest,
|
|
36
59
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
37
60
|
SessionEventPayloadMode,
|
|
38
61
|
SessionEventReadDirection,
|
|
39
62
|
SessionEventReadMode,
|
|
40
63
|
SessionEventLatestClass,
|
|
41
64
|
SessionEventResultMode,
|
|
65
|
+
SessionScopeSubjectId,
|
|
42
66
|
SessionEventSemanticClass,
|
|
43
67
|
SessionEventType,
|
|
44
68
|
SessionMcpServerId,
|
|
69
|
+
SaveNewSessionDraftRequest,
|
|
45
70
|
compactSessionEventResult,
|
|
46
71
|
sessionEventLatestClassToSemanticClass,
|
|
47
72
|
SaveComposerDraftRequest,
|
|
73
|
+
SubmitComposerDraftRequest,
|
|
48
74
|
SteerSessionQueueItemRequest,
|
|
49
75
|
SteerSessionMessageRequest,
|
|
50
76
|
TerminalExecRequest,
|
|
51
77
|
UpdateSessionChannelRequest,
|
|
78
|
+
UpdateSessionAttentionRequest,
|
|
79
|
+
UpdateSessionArchiveRequest,
|
|
52
80
|
UpdateSessionPinRequest,
|
|
53
81
|
UpdateSessionGoalRequest,
|
|
54
82
|
UpdateSessionMcpApprovalPolicyRequest,
|
|
55
83
|
UpdateSessionRequest,
|
|
84
|
+
UpdateSessionVariableSetsRequest,
|
|
85
|
+
UpdateSessionVisibilityRequest,
|
|
56
86
|
UpdateSessionToolPolicyRequest,
|
|
87
|
+
UpdateSessionAgentRequest,
|
|
88
|
+
AgentConfigError,
|
|
57
89
|
ViewerHeartbeatRequest,
|
|
58
90
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
91
|
+
WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
|
|
92
|
+
WORK_CLAIM_DISCOVERY_LIMIT,
|
|
93
|
+
WORK_CLAIM_NAMESPACE_MAX_BYTES,
|
|
94
|
+
WORK_DISCOVERY_QUERY_MAX_CHARS,
|
|
95
|
+
WORK_DISCOVERY_RECENT_HOURS_MAX,
|
|
96
|
+
WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
|
|
97
|
+
WorkClaimSubjectType,
|
|
98
|
+
normalizeWorkClaimCanonicalKey,
|
|
99
|
+
normalizeWorkClaimNamespace,
|
|
59
100
|
workspaceControlUtf8Bytes,
|
|
60
101
|
type AccessGrant,
|
|
61
102
|
type AttachViewerResponse,
|
|
62
103
|
type SandboxBackend,
|
|
63
104
|
type LineageNode,
|
|
64
105
|
type Session,
|
|
106
|
+
type SessionStatus,
|
|
107
|
+
type WorkClaimSubjectFilter,
|
|
65
108
|
type SessionGoalRevision,
|
|
66
109
|
type AgentTopologyPageResponse,
|
|
67
110
|
type ErrorCode,
|
|
@@ -70,21 +113,27 @@ import {
|
|
|
70
113
|
type TerminalPtyExitedPayload,
|
|
71
114
|
type TerminalPtyOutputDeltaPayload,
|
|
72
115
|
type TerminalPtyStartedPayload,
|
|
116
|
+
type VariableSet,
|
|
73
117
|
} from "@opengeni/contracts";
|
|
74
118
|
import { streamTokenDegraded } from "@opengeni/config";
|
|
75
119
|
import {
|
|
120
|
+
recordAuditEvent,
|
|
76
121
|
acceptSessionApprovalDecision,
|
|
77
122
|
acceptSessionHumanInputResponse,
|
|
78
123
|
clearSessionGoal,
|
|
79
124
|
clearSessionContext,
|
|
80
125
|
getOpenPtySession,
|
|
81
126
|
getEnrollment,
|
|
127
|
+
getLiveEnrollmentConnection,
|
|
82
128
|
getRetainedProcess,
|
|
83
129
|
getSandbox,
|
|
84
130
|
getSession,
|
|
131
|
+
readActiveSandbox,
|
|
132
|
+
deleteSessionTreeIfQuiescent,
|
|
85
133
|
getSessionEvent,
|
|
86
134
|
getSessionForSubject,
|
|
87
135
|
getSessionGoal,
|
|
136
|
+
getLatestSessionModelContext,
|
|
88
137
|
getSessionHumanInputRequest,
|
|
89
138
|
getSessionGoalWithContinuation,
|
|
90
139
|
getSessionGoalRevision,
|
|
@@ -93,28 +142,34 @@ import {
|
|
|
93
142
|
insertPtySession,
|
|
94
143
|
listSessionEventPage,
|
|
95
144
|
listSessionHumanInputRequests,
|
|
145
|
+
listSessionGoalRevisionPage,
|
|
96
146
|
listSessionGoalRevisions,
|
|
147
|
+
rejectSessionGoalRevisionWithEvent,
|
|
97
148
|
listSessionIdsInGroup,
|
|
98
149
|
listSessionDiscoverySummaries,
|
|
99
150
|
listSessionDiscoveryAncestorPaths,
|
|
100
151
|
listSessionsForSubject,
|
|
152
|
+
listSessionEntriesForSubject,
|
|
101
153
|
getLatestStartedSessionTurn,
|
|
102
154
|
listSessionTurns,
|
|
103
155
|
projectEffectiveControlForRelatedAccess,
|
|
104
156
|
projectSessionForRelatedAccess,
|
|
105
157
|
recordStreamAcknowledgment,
|
|
106
158
|
requestSessionCompaction,
|
|
107
|
-
|
|
108
|
-
withSessionCodexCapacityMutation,
|
|
159
|
+
switchSessionCodexAccount,
|
|
109
160
|
setSessionChannel,
|
|
161
|
+
updateSessionVariableSets,
|
|
110
162
|
ChannelNotFoundError,
|
|
163
|
+
setSessionAttention,
|
|
164
|
+
setSessionArchive,
|
|
111
165
|
setSessionPin,
|
|
166
|
+
SessionAttentionVersionConflictError,
|
|
167
|
+
SessionArchiveVersionConflictError,
|
|
112
168
|
SessionPinVersionConflictError,
|
|
113
169
|
SessionPinAccessError,
|
|
114
170
|
SessionListAccessError,
|
|
115
171
|
SessionListCursorError,
|
|
116
172
|
SessionListCursorExpiredError,
|
|
117
|
-
SessionListSnapshotLimitError,
|
|
118
173
|
decodeSessionListCursor,
|
|
119
174
|
revokeViewer,
|
|
120
175
|
setSessionGoalStatusWithEvent,
|
|
@@ -133,7 +188,15 @@ import {
|
|
|
133
188
|
SessionRealtimeConflictError,
|
|
134
189
|
SessionToolPolicyVersionConflictError,
|
|
135
190
|
SessionContextBusyError,
|
|
191
|
+
SessionVariableSetSelectionUnavailableError,
|
|
192
|
+
SessionCreateConnectionSelectionUnavailableError,
|
|
193
|
+
workspaceControlRequestLockTimeoutMs,
|
|
194
|
+
SessionTenancyAccessError,
|
|
195
|
+
SessionTenancyConflictError,
|
|
196
|
+
SessionTenancyInvalidRequestError,
|
|
197
|
+
SessionTenancyNotActivatedError,
|
|
136
198
|
HumanInputResponseValidationError,
|
|
199
|
+
SkillHumanResponseError,
|
|
137
200
|
latestWorkspaceCapture,
|
|
138
201
|
sessionLatestWorkspaceCapture,
|
|
139
202
|
renewSessionRealtimeInTransaction,
|
|
@@ -147,12 +210,18 @@ import {
|
|
|
147
210
|
type SandboxRetainedProcess,
|
|
148
211
|
type Database,
|
|
149
212
|
type SessionDiscoveryCursor,
|
|
213
|
+
type SessionDiscoveryOrderBy,
|
|
150
214
|
type SessionDiscoveryAncestor,
|
|
151
215
|
} from "@opengeni/db";
|
|
216
|
+
import {
|
|
217
|
+
backgroundCommandActivityForSessions,
|
|
218
|
+
listSessionBackgroundCommands,
|
|
219
|
+
requestSessionBackgroundCommandCancellation,
|
|
220
|
+
} from "@opengeni/db/session-background-commands";
|
|
152
221
|
import {
|
|
153
222
|
appendAndPublishEvents,
|
|
154
223
|
boundSessionEventHttpPage,
|
|
155
|
-
|
|
224
|
+
coalesceSessionEventDeltasWithCoverage,
|
|
156
225
|
publishDurableSessionEvents,
|
|
157
226
|
} from "@opengeni/events";
|
|
158
227
|
import {
|
|
@@ -160,6 +229,10 @@ import {
|
|
|
160
229
|
GatewayRealtimeBrokerError,
|
|
161
230
|
} from "../gateway-realtime";
|
|
162
231
|
import { createXaiRealtimeConnectionSecret, XaiRealtimeBrokerError } from "../xai-realtime";
|
|
232
|
+
import {
|
|
233
|
+
prepareExternalLinkTurnAdmission,
|
|
234
|
+
externalContinuationCommitAuthorizer,
|
|
235
|
+
} from "@opengeni/core";
|
|
163
236
|
import { z, ZodError } from "zod";
|
|
164
237
|
import {
|
|
165
238
|
runConcurrentChannelAReads,
|
|
@@ -173,24 +246,35 @@ import {
|
|
|
173
246
|
NatsControlRpc,
|
|
174
247
|
negotiateCapabilities,
|
|
175
248
|
negotiateSelfhostedCapabilities,
|
|
249
|
+
resolveConnectedMachineWorkspaceRoot,
|
|
250
|
+
selectBackend,
|
|
176
251
|
SelfhostedSession,
|
|
177
252
|
} from "@opengeni/runtime/sandbox";
|
|
178
253
|
import type { Context, Hono, MiddlewareHandler } from "hono";
|
|
179
254
|
import { HTTPException } from "hono/http-exception";
|
|
180
255
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
181
256
|
import {
|
|
257
|
+
getManagedAuthRequestActorEpoch,
|
|
182
258
|
hasPermission,
|
|
183
259
|
requireAccessGrant,
|
|
260
|
+
requireAccessGrantAuthorization,
|
|
261
|
+
requireFreshAccessGrant,
|
|
262
|
+
rotateSessionMcpCredentialsForRequest,
|
|
263
|
+
hasVerifiedOwningUserAuthorization,
|
|
184
264
|
requirePermission,
|
|
185
265
|
requireSessionAuthorization,
|
|
186
266
|
requireSessionAuthorizationListScope,
|
|
267
|
+
resolveWorkspaceCatalogSettings,
|
|
187
268
|
withResolvedSessionAuthorization,
|
|
188
269
|
SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
189
270
|
SessionAuthorizationDeniedError,
|
|
190
271
|
SessionAuthorizationUnavailableError,
|
|
272
|
+
SessionTenancyManagedHumanRequiredError,
|
|
273
|
+
SessionTenancyPersistenceOutcomeUnknownError,
|
|
191
274
|
type ResolvedSessionAuthorization,
|
|
192
275
|
} from "@opengeni/core";
|
|
193
276
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
277
|
+
import { SessionRetryConflictError } from "@opengeni/db";
|
|
194
278
|
import {
|
|
195
279
|
attachViewer,
|
|
196
280
|
detachViewer,
|
|
@@ -207,12 +291,16 @@ import {
|
|
|
207
291
|
import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../codex-realtime";
|
|
208
292
|
import {
|
|
209
293
|
acceptSessionUserMessage,
|
|
294
|
+
validateSessionMcpCredentialUpdates,
|
|
210
295
|
controlHumanSessionWorkstream,
|
|
296
|
+
retryFailedSession,
|
|
211
297
|
createSessionForRequest,
|
|
212
298
|
deleteHumanQueuePrompt,
|
|
213
299
|
editHumanQueuePrompt,
|
|
214
300
|
getActorNewSessionDraft,
|
|
301
|
+
getManagedHumanSessionCreateCapabilities,
|
|
215
302
|
getHumanComposerDraft,
|
|
303
|
+
forkManagedHumanSession,
|
|
216
304
|
moveHumanQueuePrompt,
|
|
217
305
|
readSessionLineage,
|
|
218
306
|
saveHumanComposerDraft,
|
|
@@ -220,27 +308,51 @@ import {
|
|
|
220
308
|
SessionSpawnDeniedError,
|
|
221
309
|
sessionSpawnDenialEnvelope,
|
|
222
310
|
steerHumanQueuePrompt,
|
|
311
|
+
submitComposerDraftForRequest,
|
|
223
312
|
updateSessionMcpApprovalPolicy,
|
|
313
|
+
updateManagedHumanSessionVisibility,
|
|
224
314
|
updateSessionToolPolicy,
|
|
315
|
+
updateSessionAgent,
|
|
225
316
|
updateSessionTitle,
|
|
226
317
|
workflowIdForSession,
|
|
227
318
|
sessionWithEffectiveToolPolicy,
|
|
228
|
-
|
|
229
|
-
|
|
319
|
+
workspaceSessionEffectiveToolsContext,
|
|
320
|
+
workspaceSessionToolPolicyContext,
|
|
230
321
|
relayConfigFromSettings,
|
|
322
|
+
validateVariableSetAttachment,
|
|
231
323
|
} from "@opengeni/core";
|
|
232
324
|
import { assertSessionExists, boundedLimit } from "../http/common";
|
|
233
|
-
import { sseSessionStream } from "../http/sse";
|
|
325
|
+
import { browserSseDeliveryOptions, sseSessionStream } from "../http/sse";
|
|
234
326
|
import {
|
|
235
327
|
serveWorkspaceCapture,
|
|
236
328
|
serveWorkspaceCaptureFile,
|
|
237
329
|
WorkspaceCaptureManifestCache,
|
|
238
330
|
} from "./workspace-capture";
|
|
331
|
+
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
332
|
+
import { ApiHttpError } from "../http/api-error";
|
|
333
|
+
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
334
|
+
import { recordAcceptedApiAdmission } from "../admission-trace";
|
|
335
|
+
import { parseRequestBody, parseRequestJson } from "../http/request-body";
|
|
336
|
+
import { measureSessionCreatePhase } from "../session-create-observability";
|
|
239
337
|
|
|
240
338
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
241
339
|
|
|
242
340
|
const VIEWER_LIFECYCLE_PERMISSIONS = ["stream:view", "terminal:attach", "files:write"] as const;
|
|
243
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Bounded responder kind recorded on a child's requires-action resolution
|
|
344
|
+
* notice for its parent: a key principal is `api`, a signed agent attempt is
|
|
345
|
+
* `agent_attempt`, and every other HTTP caller is a human. Never a subject id.
|
|
346
|
+
*/
|
|
347
|
+
function childRequiresActionRespondedByKindForGrant(
|
|
348
|
+
grant: Pick<AccessGrant, "principalKind">,
|
|
349
|
+
): "human" | "api" | "agent_attempt" | "system" {
|
|
350
|
+
if (grant.principalKind === "api_key" || grant.principalKind === "configured_key") return "api";
|
|
351
|
+
if (grant.principalKind === "agent_attempt") return "agent_attempt";
|
|
352
|
+
if (grant.principalKind === "service") return "system";
|
|
353
|
+
return "human";
|
|
354
|
+
}
|
|
355
|
+
|
|
244
356
|
function requireViewerLifecyclePermission(grant: AccessGrant): void {
|
|
245
357
|
if (
|
|
246
358
|
VIEWER_LIFECYCLE_PERMISSIONS.some((permission) => hasPermission(grant.permissions, permission))
|
|
@@ -258,9 +370,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
258
370
|
db,
|
|
259
371
|
settings,
|
|
260
372
|
bus,
|
|
373
|
+
objectStorage,
|
|
261
374
|
observability: deps.observability,
|
|
262
375
|
};
|
|
263
376
|
const workspaceCaptureManifestCache = new WorkspaceCaptureManifestCache();
|
|
377
|
+
const withSiteCommandOrigin = <T>(c: Context, workspaceId: string, run: () => Promise<T>) =>
|
|
378
|
+
withOptionalSiteCommandOrigin(
|
|
379
|
+
db,
|
|
380
|
+
workspaceId,
|
|
381
|
+
c.req.header("x-opengeni-site-id"),
|
|
382
|
+
c.req.header("x-opengeni-site-version"),
|
|
383
|
+
run,
|
|
384
|
+
);
|
|
264
385
|
const ptyIdentity = (pty: SandboxOpenPtySessionRow): SandboxPtyProcessIdentity => ({
|
|
265
386
|
leaseId: pty.leaseId,
|
|
266
387
|
sandboxGroupId: pty.sandboxGroupId,
|
|
@@ -317,8 +438,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
317
438
|
message: "pty retained-process identity is stale; reopen the terminal",
|
|
318
439
|
});
|
|
319
440
|
}
|
|
441
|
+
const providerCommand = await getRetainedProviderCommand(db, {
|
|
442
|
+
accountId: ctx.accountId,
|
|
443
|
+
workspaceId: ctx.workspaceId,
|
|
444
|
+
sessionId: ctx.session.id,
|
|
445
|
+
processId: process.id,
|
|
446
|
+
});
|
|
320
447
|
handle.routingSession.adoptRetainedProcess({
|
|
321
|
-
process: {
|
|
448
|
+
process: {
|
|
449
|
+
id: process.id,
|
|
450
|
+
providerSessionId: process.providerSessionId,
|
|
451
|
+
...(providerCommand ? { providerCommand } : {}),
|
|
452
|
+
},
|
|
322
453
|
backend: {
|
|
323
454
|
sandboxId: null,
|
|
324
455
|
leaseEpoch: process.leaseEpoch,
|
|
@@ -406,6 +537,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
406
537
|
if (!operation) {
|
|
407
538
|
throw sessionAuthorizationHttpError(new SessionAuthorizationUnavailableError());
|
|
408
539
|
}
|
|
540
|
+
if (operation === "session.visibility.write" || operation === "session.fork.create") {
|
|
541
|
+
// These product mutations perform their target-free managed-cookie,
|
|
542
|
+
// permission, and activation gates inside core before resolving the
|
|
543
|
+
// target exactly once. Visibility changes remain owner-only; a shared
|
|
544
|
+
// source may be forked by any currently authorized workspace member.
|
|
545
|
+
// Running the generic middleware here would both create a pre-gate
|
|
546
|
+
// existence oracle and double-call the host.
|
|
547
|
+
await next();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
409
550
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
410
551
|
try {
|
|
411
552
|
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
@@ -429,17 +570,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
429
570
|
db,
|
|
430
571
|
settings,
|
|
431
572
|
bus,
|
|
573
|
+
objectStorage,
|
|
432
574
|
...(deps.establishSandboxSession
|
|
433
575
|
? { establishSandboxSession: deps.establishSandboxSession }
|
|
434
576
|
: {}),
|
|
435
577
|
};
|
|
436
578
|
|
|
579
|
+
app.get("/v1/workspaces/:workspaceId/session-tenancy/capabilities", async (c) => {
|
|
580
|
+
const workspaceId = c.req.param("workspaceId");
|
|
581
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
582
|
+
c,
|
|
583
|
+
deps,
|
|
584
|
+
workspaceId,
|
|
585
|
+
"sessions:create",
|
|
586
|
+
);
|
|
587
|
+
return c.json(await getManagedHumanSessionCreateCapabilities(deps, authorization, workspaceId));
|
|
588
|
+
});
|
|
589
|
+
|
|
437
590
|
app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
438
591
|
const workspaceId = c.req.param("workspaceId");
|
|
439
|
-
const
|
|
592
|
+
const authorization = await measureSessionCreatePhase(deps.observability, "authorization", () =>
|
|
593
|
+
requireAccessGrantAuthorization(c, deps, workspaceId, "sessions:create"),
|
|
594
|
+
);
|
|
595
|
+
const grant = authorization.grant;
|
|
440
596
|
let payload: unknown;
|
|
441
597
|
try {
|
|
442
|
-
payload = await
|
|
598
|
+
payload = await measureSessionCreatePhase(deps.observability, "body_read", () =>
|
|
599
|
+
c.req.json(),
|
|
600
|
+
);
|
|
443
601
|
} catch {
|
|
444
602
|
return c.json(
|
|
445
603
|
{
|
|
@@ -451,25 +609,66 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
451
609
|
}
|
|
452
610
|
let session: Session;
|
|
453
611
|
try {
|
|
454
|
-
|
|
612
|
+
CreateSessionRequest.parse(payload);
|
|
613
|
+
const origin = await measureSessionCreatePhase(deps.observability, "site_origin", () =>
|
|
614
|
+
resolveSiteSessionOrigin(
|
|
615
|
+
db,
|
|
616
|
+
workspaceId,
|
|
617
|
+
c.req.header("x-opengeni-site-id"),
|
|
618
|
+
c.req.header("x-opengeni-site-version"),
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
const create = () =>
|
|
622
|
+
createSessionForRequest(deps, grant, workspaceId, payload, authorization);
|
|
623
|
+
session = await measureSessionCreatePhase(deps.observability, "core_create", () =>
|
|
624
|
+
origin ? withSiteSessionOrigin(origin, create) : create(),
|
|
625
|
+
);
|
|
455
626
|
} catch (error) {
|
|
456
627
|
return sessionCreateErrorResponse(c, error);
|
|
457
628
|
}
|
|
458
629
|
// Creation has committed by this point. Keep response projection outside
|
|
459
630
|
// the create-rejection boundary so a post-commit policy read cannot be
|
|
460
631
|
// misreported as though the session itself was rejected.
|
|
461
|
-
return c.json(
|
|
632
|
+
return c.json(
|
|
633
|
+
await measureSessionCreatePhase(deps.observability, "response_projection", () =>
|
|
634
|
+
withEffectivePolicy(deps, workspaceId, grant.subjectId, session),
|
|
635
|
+
),
|
|
636
|
+
202,
|
|
637
|
+
);
|
|
462
638
|
});
|
|
463
639
|
|
|
464
640
|
app.get("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
|
|
465
641
|
const workspaceId = c.req.param("workspaceId");
|
|
466
|
-
const
|
|
467
|
-
|
|
642
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
643
|
+
c,
|
|
644
|
+
deps,
|
|
645
|
+
workspaceId,
|
|
646
|
+
"sessions:read",
|
|
647
|
+
);
|
|
648
|
+
const grant = authorization.grant;
|
|
649
|
+
const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
|
|
650
|
+
accountId: grant.accountId,
|
|
651
|
+
workspaceId,
|
|
652
|
+
});
|
|
653
|
+
return c.json(
|
|
654
|
+
await getActorNewSessionDraft(
|
|
655
|
+
{ settings: catalog.settings, db },
|
|
656
|
+
grant,
|
|
657
|
+
workspaceId,
|
|
658
|
+
authorization,
|
|
659
|
+
),
|
|
660
|
+
);
|
|
468
661
|
});
|
|
469
662
|
|
|
470
663
|
app.put("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
|
|
471
664
|
const workspaceId = c.req.param("workspaceId");
|
|
472
|
-
const
|
|
665
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
666
|
+
c,
|
|
667
|
+
deps,
|
|
668
|
+
workspaceId,
|
|
669
|
+
"sessions:create",
|
|
670
|
+
);
|
|
671
|
+
const grant = authorization.grant;
|
|
473
672
|
let payload: unknown;
|
|
474
673
|
try {
|
|
475
674
|
payload = await c.req.json();
|
|
@@ -483,12 +682,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
483
682
|
);
|
|
484
683
|
}
|
|
485
684
|
try {
|
|
685
|
+
SaveNewSessionDraftRequest.parse(payload);
|
|
686
|
+
const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
|
|
687
|
+
accountId: grant.accountId,
|
|
688
|
+
workspaceId,
|
|
689
|
+
});
|
|
486
690
|
return c.json(
|
|
487
691
|
await saveActorNewSessionDraft(
|
|
488
|
-
{ settings, db, objectStorage },
|
|
692
|
+
{ settings: catalog.settings, db, objectStorage },
|
|
489
693
|
grant,
|
|
490
694
|
workspaceId,
|
|
491
695
|
payload,
|
|
696
|
+
authorization.canonicalManagedHumanSession,
|
|
697
|
+
authorization,
|
|
492
698
|
),
|
|
493
699
|
);
|
|
494
700
|
} catch (error) {
|
|
@@ -515,9 +721,68 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
515
721
|
}
|
|
516
722
|
});
|
|
517
723
|
|
|
724
|
+
app.get("/v1/workspaces/:workspaceId/session-message-search", async (c) => {
|
|
725
|
+
const workspaceId = c.req.param("workspaceId");
|
|
726
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
727
|
+
c,
|
|
728
|
+
deps,
|
|
729
|
+
workspaceId,
|
|
730
|
+
"sessions:read",
|
|
731
|
+
);
|
|
732
|
+
const grant = authorization.grant;
|
|
733
|
+
// A single-session filter is not authority: both searches use the complete
|
|
734
|
+
// live host/agent list scope, plus ordinary private/archive/RLS boundaries.
|
|
735
|
+
let authorizationScope;
|
|
736
|
+
try {
|
|
737
|
+
authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
|
|
738
|
+
} catch (error) {
|
|
739
|
+
throw sessionAuthorizationHttpError(error);
|
|
740
|
+
}
|
|
741
|
+
const raw = c.req.query();
|
|
742
|
+
const parsed = SessionMessageSearchRequest.safeParse({
|
|
743
|
+
...raw,
|
|
744
|
+
...(raw.limit !== undefined ? { limit: Number(raw.limit) } : {}),
|
|
745
|
+
});
|
|
746
|
+
if (!parsed.success)
|
|
747
|
+
throw new HTTPException(400, { message: "Invalid session message search request" });
|
|
748
|
+
try {
|
|
749
|
+
return c.json(
|
|
750
|
+
await searchSessionMessagesForSubject(
|
|
751
|
+
db,
|
|
752
|
+
workspaceId,
|
|
753
|
+
parsed.data,
|
|
754
|
+
{
|
|
755
|
+
subjectId: grant.subjectId,
|
|
756
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
757
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
758
|
+
},
|
|
759
|
+
{ signal: c.req.raw.signal },
|
|
760
|
+
),
|
|
761
|
+
);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (error instanceof SessionListAccessError)
|
|
764
|
+
throw new HTTPException(403, { message: error.message });
|
|
765
|
+
if (error instanceof SessionMessageSearchCursorError)
|
|
766
|
+
throw new HTTPException(400, { message: error.message });
|
|
767
|
+
// Clients are told to cancel superseded searches, so an abort here is a
|
|
768
|
+
// routine client disconnect, never a server failure. Match the existing
|
|
769
|
+
// client-gone convention (mcp/request-abort.ts): 499 with an empty body,
|
|
770
|
+
// which a departed client never reads and monitors never count as 5xx.
|
|
771
|
+
if (c.req.raw.signal.aborted || (error instanceof Error && error.name === "AbortError"))
|
|
772
|
+
return new Response(null, { status: 499 });
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
518
777
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
519
778
|
const workspaceId = c.req.param("workspaceId");
|
|
520
|
-
const
|
|
779
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
780
|
+
c,
|
|
781
|
+
deps,
|
|
782
|
+
workspaceId,
|
|
783
|
+
"sessions:read",
|
|
784
|
+
);
|
|
785
|
+
const grant = authorization.grant;
|
|
521
786
|
let authorizationScope;
|
|
522
787
|
try {
|
|
523
788
|
authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
|
|
@@ -526,18 +791,50 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
526
791
|
}
|
|
527
792
|
const pageView = c.req.query("view") === "page";
|
|
528
793
|
const query = sessionListQuery(c.req.query(), pageView);
|
|
529
|
-
|
|
794
|
+
const projection = c.req.query("projection");
|
|
795
|
+
if (projection !== undefined && projection !== "summary")
|
|
796
|
+
throw new HTTPException(400, { message: 'projection must be "summary"' });
|
|
797
|
+
if (projection && !pageView)
|
|
798
|
+
throw new HTTPException(400, { message: 'summary projection requires view="page"' });
|
|
799
|
+
const summary = projection === "summary";
|
|
800
|
+
let page: Awaited<
|
|
801
|
+
ReturnType<typeof listSessionsForSubject | typeof listSessionEntriesForSubject>
|
|
802
|
+
>;
|
|
530
803
|
try {
|
|
531
|
-
page = await
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
804
|
+
page = await (summary ? listSessionEntriesForSubject : listSessionsForSubject)(
|
|
805
|
+
db,
|
|
806
|
+
workspaceId,
|
|
807
|
+
{
|
|
808
|
+
subjectId: grant.subjectId,
|
|
809
|
+
limit: boundedLimit(query.limit),
|
|
810
|
+
materializeSnapshot: pageView,
|
|
811
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
812
|
+
...(query.search ? { search: query.search } : {}),
|
|
813
|
+
...(query.pinsOnly ? { pinsOnly: true } : {}),
|
|
814
|
+
...(query.includeTotals ? { includeTotals: true } : {}),
|
|
815
|
+
...(query.needsYouOnly ? { needsYouOnly: true } : {}),
|
|
816
|
+
...(query.includePinned === false ? { includePinned: false } : {}),
|
|
817
|
+
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
818
|
+
...(query.sortBy ? { sortBy: query.sortBy } : {}),
|
|
819
|
+
...(query.archiveStatus ? { archiveStatus: query.archiveStatus } : {}),
|
|
820
|
+
...(query.parentSessionId !== undefined
|
|
821
|
+
? { parentSessionId: query.parentSessionId }
|
|
822
|
+
: {}),
|
|
823
|
+
...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
|
|
824
|
+
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
825
|
+
...(query.createdBy ? { createdBy: query.createdBy } : {}),
|
|
826
|
+
...(query.updatedFrom ? { updatedFrom: query.updatedFrom } : {}),
|
|
827
|
+
...(query.updatedBefore ? { updatedBefore: query.updatedBefore } : {}),
|
|
828
|
+
...(query.createdFrom ? { createdFrom: query.createdFrom } : {}),
|
|
829
|
+
...(query.createdBefore ? { createdBefore: query.createdBefore } : {}),
|
|
830
|
+
...(query.scopeSubjectId ? { scopeSubjectId: query.scopeSubjectId } : {}),
|
|
831
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
832
|
+
// A managed human's own personal workspace has no membership row, so
|
|
833
|
+
// the list's removal fence must fall back to the organization-membership
|
|
834
|
+
// pointer, only with verified native or external owning-user provenance.
|
|
835
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
836
|
+
},
|
|
837
|
+
);
|
|
541
838
|
} catch (error) {
|
|
542
839
|
if (error instanceof SessionListAccessError) {
|
|
543
840
|
throw new HTTPException(403, { message: error.message });
|
|
@@ -552,26 +849,65 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
552
849
|
if (error instanceof SessionListCursorError) {
|
|
553
850
|
throw new HTTPException(400, { message: error.message });
|
|
554
851
|
}
|
|
555
|
-
if (error instanceof SessionListSnapshotLimitError) {
|
|
556
|
-
c.header("Retry-After", "5");
|
|
557
|
-
throw new HTTPException(429, { message: error.message });
|
|
558
|
-
}
|
|
559
852
|
throw error;
|
|
560
853
|
}
|
|
561
854
|
// The page body carries this fact directly. Preserve the historical array
|
|
562
855
|
// body for older clients while still making its older-pin omission visible
|
|
563
856
|
// to raw HTTP consumers without changing that response shape.
|
|
564
857
|
c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
858
|
+
const commandActivity = await backgroundCommandActivityForSessions(db, {
|
|
859
|
+
accountId: grant.accountId,
|
|
860
|
+
workspaceId,
|
|
861
|
+
sessionIds: [...page.pinned, ...page.sessions].map((session) => session.id),
|
|
862
|
+
});
|
|
863
|
+
const scheduleTargets =
|
|
864
|
+
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
865
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
866
|
+
? await scheduledSessionIds(
|
|
867
|
+
db,
|
|
868
|
+
workspaceId,
|
|
869
|
+
[...page.pinned, ...page.sessions].map((session) => session.id),
|
|
870
|
+
)
|
|
871
|
+
: new Set<string>();
|
|
872
|
+
if ("projection" in page) {
|
|
873
|
+
const decorateEntry = (session: (typeof page.sessions)[number]) => ({
|
|
874
|
+
...session,
|
|
875
|
+
hasSchedules: scheduleTargets.has(session.id),
|
|
876
|
+
...(commandActivity.has(session.id)
|
|
877
|
+
? { backgroundCommandActivity: commandActivity.get(session.id)! }
|
|
878
|
+
: {}),
|
|
879
|
+
});
|
|
880
|
+
return c.json({
|
|
881
|
+
...page,
|
|
882
|
+
...(query.hasPageFilters ? { filtersApplied: true as const } : {}),
|
|
883
|
+
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
884
|
+
pinned: page.pinned.map(decorateEntry),
|
|
885
|
+
sessions: page.sessions.map(decorateEntry),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId, grant.subjectId, [
|
|
889
|
+
...page.pinned,
|
|
890
|
+
...page.sessions,
|
|
891
|
+
]);
|
|
892
|
+
const decorate = (session: Session): Session => {
|
|
893
|
+
const activity = commandActivity.get(session.id);
|
|
894
|
+
const decorated = {
|
|
895
|
+
...session,
|
|
896
|
+
hasSchedules: scheduleTargets.has(session.id),
|
|
897
|
+
...(activity ? { backgroundCommandActivity: activity } : {}),
|
|
898
|
+
};
|
|
899
|
+
return sessionWithEffectiveToolPolicy(
|
|
900
|
+
decorated,
|
|
569
901
|
policy.workspaceServerIds,
|
|
570
902
|
policy.workspaceDefaultServerIds,
|
|
903
|
+
policy.effectiveToolsContext,
|
|
571
904
|
);
|
|
905
|
+
};
|
|
572
906
|
if (pageView) {
|
|
573
907
|
return c.json({
|
|
574
908
|
...page,
|
|
909
|
+
...(query.hasPageFilters ? { filtersApplied: true as const } : {}),
|
|
910
|
+
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
575
911
|
pinned: page.pinned.map(decorate),
|
|
576
912
|
sessions: page.sessions.map(decorate),
|
|
577
913
|
});
|
|
@@ -594,71 +930,144 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
594
930
|
throw sessionAuthorizationHttpError(error);
|
|
595
931
|
}
|
|
596
932
|
const query = agentTopologyQuery(c.req.query());
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
933
|
+
const startedAtMs = performance.now();
|
|
934
|
+
const mode = query.subject ? "subject" : query.query ? "query" : "browse";
|
|
935
|
+
const metricAuthorizationScope = authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
|
|
936
|
+
if ((query.query || query.subject) && !settings.workDiscoveryEnabled) {
|
|
937
|
+
observeWorkDiscovery(deps.observability, {
|
|
938
|
+
surface: "agent_topology",
|
|
939
|
+
mode,
|
|
940
|
+
outcome: "disabled",
|
|
941
|
+
authorizationScope: metricAuthorizationScope,
|
|
942
|
+
durationMs: performance.now() - startedAtMs,
|
|
943
|
+
responseBytes: 0,
|
|
944
|
+
resultCount: 0,
|
|
945
|
+
overlapCount: 0,
|
|
946
|
+
matchCounts: {},
|
|
947
|
+
});
|
|
948
|
+
throw new HTTPException(503, {
|
|
949
|
+
message: "Agent work discovery is disabled by the operator.",
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const orderBy: SessionDiscoveryOrderBy =
|
|
954
|
+
query.query || query.subject ? "relevance" : "updatedAt";
|
|
955
|
+
const page = await listSessionDiscoverySummaries(db, workspaceId, {
|
|
956
|
+
limit: query.limit,
|
|
957
|
+
orderBy,
|
|
958
|
+
subjectId: grant.subjectId,
|
|
959
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
960
|
+
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
961
|
+
...(query.rootSessionId ? { rootSessionId: query.rootSessionId } : {}),
|
|
962
|
+
...(query.query ? { query: query.query } : {}),
|
|
963
|
+
...(query.statuses ? { statuses: query.statuses } : {}),
|
|
964
|
+
activeOnly: query.activeOnly,
|
|
965
|
+
...(query.recentHours !== undefined ? { recentHours: query.recentHours } : {}),
|
|
966
|
+
...(query.subject ? { subject: query.subject } : {}),
|
|
967
|
+
...(query.claimLimit !== undefined ? { claimLimit: query.claimLimit } : {}),
|
|
968
|
+
includeWorkDiscovery: settings.workDiscoveryEnabled,
|
|
969
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
970
|
+
});
|
|
971
|
+
const ancestorPaths =
|
|
972
|
+
query.query || query.subject
|
|
973
|
+
? await listSessionDiscoveryAncestorPaths(
|
|
974
|
+
db,
|
|
975
|
+
workspaceId,
|
|
976
|
+
page.sessions.map((session) => session.id),
|
|
977
|
+
authorizationScope ?? undefined,
|
|
978
|
+
grant.subjectId,
|
|
979
|
+
)
|
|
980
|
+
: new Map<string, SessionDiscoveryAncestor[]>();
|
|
981
|
+
const sessions: AgentTopologyPageResponse["sessions"] = page.sessions.map((session) => {
|
|
982
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
983
|
+
return {
|
|
984
|
+
id: session.id,
|
|
985
|
+
title: session.title,
|
|
627
986
|
titleTruncated:
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
987
|
+
session.titleOriginalChars !== null &&
|
|
988
|
+
session.titleOriginalChars > Array.from(session.title ?? "").length,
|
|
989
|
+
parentSessionId: session.parentSessionId,
|
|
990
|
+
rootSessionId: session.rootSessionId,
|
|
991
|
+
nestedAgentDepth: session.nestedAgentDepth,
|
|
992
|
+
ancestorPath: (ancestorPaths.get(session.id) ?? []).map((ancestor) => ({
|
|
993
|
+
id: ancestor.id,
|
|
994
|
+
title: ancestor.title,
|
|
995
|
+
titleTruncated:
|
|
996
|
+
ancestor.titleOriginalChars !== null &&
|
|
997
|
+
ancestor.titleOriginalChars > Array.from(ancestor.title ?? "").length,
|
|
998
|
+
})),
|
|
999
|
+
status: session.status,
|
|
1000
|
+
goal: session.goal
|
|
636
1001
|
? {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
1002
|
+
status: session.goal.status,
|
|
1003
|
+
summary: session.goal.text,
|
|
1004
|
+
summaryTruncated:
|
|
1005
|
+
session.goal.textOriginalChars > Array.from(session.goal.text).length,
|
|
642
1006
|
}
|
|
643
1007
|
: null,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1008
|
+
pause: {
|
|
1009
|
+
state: session.effectiveControl.state,
|
|
1010
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
1011
|
+
source: blocker
|
|
1012
|
+
? {
|
|
1013
|
+
kind: blocker.kind,
|
|
1014
|
+
...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
|
|
1015
|
+
displayName: blocker.displayName,
|
|
1016
|
+
displayNameTruncated:
|
|
1017
|
+
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
1018
|
+
}
|
|
1019
|
+
: null,
|
|
1020
|
+
},
|
|
1021
|
+
children: session.treeStats,
|
|
1022
|
+
relatedWork: session.workDiscovery,
|
|
1023
|
+
createdAt: session.createdAt,
|
|
1024
|
+
updatedAt: session.updatedAt,
|
|
1025
|
+
};
|
|
1026
|
+
});
|
|
1027
|
+
const response = {
|
|
1028
|
+
sessions,
|
|
1029
|
+
total: page.total,
|
|
1030
|
+
hasMore: page.hasMore,
|
|
1031
|
+
humanAdvisoriesEnabled:
|
|
1032
|
+
settings.workDiscoveryEnabled && settings.workDiscoveryHumanAdvisoriesEnabled,
|
|
1033
|
+
nextCursor: page.nextCursor
|
|
1034
|
+
? encodeAgentTopologyCursor({
|
|
1035
|
+
cursor: page.nextCursor,
|
|
1036
|
+
parentSessionId: query.parentSessionId === undefined ? "all" : query.parentSessionId,
|
|
1037
|
+
rootSessionId: query.rootSessionId ?? null,
|
|
1038
|
+
query: query.query ?? null,
|
|
1039
|
+
statuses: query.statuses ?? [],
|
|
1040
|
+
activeOnly: query.activeOnly,
|
|
1041
|
+
recentHours: query.recentHours ?? null,
|
|
1042
|
+
subject: query.subject ?? null,
|
|
1043
|
+
claimLimit: query.claimLimit ?? null,
|
|
1044
|
+
})
|
|
1045
|
+
: null,
|
|
1046
|
+
} satisfies AgentTopologyPageResponse;
|
|
1047
|
+
observeWorkDiscovery(deps.observability, {
|
|
1048
|
+
surface: "agent_topology",
|
|
1049
|
+
mode,
|
|
1050
|
+
outcome: sessions.length === 0 ? "empty" : "ok",
|
|
1051
|
+
authorizationScope: metricAuthorizationScope,
|
|
1052
|
+
durationMs: performance.now() - startedAtMs,
|
|
1053
|
+
responseBytes: Buffer.byteLength(JSON.stringify(response), "utf8"),
|
|
1054
|
+
...summarizeWorkDiscoveryRows(sessions),
|
|
1055
|
+
});
|
|
1056
|
+
return c.json(response);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
observeWorkDiscovery(deps.observability, {
|
|
1059
|
+
surface: "agent_topology",
|
|
1060
|
+
mode,
|
|
1061
|
+
outcome: "error",
|
|
1062
|
+
authorizationScope: metricAuthorizationScope,
|
|
1063
|
+
durationMs: performance.now() - startedAtMs,
|
|
1064
|
+
responseBytes: 0,
|
|
1065
|
+
resultCount: 0,
|
|
1066
|
+
overlapCount: 0,
|
|
1067
|
+
matchCounts: {},
|
|
1068
|
+
});
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
662
1071
|
});
|
|
663
1072
|
|
|
664
1073
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
@@ -678,7 +1087,143 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
678
1087
|
if (!session) {
|
|
679
1088
|
throw new HTTPException(404, { message: "session not found" });
|
|
680
1089
|
}
|
|
681
|
-
|
|
1090
|
+
const activity = await backgroundCommandActivityForSessions(db, {
|
|
1091
|
+
accountId: grant.accountId,
|
|
1092
|
+
workspaceId,
|
|
1093
|
+
sessionIds: [sessionId],
|
|
1094
|
+
});
|
|
1095
|
+
const scheduleTargets =
|
|
1096
|
+
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
1097
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
1098
|
+
? await scheduledSessionIds(db, workspaceId, [sessionId])
|
|
1099
|
+
: new Set<string>();
|
|
1100
|
+
return c.json(
|
|
1101
|
+
await withEffectivePolicy(deps, workspaceId, grant.subjectId, {
|
|
1102
|
+
...session,
|
|
1103
|
+
hasSchedules: scheduleTargets.has(sessionId),
|
|
1104
|
+
...(activity.get(sessionId) ? { backgroundCommandActivity: activity.get(sessionId) } : {}),
|
|
1105
|
+
}),
|
|
1106
|
+
);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/model-context", async (c) => {
|
|
1110
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1111
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1112
|
+
const sessionId = c.req.param("sessionId");
|
|
1113
|
+
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
1114
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1115
|
+
}
|
|
1116
|
+
const session = await getSessionForSubject(
|
|
1117
|
+
db,
|
|
1118
|
+
workspaceId,
|
|
1119
|
+
sessionId,
|
|
1120
|
+
grant.subjectId,
|
|
1121
|
+
relatedSessionAccessFor(c),
|
|
1122
|
+
);
|
|
1123
|
+
if (!session) {
|
|
1124
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1125
|
+
}
|
|
1126
|
+
return c.json(
|
|
1127
|
+
await getLatestSessionModelContext(db, {
|
|
1128
|
+
accountId: grant.accountId,
|
|
1129
|
+
workspaceId,
|
|
1130
|
+
sessionId,
|
|
1131
|
+
}),
|
|
1132
|
+
);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands", async (c) => {
|
|
1136
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1137
|
+
const sessionId = c.req.param("sessionId");
|
|
1138
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1139
|
+
const commands = await listSessionBackgroundCommands(db, {
|
|
1140
|
+
accountId: grant.accountId,
|
|
1141
|
+
workspaceId,
|
|
1142
|
+
sessionId,
|
|
1143
|
+
activeOnly: true,
|
|
1144
|
+
});
|
|
1145
|
+
return c.json({ commands });
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
app.delete(
|
|
1149
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands/:commandId",
|
|
1150
|
+
async (c) => {
|
|
1151
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1152
|
+
const sessionId = c.req.param("sessionId");
|
|
1153
|
+
const commandId = c.req.param("commandId");
|
|
1154
|
+
if (!z.string().uuid().safeParse(commandId).success) {
|
|
1155
|
+
throw new HTTPException(404, { message: "background command not found" });
|
|
1156
|
+
}
|
|
1157
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1158
|
+
const result = await requestSessionBackgroundCommandCancellation(db, {
|
|
1159
|
+
accountId: grant.accountId,
|
|
1160
|
+
workspaceId,
|
|
1161
|
+
sessionId,
|
|
1162
|
+
commandId,
|
|
1163
|
+
subjectId: grant.subjectId,
|
|
1164
|
+
});
|
|
1165
|
+
if (!result.command) {
|
|
1166
|
+
throw new HTTPException(404, { message: "background command not found" });
|
|
1167
|
+
}
|
|
1168
|
+
return c.json(result);
|
|
1169
|
+
},
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/visibility", async (c) => {
|
|
1173
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1174
|
+
const sessionId = c.req.param("sessionId");
|
|
1175
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1176
|
+
const parsed = UpdateSessionVisibilityRequest.safeParse(await c.req.json().catch(() => null));
|
|
1177
|
+
if (!parsed.success) {
|
|
1178
|
+
throw new ApiHttpError(422, {
|
|
1179
|
+
code: "validation_failed",
|
|
1180
|
+
message: "Invalid session visibility request.",
|
|
1181
|
+
retryable: false,
|
|
1182
|
+
details: { fields: zodErrorFields(parsed.error) },
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
return c.json(
|
|
1187
|
+
await updateManagedHumanSessionVisibility(
|
|
1188
|
+
deps,
|
|
1189
|
+
authorization,
|
|
1190
|
+
workspaceId,
|
|
1191
|
+
sessionId,
|
|
1192
|
+
parsed.data,
|
|
1193
|
+
"http",
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
throw sessionTenancyHttpError(error);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/forks", async (c) => {
|
|
1202
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1203
|
+
const sessionId = c.req.param("sessionId");
|
|
1204
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1205
|
+
const parsed = ForkSessionRequest.safeParse(await c.req.json().catch(() => null));
|
|
1206
|
+
if (!parsed.success) {
|
|
1207
|
+
throw new ApiHttpError(422, {
|
|
1208
|
+
code: "validation_failed",
|
|
1209
|
+
message: "Invalid session fork request.",
|
|
1210
|
+
retryable: false,
|
|
1211
|
+
details: { fields: zodErrorFields(parsed.error) },
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const response = await forkManagedHumanSession(
|
|
1216
|
+
deps,
|
|
1217
|
+
authorization,
|
|
1218
|
+
workspaceId,
|
|
1219
|
+
sessionId,
|
|
1220
|
+
parsed.data,
|
|
1221
|
+
"http",
|
|
1222
|
+
);
|
|
1223
|
+
return c.json(response, response.replay ? 200 : 201);
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
throw sessionTenancyHttpError(error);
|
|
1226
|
+
}
|
|
682
1227
|
});
|
|
683
1228
|
|
|
684
1229
|
const publishRealtimeMutation = async (
|
|
@@ -719,15 +1264,23 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
719
1264
|
});
|
|
720
1265
|
}
|
|
721
1266
|
try {
|
|
722
|
-
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) =>
|
|
723
|
-
|
|
1267
|
+
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) => {
|
|
1268
|
+
const accounts = await freezeSessionRealtimeConnectionAccounts({
|
|
1269
|
+
db: scopedDb as unknown as Database,
|
|
1270
|
+
settings,
|
|
1271
|
+
grant,
|
|
1272
|
+
workspaceId,
|
|
1273
|
+
sessionId,
|
|
1274
|
+
});
|
|
1275
|
+
return beginSessionRealtimeInTransaction(scopedDb, {
|
|
724
1276
|
accountId: grant.accountId,
|
|
725
1277
|
workspaceId,
|
|
726
1278
|
sessionId,
|
|
727
1279
|
ownerSubjectId: grant.subjectId,
|
|
728
1280
|
...parsed.data,
|
|
729
|
-
|
|
730
|
-
|
|
1281
|
+
...accounts,
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
731
1284
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
732
1285
|
c.header("cache-control", "private, no-store");
|
|
733
1286
|
return c.json({ mode: result.mode, replay: result.replay }, result.replay ? 200 : 201);
|
|
@@ -1276,7 +1829,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1276
1829
|
const workspaceId = c.req.param("workspaceId");
|
|
1277
1830
|
const sessionId = c.req.param("sessionId");
|
|
1278
1831
|
const realtimeId = c.req.param("realtimeId");
|
|
1279
|
-
const
|
|
1832
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1833
|
+
c,
|
|
1834
|
+
deps,
|
|
1835
|
+
workspaceId,
|
|
1836
|
+
"sessions:control",
|
|
1837
|
+
);
|
|
1838
|
+
const grant = authorization.grant;
|
|
1839
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
1840
|
+
const captureLinked = prepareExternalLinkTurnAdmission(authorization);
|
|
1280
1841
|
if (
|
|
1281
1842
|
!z.string().uuid().safeParse(sessionId).success ||
|
|
1282
1843
|
!z.string().uuid().safeParse(realtimeId).success
|
|
@@ -1292,15 +1853,29 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1292
1853
|
});
|
|
1293
1854
|
}
|
|
1294
1855
|
try {
|
|
1295
|
-
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) =>
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1856
|
+
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) => {
|
|
1857
|
+
// Acquire origin authority before inference/session locks. A deferred
|
|
1858
|
+
// realtime session has no initial worker turn; capture its exact
|
|
1859
|
+
// linked actor when the ledger actually admits ordinary agent work.
|
|
1860
|
+
await beforeCommit?.(scopedDb as unknown as Database);
|
|
1861
|
+
return syncSessionRealtimeLedgerInTransaction(
|
|
1862
|
+
scopedDb,
|
|
1863
|
+
{
|
|
1864
|
+
workspaceId,
|
|
1865
|
+
sessionId,
|
|
1866
|
+
realtimeId,
|
|
1867
|
+
ownerSubjectId: grant.subjectId,
|
|
1868
|
+
...parsed.data,
|
|
1869
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1870
|
+
},
|
|
1871
|
+
captureLinked
|
|
1872
|
+
? {
|
|
1873
|
+
afterDelegationAdmission: async ({ turnId }) =>
|
|
1874
|
+
captureLinked(scopedDb as unknown as Database, sessionId, turnId),
|
|
1875
|
+
}
|
|
1876
|
+
: {},
|
|
1877
|
+
);
|
|
1878
|
+
});
|
|
1304
1879
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
1305
1880
|
c.header("cache-control", "private, no-store");
|
|
1306
1881
|
return c.json({ accepted: result.accepted, outbound: result.outbound });
|
|
@@ -1315,7 +1890,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1315
1890
|
// (not session control) and returns 404 for a foreign/inaccessible session.
|
|
1316
1891
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/pin", async (c) => {
|
|
1317
1892
|
const workspaceId = c.req.param("workspaceId");
|
|
1318
|
-
const
|
|
1893
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1894
|
+
c,
|
|
1895
|
+
deps,
|
|
1896
|
+
workspaceId,
|
|
1897
|
+
"sessions:read",
|
|
1898
|
+
);
|
|
1899
|
+
const grant = authorization.grant;
|
|
1319
1900
|
const sessionId = c.req.param("sessionId");
|
|
1320
1901
|
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
1321
1902
|
throw new HTTPException(404, { message: "session not found" });
|
|
@@ -1329,6 +1910,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1329
1910
|
workspaceId,
|
|
1330
1911
|
subjectId: grant.subjectId,
|
|
1331
1912
|
sessionId,
|
|
1913
|
+
// Same owner-only personal-workspace fallback as the list above.
|
|
1914
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1332
1915
|
...parsed.data,
|
|
1333
1916
|
});
|
|
1334
1917
|
if (!session) {
|
|
@@ -1359,28 +1942,186 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1359
1942
|
}
|
|
1360
1943
|
});
|
|
1361
1944
|
|
|
1362
|
-
|
|
1945
|
+
// Personal follow-up state. Session reads stay side-effect free; clients use
|
|
1946
|
+
// this explicit mutation after a deliberate read/unread action. The managed
|
|
1947
|
+
// web console treats an exact foreground chat event frontier as that action,
|
|
1948
|
+
// while the actively-working label remains independent of acknowledgment.
|
|
1949
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/attention", async (c) => {
|
|
1363
1950
|
const workspaceId = c.req.param("workspaceId");
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1951
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1952
|
+
c,
|
|
1953
|
+
deps,
|
|
1954
|
+
workspaceId,
|
|
1955
|
+
"sessions:read",
|
|
1956
|
+
);
|
|
1957
|
+
const grant = authorization.grant;
|
|
1958
|
+
const sessionId = c.req.param("sessionId");
|
|
1959
|
+
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
1960
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1961
|
+
}
|
|
1962
|
+
const parsed = UpdateSessionAttentionRequest.safeParse(await c.req.json().catch(() => null));
|
|
1963
|
+
if (!parsed.success) {
|
|
1964
|
+
throw new HTTPException(400, { message: "invalid session attention request" });
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
const session = await setSessionAttention(db, {
|
|
1968
|
+
workspaceId,
|
|
1969
|
+
subjectId: grant.subjectId,
|
|
1970
|
+
sessionId,
|
|
1971
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1972
|
+
...parsed.data,
|
|
1973
|
+
});
|
|
1974
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
1975
|
+
return c.json(
|
|
1976
|
+
await withEffectivePolicy(
|
|
1977
|
+
deps,
|
|
1978
|
+
workspaceId,
|
|
1979
|
+
grant.subjectId,
|
|
1980
|
+
projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
|
|
1981
|
+
),
|
|
1982
|
+
);
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
if (error instanceof SessionPinAccessError) {
|
|
1985
|
+
throw new HTTPException(403, { message: error.message });
|
|
1986
|
+
}
|
|
1987
|
+
if (error instanceof SessionAttentionVersionConflictError) {
|
|
1988
|
+
return c.json(
|
|
1989
|
+
{
|
|
1990
|
+
message: "session attention changed in another client",
|
|
1991
|
+
current: error.current,
|
|
1992
|
+
},
|
|
1993
|
+
409,
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
throw error;
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
// Personal chat organization: archived roots leave the ordinary rail only
|
|
2001
|
+
// for this member and remain recoverable through the archived list view.
|
|
2002
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
|
|
2003
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2004
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
2005
|
+
c,
|
|
2006
|
+
deps,
|
|
2007
|
+
workspaceId,
|
|
2008
|
+
"sessions:read",
|
|
2009
|
+
);
|
|
2010
|
+
const grant = authorization.grant;
|
|
2011
|
+
const sessionId = c.req.param("sessionId");
|
|
2012
|
+
const parsed = UpdateSessionArchiveRequest.safeParse(await c.req.json().catch(() => null));
|
|
2013
|
+
if (!parsed.success) {
|
|
2014
|
+
throw new HTTPException(400, { message: "invalid session archive request" });
|
|
2015
|
+
}
|
|
2016
|
+
try {
|
|
2017
|
+
const session = await setSessionArchive(db, {
|
|
2018
|
+
workspaceId,
|
|
2019
|
+
subjectId: grant.subjectId,
|
|
2020
|
+
sessionId,
|
|
2021
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
2022
|
+
...parsed.data,
|
|
2023
|
+
});
|
|
2024
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
2025
|
+
return c.json(
|
|
2026
|
+
await withEffectivePolicy(
|
|
2027
|
+
deps,
|
|
2028
|
+
workspaceId,
|
|
2029
|
+
grant.subjectId,
|
|
2030
|
+
projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
|
|
2031
|
+
),
|
|
2032
|
+
);
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
if (error instanceof SessionPinAccessError) {
|
|
2035
|
+
throw new HTTPException(403, { message: error.message });
|
|
2036
|
+
}
|
|
2037
|
+
if (error instanceof SessionArchiveVersionConflictError) {
|
|
2038
|
+
return c.json(
|
|
2039
|
+
{ message: "session archive changed in another client", current: error.current },
|
|
2040
|
+
409,
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
throw error;
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
|
|
2048
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2049
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
2050
|
+
const lineage = await readSessionLineage(deps, grant, c.req.param("sessionId"));
|
|
2051
|
+
const sessionIds = [
|
|
2052
|
+
c.req.param("sessionId"),
|
|
2053
|
+
...lineage.ancestors.map((session) => session.id),
|
|
2054
|
+
];
|
|
2055
|
+
const lineageSessions = [...lineage.ancestors];
|
|
2056
|
+
const collect = (nodes: LineageNode[]) => {
|
|
2057
|
+
for (const node of nodes) {
|
|
2058
|
+
sessionIds.push(node.session.id);
|
|
2059
|
+
lineageSessions.push(node.session as Session);
|
|
2060
|
+
collect(node.children);
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
collect(lineage.children);
|
|
2064
|
+
const targets =
|
|
2065
|
+
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
2066
|
+
hasPermission(grant.permissions, "sessions:control")
|
|
2067
|
+
? await scheduledSessionIds(db, workspaceId, sessionIds)
|
|
2068
|
+
: new Set<string>();
|
|
2069
|
+
const decorateNodes = (nodes: LineageNode[]): LineageNode[] =>
|
|
2070
|
+
nodes.map((node) => ({
|
|
2071
|
+
...node,
|
|
2072
|
+
session: { ...node.session, hasSchedules: targets.has(node.session.id) },
|
|
2073
|
+
children: decorateNodes(node.children),
|
|
2074
|
+
}));
|
|
2075
|
+
const policy = await loadEffectivePolicyContext(
|
|
2076
|
+
deps,
|
|
2077
|
+
workspaceId,
|
|
2078
|
+
grant.subjectId,
|
|
2079
|
+
lineageSessions,
|
|
2080
|
+
);
|
|
2081
|
+
return c.json({
|
|
1368
2082
|
...lineage,
|
|
2083
|
+
sessionHasSchedules: targets.has(c.req.param("sessionId")),
|
|
1369
2084
|
ancestors: lineage.ancestors.map((session) =>
|
|
1370
2085
|
sessionWithEffectiveToolPolicy(
|
|
1371
|
-
session,
|
|
2086
|
+
{ ...session, hasSchedules: targets.has(session.id) },
|
|
1372
2087
|
policy.workspaceServerIds,
|
|
1373
2088
|
policy.workspaceDefaultServerIds,
|
|
2089
|
+
policy.effectiveToolsContext,
|
|
1374
2090
|
),
|
|
1375
2091
|
),
|
|
1376
|
-
children: mapLineageNodes(lineage.children, policy),
|
|
2092
|
+
children: decorateNodes(mapLineageNodes(lineage.children, policy)),
|
|
2093
|
+
});
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-accounts", async (c) => {
|
|
2097
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2098
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
2099
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
2100
|
+
// authorizeSessionHttp has already enforced private-session and agent scope.
|
|
2101
|
+
const projection = await getSessionCodexAccounts(db, workspaceId, c.req.param("sessionId"));
|
|
2102
|
+
if (!projection) throw new HTTPException(404, { message: "session not found" });
|
|
2103
|
+
const activeAccountId = projection.rotation?.activeCredentialId ?? null;
|
|
2104
|
+
return c.json({
|
|
2105
|
+
accounts: projection.accounts.map((account) => codexAccountJson(account)),
|
|
2106
|
+
activeAccountId,
|
|
2107
|
+
settings: {
|
|
2108
|
+
rotationEnabled: projection.rotation?.rotationEnabled ?? false,
|
|
2109
|
+
rotationStrategy: "sharded",
|
|
2110
|
+
activeCredentialId: activeAccountId,
|
|
2111
|
+
},
|
|
2112
|
+
currentSelection: projection.currentSelection,
|
|
2113
|
+
currentAccount: projection.currentAccount
|
|
2114
|
+
? codexAccountJson(projection.currentAccount)
|
|
2115
|
+
: null,
|
|
2116
|
+
pinnedAccountId: projection.pinnedAccountId,
|
|
2117
|
+
lastAccountId: projection.lastAccountId,
|
|
1377
2118
|
});
|
|
1378
2119
|
});
|
|
1379
2120
|
|
|
1380
2121
|
// Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
|
|
1381
2122
|
// "auto" clears the pin (the session follows the workspace active pointer); a
|
|
1382
|
-
// uuid pins the session to that specific account.
|
|
1383
|
-
// turn
|
|
2123
|
+
// uuid pins the session to that specific account. Overrides a capacity-blocked
|
|
2124
|
+
// turn; a running attempt keeps its account. 404 when the session or the target
|
|
1384
2125
|
// account id isn't in the workspace.
|
|
1385
2126
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
|
|
1386
2127
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -1405,15 +2146,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1405
2146
|
}
|
|
1406
2147
|
}
|
|
1407
2148
|
const pinned = target === "auto" ? null : target;
|
|
1408
|
-
const mutation = await
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
);
|
|
1416
|
-
const ok = mutation.result;
|
|
2149
|
+
const mutation = await switchSessionCodexAccount(db, {
|
|
2150
|
+
workspaceId,
|
|
2151
|
+
sessionId,
|
|
2152
|
+
credentialId: pinned,
|
|
2153
|
+
subjectId: grant.subjectId,
|
|
2154
|
+
});
|
|
2155
|
+
const ok = mutation.result.changed;
|
|
1417
2156
|
if (!ok) {
|
|
1418
2157
|
throw new HTTPException(404, {
|
|
1419
2158
|
message: "session or codex account not found",
|
|
@@ -1439,7 +2178,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1439
2178
|
}),
|
|
1440
2179
|
),
|
|
1441
2180
|
);
|
|
1442
|
-
|
|
2181
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, mutation.result.events);
|
|
2182
|
+
return c.json({
|
|
2183
|
+
pinned: target === "auto" ? "auto" : target,
|
|
2184
|
+
appliedTo: mutation.result.appliedTo,
|
|
2185
|
+
});
|
|
1443
2186
|
});
|
|
1444
2187
|
|
|
1445
2188
|
// Re-file the session into a workspace channel (rail organization only;
|
|
@@ -1450,7 +2193,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1450
2193
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1451
2194
|
const sessionId = c.req.param("sessionId");
|
|
1452
2195
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1453
|
-
|
|
2196
|
+
await requireSessionAuthorization(deps, grant, {
|
|
2197
|
+
sessionId,
|
|
2198
|
+
operation: "session.control",
|
|
2199
|
+
surface: "http",
|
|
2200
|
+
});
|
|
2201
|
+
const payload = await parseRequestJson(c, UpdateSessionChannelRequest);
|
|
1454
2202
|
try {
|
|
1455
2203
|
const updated = await setSessionChannel(db, {
|
|
1456
2204
|
workspaceId,
|
|
@@ -1479,6 +2227,91 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1479
2227
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
1480
2228
|
});
|
|
1481
2229
|
|
|
2230
|
+
// Replace the complete ordered Variable Set selection. The DB mutation
|
|
2231
|
+
// serializes with turn claim, rejects live/shared sandbox use, and requests a
|
|
2232
|
+
// cold rotation before the new environment can be materialized.
|
|
2233
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/variable-sets", async (c) => {
|
|
2234
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2235
|
+
const sessionId = c.req.param("sessionId");
|
|
2236
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2237
|
+
try {
|
|
2238
|
+
await requireSessionAuthorization(deps, grant, {
|
|
2239
|
+
sessionId,
|
|
2240
|
+
operation: "session.variable_sets.write",
|
|
2241
|
+
surface: "http",
|
|
2242
|
+
});
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
throw sessionAuthorizationHttpError(error);
|
|
2245
|
+
}
|
|
2246
|
+
// Detach still requires attach authority: it changes which protected
|
|
2247
|
+
// resources the session may materialize. Non-empty selections additionally
|
|
2248
|
+
// require use authority through validateVariableSetAttachment.
|
|
2249
|
+
requirePermission(grant, "variable-sets:attach");
|
|
2250
|
+
const parsed = UpdateSessionVariableSetsRequest.safeParse(await c.req.json().catch(() => null));
|
|
2251
|
+
if (!parsed.success) {
|
|
2252
|
+
throw new ApiHttpError(422, {
|
|
2253
|
+
code: "validation_failed",
|
|
2254
|
+
message: "Invalid session Variable Set request.",
|
|
2255
|
+
retryable: false,
|
|
2256
|
+
details: { fields: zodErrorFields(parsed.error) },
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
const variableSets: VariableSet[] = [];
|
|
2260
|
+
for (const variableSetId of parsed.data.variableSetIds) {
|
|
2261
|
+
variableSets.push(
|
|
2262
|
+
await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
const result = await updateSessionVariableSets(db, {
|
|
2266
|
+
accountId: grant.accountId,
|
|
2267
|
+
workspaceId,
|
|
2268
|
+
sessionId,
|
|
2269
|
+
subjectId: grant.subjectId,
|
|
2270
|
+
variableSets: variableSets.map((variableSet) => ({
|
|
2271
|
+
id: variableSet.id,
|
|
2272
|
+
name: variableSet.name,
|
|
2273
|
+
scope: variableSet.scope,
|
|
2274
|
+
})),
|
|
2275
|
+
});
|
|
2276
|
+
if (result.status === "not_found") {
|
|
2277
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
2278
|
+
}
|
|
2279
|
+
if (result.status === "invalid_variable_sets") {
|
|
2280
|
+
throw new ApiHttpError(422, {
|
|
2281
|
+
code: "validation_failed",
|
|
2282
|
+
message: "One or more selected Variable Sets are no longer available.",
|
|
2283
|
+
retryable: false,
|
|
2284
|
+
outcomeUnknown: false,
|
|
2285
|
+
details: { variableSetIds: result.variableSetIds },
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
if (result.status === "blocked") {
|
|
2289
|
+
const messages = {
|
|
2290
|
+
turn_in_flight:
|
|
2291
|
+
"Variable Sets can be changed only when the session has no accepted, queued, claimed, or pending work.",
|
|
2292
|
+
shared_sandbox_group:
|
|
2293
|
+
"Variable Sets cannot be changed while this session shares a sandbox; fork it into a separate session first.",
|
|
2294
|
+
live_sandbox_holders:
|
|
2295
|
+
"Close active terminal, desktop, and sandbox operations before changing Variable Sets.",
|
|
2296
|
+
} as const;
|
|
2297
|
+
throw new HTTPException(409, { message: messages[result.reason] });
|
|
2298
|
+
}
|
|
2299
|
+
if (result.status === "updated") {
|
|
2300
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, [result.event]);
|
|
2301
|
+
}
|
|
2302
|
+
const session = await getSessionForSubject(
|
|
2303
|
+
db,
|
|
2304
|
+
workspaceId,
|
|
2305
|
+
sessionId,
|
|
2306
|
+
grant.subjectId,
|
|
2307
|
+
relatedSessionAccessFor(c),
|
|
2308
|
+
);
|
|
2309
|
+
if (!session) {
|
|
2310
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
2311
|
+
}
|
|
2312
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2313
|
+
});
|
|
2314
|
+
|
|
1482
2315
|
// Manual rename. A user-set title is permanent: the db write is
|
|
1483
2316
|
// unconditional (source='user'), so it always pins the session over later
|
|
1484
2317
|
// agent writes. Returns the refreshed session, mirroring GET detail.
|
|
@@ -1487,7 +2320,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1487
2320
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1488
2321
|
const sessionId = c.req.param("sessionId");
|
|
1489
2322
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1490
|
-
const payload =
|
|
2323
|
+
const payload = await parseRequestJson(c, UpdateSessionRequest);
|
|
1491
2324
|
const titleUpdate = await updateSessionTitle(deps, grant, sessionId, payload.title, "user");
|
|
1492
2325
|
// A session-returning member route must preserve the caller's private pin
|
|
1493
2326
|
// projection. Returning the generic mapSession() default here would reset a
|
|
@@ -1505,6 +2338,49 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1505
2338
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
1506
2339
|
});
|
|
1507
2340
|
|
|
2341
|
+
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
2342
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2343
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2344
|
+
const deleted = await deleteSessionTreeIfQuiescent(db, {
|
|
2345
|
+
workspaceId,
|
|
2346
|
+
subjectId: grant.subjectId,
|
|
2347
|
+
sessionId: c.req.param("sessionId"),
|
|
2348
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
2349
|
+
});
|
|
2350
|
+
switch (deleted.status) {
|
|
2351
|
+
case "deleted":
|
|
2352
|
+
return c.json({ deletedSessionCount: deleted.deletedSessionCount });
|
|
2353
|
+
case "not_found":
|
|
2354
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
2355
|
+
case "not_root":
|
|
2356
|
+
throw new HTTPException(409, {
|
|
2357
|
+
message: "delete the root session to remove the complete workstream",
|
|
2358
|
+
});
|
|
2359
|
+
case "active_sessions":
|
|
2360
|
+
throw new HTTPException(409, {
|
|
2361
|
+
message: "cancel the workstream and wait for active turns to finish before deleting it",
|
|
2362
|
+
});
|
|
2363
|
+
case "active_video_generations":
|
|
2364
|
+
throw new HTTPException(409, {
|
|
2365
|
+
message: "wait for active video generations to finish before deleting this workstream",
|
|
2366
|
+
});
|
|
2367
|
+
case "active_background_commands":
|
|
2368
|
+
throw new HTTPException(409, {
|
|
2369
|
+
message: "pause or cancel this workstream's background commands before deleting it",
|
|
2370
|
+
});
|
|
2371
|
+
case "live_sandboxes":
|
|
2372
|
+
throw new HTTPException(409, {
|
|
2373
|
+
message:
|
|
2374
|
+
"wait for the workstream's sandbox activity to finish draining before deleting it",
|
|
2375
|
+
});
|
|
2376
|
+
case "externally_referenced":
|
|
2377
|
+
throw new HTTPException(409, {
|
|
2378
|
+
message:
|
|
2379
|
+
"this workstream has durable workspace outputs or independent forks; archive it instead",
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
|
|
1508
2384
|
app.patch(
|
|
1509
2385
|
"/v1/workspaces/:workspaceId/sessions/:sessionId/mcp-servers/:serverId/approval-policy",
|
|
1510
2386
|
async (c) => {
|
|
@@ -1537,7 +2413,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1537
2413
|
const workspaceId = c.req.param("workspaceId");
|
|
1538
2414
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1539
2415
|
const sessionId = c.req.param("sessionId");
|
|
1540
|
-
const payload =
|
|
2416
|
+
const payload = parseRequestBody(
|
|
2417
|
+
UpdateSessionToolPolicyRequest,
|
|
2418
|
+
await c.req.json().catch(() => null),
|
|
2419
|
+
);
|
|
1541
2420
|
try {
|
|
1542
2421
|
const session = await updateSessionToolPolicy(deps, grant, sessionId, payload);
|
|
1543
2422
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
@@ -1556,6 +2435,35 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1556
2435
|
}
|
|
1557
2436
|
});
|
|
1558
2437
|
|
|
2438
|
+
// Replace the session's agent configuration (capabilities, identity,
|
|
2439
|
+
// instructions alias, renderer). Shares the tool-policy version CAS and
|
|
2440
|
+
// applies from the next attempt.
|
|
2441
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/agent", async (c) => {
|
|
2442
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2443
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2444
|
+
const sessionId = c.req.param("sessionId");
|
|
2445
|
+
const payload = parseRequestBody(
|
|
2446
|
+
UpdateSessionAgentRequest,
|
|
2447
|
+
await c.req.json().catch(() => null),
|
|
2448
|
+
);
|
|
2449
|
+
try {
|
|
2450
|
+
const session = await updateSessionAgent(deps, grant, sessionId, payload);
|
|
2451
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
if (error instanceof SessionToolPolicyVersionConflictError) {
|
|
2454
|
+
return c.json(
|
|
2455
|
+
{
|
|
2456
|
+
code: error.code,
|
|
2457
|
+
message: error.message,
|
|
2458
|
+
currentVersion: error.currentVersion,
|
|
2459
|
+
},
|
|
2460
|
+
409,
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
throw error;
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
|
|
1559
2467
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
1560
2468
|
const workspaceId = c.req.param("workspaceId");
|
|
1561
2469
|
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -1576,6 +2484,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1576
2484
|
return c.json(await listSessionGoalRevisions(db, workspaceId, sessionId));
|
|
1577
2485
|
});
|
|
1578
2486
|
|
|
2487
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/page", async (c) => {
|
|
2488
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2489
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
2490
|
+
const sessionId = c.req.param("sessionId");
|
|
2491
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
2492
|
+
const parsedQuery = ListSessionGoalRevisionsQuery.safeParse({
|
|
2493
|
+
limit: c.req.query("limit"),
|
|
2494
|
+
before: c.req.query("before"),
|
|
2495
|
+
});
|
|
2496
|
+
if (!parsedQuery.success) {
|
|
2497
|
+
throw new HTTPException(400, { message: "invalid goal revision page query" });
|
|
2498
|
+
}
|
|
2499
|
+
const query = parsedQuery.data;
|
|
2500
|
+
try {
|
|
2501
|
+
return c.json(
|
|
2502
|
+
await listSessionGoalRevisionPage(db, workspaceId, sessionId, {
|
|
2503
|
+
limit: query.limit,
|
|
2504
|
+
...(query.before ? { before: query.before } : {}),
|
|
2505
|
+
}),
|
|
2506
|
+
);
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
if (error instanceof SessionControlConflictError) {
|
|
2509
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2510
|
+
}
|
|
2511
|
+
throw error;
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
|
|
1579
2515
|
app.post(
|
|
1580
2516
|
"/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/apply",
|
|
1581
2517
|
async (c) => {
|
|
@@ -1583,7 +2519,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1583
2519
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1584
2520
|
const sessionId = c.req.param("sessionId");
|
|
1585
2521
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1586
|
-
const payload =
|
|
2522
|
+
const payload = await parseRequestJson(c, ApplySessionGoalRevisionRequest);
|
|
1587
2523
|
const revision = await getSessionGoalRevision(
|
|
1588
2524
|
db,
|
|
1589
2525
|
workspaceId,
|
|
@@ -1607,6 +2543,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1607
2543
|
sessionId,
|
|
1608
2544
|
text: revision.text,
|
|
1609
2545
|
successCriteria: revision.successCriteria,
|
|
2546
|
+
rootConstraints: revision.rootConstraints,
|
|
1610
2547
|
mutationPolicy: revision.mutationPolicy,
|
|
1611
2548
|
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
1612
2549
|
expectedGoalId: revision.goalId,
|
|
@@ -1639,12 +2576,94 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1639
2576
|
},
|
|
1640
2577
|
);
|
|
1641
2578
|
|
|
2579
|
+
app.post(
|
|
2580
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/reject",
|
|
2581
|
+
async (c) => {
|
|
2582
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2583
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2584
|
+
const sessionId = c.req.param("sessionId");
|
|
2585
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
2586
|
+
const payload = await parseRequestJson(c, RejectSessionGoalRevisionRequest);
|
|
2587
|
+
try {
|
|
2588
|
+
const result = await rejectSessionGoalRevisionWithEvent(db, {
|
|
2589
|
+
accountId: grant.accountId,
|
|
2590
|
+
workspaceId,
|
|
2591
|
+
sessionId,
|
|
2592
|
+
revisionId: c.req.param("revisionId"),
|
|
2593
|
+
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
2594
|
+
rationale: payload.rationale,
|
|
2595
|
+
});
|
|
2596
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
|
|
2597
|
+
return c.json({ revision: result.revision, replay: result.replay });
|
|
2598
|
+
} catch (error) {
|
|
2599
|
+
if (error instanceof SessionControlConflictError) {
|
|
2600
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2601
|
+
}
|
|
2602
|
+
throw error;
|
|
2603
|
+
}
|
|
2604
|
+
},
|
|
2605
|
+
);
|
|
2606
|
+
|
|
2607
|
+
app.post(
|
|
2608
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/rollback",
|
|
2609
|
+
async (c) => {
|
|
2610
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2611
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2612
|
+
const sessionId = c.req.param("sessionId");
|
|
2613
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
2614
|
+
const payload = await parseRequestJson(c, RollbackSessionGoalRevisionRequest);
|
|
2615
|
+
const revision = await getSessionGoalRevision(
|
|
2616
|
+
db,
|
|
2617
|
+
workspaceId,
|
|
2618
|
+
sessionId,
|
|
2619
|
+
c.req.param("revisionId"),
|
|
2620
|
+
);
|
|
2621
|
+
if (!revision || revision.disposition !== "applied") {
|
|
2622
|
+
throw new HTTPException(404, { message: "applied goal revision not found" });
|
|
2623
|
+
}
|
|
2624
|
+
try {
|
|
2625
|
+
const { goal, workflowWakeRevision, events } = await upsertSessionGoalWithEvent(db, {
|
|
2626
|
+
accountId: grant.accountId,
|
|
2627
|
+
workspaceId,
|
|
2628
|
+
sessionId,
|
|
2629
|
+
text: revision.text,
|
|
2630
|
+
successCriteria: revision.successCriteria,
|
|
2631
|
+
rootConstraints: revision.rootConstraints,
|
|
2632
|
+
mutationPolicy: revision.mutationPolicy,
|
|
2633
|
+
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
2634
|
+
expectedGoalId: revision.goalId,
|
|
2635
|
+
changeKind: "replacement",
|
|
2636
|
+
changeRationale: payload.rationale,
|
|
2637
|
+
rollbackOfRevisionId: revision.id,
|
|
2638
|
+
createdBy: "api",
|
|
2639
|
+
actor: "api",
|
|
2640
|
+
});
|
|
2641
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, events);
|
|
2642
|
+
if (workflowWakeRevision !== null) {
|
|
2643
|
+
await workflowClient.wakeSessionWorkflow({
|
|
2644
|
+
accountId: grant.accountId,
|
|
2645
|
+
workspaceId,
|
|
2646
|
+
sessionId,
|
|
2647
|
+
workflowId: workflowIdForSession(sessionId),
|
|
2648
|
+
wakeRevision: workflowWakeRevision,
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
return c.json((await getSessionGoalWithContinuation(db, workspaceId, sessionId)) ?? goal);
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
if (error instanceof SessionControlConflictError) {
|
|
2654
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2655
|
+
}
|
|
2656
|
+
throw error;
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
);
|
|
2660
|
+
|
|
1642
2661
|
app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
1643
2662
|
const workspaceId = c.req.param("workspaceId");
|
|
1644
2663
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1645
2664
|
const sessionId = c.req.param("sessionId");
|
|
1646
2665
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1647
|
-
const payload =
|
|
2666
|
+
const payload = await parseRequestJson(c, UpdateSessionGoalRequest);
|
|
1648
2667
|
const existing = await getSessionGoal(db, workspaceId, sessionId);
|
|
1649
2668
|
if (!existing) {
|
|
1650
2669
|
throw new HTTPException(404, { message: "session goal not found" });
|
|
@@ -1660,6 +2679,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1660
2679
|
payload.successCriteria !== undefined
|
|
1661
2680
|
? payload.successCriteria
|
|
1662
2681
|
: existing.successCriteria,
|
|
2682
|
+
rootConstraints:
|
|
2683
|
+
payload.rootConstraints !== undefined
|
|
2684
|
+
? payload.rootConstraints
|
|
2685
|
+
: existing.rootConstraints,
|
|
1663
2686
|
maxAutoContinuations: existing.maxAutoContinuations,
|
|
1664
2687
|
mutationPolicy: payload.mutationPolicy ?? existing.mutationPolicy,
|
|
1665
2688
|
expectedObjectiveRevision: payload.expectedObjectiveRevision,
|
|
@@ -1752,10 +2775,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1752
2775
|
|
|
1753
2776
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
1754
2777
|
const workspaceId = c.req.param("workspaceId");
|
|
1755
|
-
await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2778
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1756
2779
|
const sessionId = c.req.param("sessionId");
|
|
1757
2780
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1758
|
-
const { event } = await clearSessionGoal(db, workspaceId, sessionId
|
|
2781
|
+
const { event } = await clearSessionGoal(db, workspaceId, sessionId, {
|
|
2782
|
+
actor: grant.principalKind === "agent_attempt" ? "agent" : "api",
|
|
2783
|
+
}).catch((error: unknown) => {
|
|
2784
|
+
if (error instanceof SessionControlConflictError) {
|
|
2785
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2786
|
+
}
|
|
2787
|
+
throw error;
|
|
2788
|
+
});
|
|
1759
2789
|
if (event) {
|
|
1760
2790
|
try {
|
|
1761
2791
|
await bus.publish(workspaceId, sessionId, [event]);
|
|
@@ -1818,7 +2848,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1818
2848
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1819
2849
|
const sessionId = c.req.param("sessionId");
|
|
1820
2850
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1821
|
-
CompactSessionContextRequest
|
|
2851
|
+
parseRequestBody(CompactSessionContextRequest, (await c.req.json().catch(() => ({}))) ?? {});
|
|
1822
2852
|
// /compact sets one durable request. The worker clears it only in the same
|
|
1823
2853
|
// fenced transaction that installs replacement history, so failed or stale
|
|
1824
2854
|
// attempts cannot lose the request.
|
|
@@ -1967,22 +2997,35 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1967
2997
|
c.header("X-OpenGeni-Covered-Last", String(result.coveredSequence.last));
|
|
1968
2998
|
return c.json(result);
|
|
1969
2999
|
}
|
|
1970
|
-
const
|
|
3000
|
+
const compactProjection = compact ? coalesceSessionEventDeltasWithCoverage(events) : null;
|
|
3001
|
+
const projected = compactProjection?.events ?? events;
|
|
3002
|
+
const forensicExact =
|
|
3003
|
+
!compact && mode === "forensic" && payloadMode === "full" && dbPage.fullPayloadsExact;
|
|
1971
3004
|
const page = boundSessionEventHttpPage(projected, {
|
|
1972
3005
|
direction,
|
|
1973
|
-
eventProjection:
|
|
3006
|
+
eventProjection: payloadMode === "full" ? "exact" : "bounded",
|
|
3007
|
+
...(compactProjection
|
|
3008
|
+
? { coveredThroughBySequence: compactProjection.coveredThroughBySequence }
|
|
3009
|
+
: {}),
|
|
1974
3010
|
});
|
|
1975
3011
|
const hasMore = dbPage.hasMore || page.truncated;
|
|
1976
3012
|
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
1977
|
-
|
|
3013
|
+
// One oversized exact event is admitted alone; never advertise a maximum
|
|
3014
|
+
// smaller than the response we actually deliver.
|
|
3015
|
+
c.header("X-OpenGeni-Page-Max-Bytes", String(Math.max(1024 * 1024, page.bytes)));
|
|
1978
3016
|
c.header("X-OpenGeni-Page-Truncated", String(hasMore));
|
|
1979
3017
|
c.header("X-OpenGeni-Has-More", String(hasMore));
|
|
1980
3018
|
c.header("X-OpenGeni-Event-Mode", mode);
|
|
1981
3019
|
c.header("X-OpenGeni-Event-Direction", direction);
|
|
1982
3020
|
c.header("X-OpenGeni-Payload-Mode", payloadMode);
|
|
1983
|
-
c.header("X-OpenGeni-Forensic-Exact", String(
|
|
3021
|
+
c.header("X-OpenGeni-Forensic-Exact", String(forensicExact));
|
|
1984
3022
|
const coveredFirst = page.events[0]?.sequence;
|
|
1985
|
-
const
|
|
3023
|
+
const coveredLastEvent = page.events.at(-1);
|
|
3024
|
+
const coveredLast =
|
|
3025
|
+
coveredLastEvent === undefined
|
|
3026
|
+
? undefined
|
|
3027
|
+
: (compactProjection?.coveredThroughBySequence.get(coveredLastEvent.sequence) ??
|
|
3028
|
+
coveredLastEvent.sequence);
|
|
1986
3029
|
if (coveredFirst !== undefined) c.header("X-OpenGeni-Covered-First", String(coveredFirst));
|
|
1987
3030
|
if (coveredLast !== undefined) c.header("X-OpenGeni-Covered-Last", String(coveredLast));
|
|
1988
3031
|
const truncatedBy = page.truncated ? "http_bytes" : dbPage.truncatedBy;
|
|
@@ -1996,6 +3039,90 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1996
3039
|
return c.json(page.events);
|
|
1997
3040
|
});
|
|
1998
3041
|
|
|
3042
|
+
// Selected search hit only: never use the audit event projection here, even
|
|
3043
|
+
// in summary mode (it can contain modelContext or truncate visible text).
|
|
3044
|
+
app.get(
|
|
3045
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/events/:eventId/message-preview",
|
|
3046
|
+
async (c) => {
|
|
3047
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3048
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
3049
|
+
const sessionId = c.req.param("sessionId");
|
|
3050
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
3051
|
+
const eventId = c.req.param("eventId");
|
|
3052
|
+
const rawSequence = c.req.query("sequence");
|
|
3053
|
+
const sequence = Number(rawSequence);
|
|
3054
|
+
if (
|
|
3055
|
+
!z.string().uuid().safeParse(eventId).success ||
|
|
3056
|
+
rawSequence === undefined ||
|
|
3057
|
+
!/^\d+$/.test(rawSequence) ||
|
|
3058
|
+
!Number.isSafeInteger(sequence) ||
|
|
3059
|
+
sequence < 1 ||
|
|
3060
|
+
sequence > 2_147_483_647
|
|
3061
|
+
)
|
|
3062
|
+
throw new HTTPException(400, { message: "Invalid message preview reference" });
|
|
3063
|
+
|
|
3064
|
+
const notFound = () => new HTTPException(404, { message: "message not found" });
|
|
3065
|
+
const maxUnits = 12_000;
|
|
3066
|
+
const eventTable = sessionPreviewSchema.sessionEvents;
|
|
3067
|
+
// The slice helper supports structured result views too. Check only the
|
|
3068
|
+
// JSON scalar *kind* before calling it; never fetch a structured text value.
|
|
3069
|
+
const candidates = await withWorkspaceRls(db, workspaceId, (tx) =>
|
|
3070
|
+
tx
|
|
3071
|
+
.select({ kind: sql<string | null>`jsonb_typeof(${eventTable.payload}->'text')` })
|
|
3072
|
+
.from(eventTable)
|
|
3073
|
+
.where(
|
|
3074
|
+
and(
|
|
3075
|
+
eq(eventTable.workspaceId, workspaceId),
|
|
3076
|
+
eq(eventTable.sessionId, sessionId),
|
|
3077
|
+
eq(eventTable.id, eventId),
|
|
3078
|
+
eq(eventTable.sequence, sequence),
|
|
3079
|
+
sql`${eventTable.type} in ('user.message', 'agent.message.completed')`,
|
|
3080
|
+
),
|
|
3081
|
+
)
|
|
3082
|
+
.limit(1),
|
|
3083
|
+
);
|
|
3084
|
+
if (candidates[0]?.kind !== "string") throw notFound();
|
|
3085
|
+
const read = (offset: number) =>
|
|
3086
|
+
listSessionEventSlices(db, workspaceId, sessionId, {
|
|
3087
|
+
sourceSequence: sequence,
|
|
3088
|
+
sourceOffset: offset,
|
|
3089
|
+
after: sequence - 1,
|
|
3090
|
+
before: sequence + 1,
|
|
3091
|
+
includeTypes: ["user.message", "agent.message.completed"],
|
|
3092
|
+
view: "conversation",
|
|
3093
|
+
});
|
|
3094
|
+
const first = await read(0);
|
|
3095
|
+
const event = first.events[0];
|
|
3096
|
+
const slice = first.slices?.[sequence];
|
|
3097
|
+
// Includes the slice reader's duplicate, late and unclaimed-prompt gates.
|
|
3098
|
+
if (!event || event.id !== eventId || event.sequence !== sequence || !slice || slice.omitted)
|
|
3099
|
+
throw notFound();
|
|
3100
|
+
if (slice.total > maxUnits) return c.json({ status: "unavailable" as const });
|
|
3101
|
+
|
|
3102
|
+
let text = slice.text;
|
|
3103
|
+
let offset = slice.unit === "utf16" ? slice.text.length : Array.from(slice.text).length;
|
|
3104
|
+
if (text.length > maxUnits) return c.json({ status: "unavailable" as const });
|
|
3105
|
+
while (offset < slice.total) {
|
|
3106
|
+
if (offset === 0) throw notFound();
|
|
3107
|
+
const next = await read(offset);
|
|
3108
|
+
const part = next.slices?.[sequence];
|
|
3109
|
+
if (
|
|
3110
|
+
next.events[0]?.id !== eventId ||
|
|
3111
|
+
!part ||
|
|
3112
|
+
part.omitted ||
|
|
3113
|
+
part.unit !== slice.unit ||
|
|
3114
|
+
part.total !== slice.total ||
|
|
3115
|
+
part.offset !== offset
|
|
3116
|
+
)
|
|
3117
|
+
throw notFound();
|
|
3118
|
+
text += part.text;
|
|
3119
|
+
offset += slice.unit === "utf16" ? part.text.length : Array.from(part.text).length;
|
|
3120
|
+
if (text.length > maxUnits) return c.json({ status: "unavailable" as const });
|
|
3121
|
+
}
|
|
3122
|
+
return c.json({ status: "available" as const, text });
|
|
3123
|
+
},
|
|
3124
|
+
);
|
|
3125
|
+
|
|
1999
3126
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events/stream", async (c) => {
|
|
2000
3127
|
const workspaceId = c.req.param("workspaceId");
|
|
2001
3128
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -2020,20 +3147,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2020
3147
|
Number.isFinite(after) ? after : 0,
|
|
2021
3148
|
c.req.raw.signal,
|
|
2022
3149
|
{
|
|
3150
|
+
...browserSseDeliveryOptions(c.req.query("transport")),
|
|
2023
3151
|
observability: deps.observability,
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
}
|
|
2036
|
-
: {}),
|
|
3152
|
+
actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
|
|
3153
|
+
reauthorizeAfterMs:
|
|
3154
|
+
authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
3155
|
+
reauthorize: async () => {
|
|
3156
|
+
const freshGrant = await requireFreshAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
3157
|
+
await requireSessionAuthorization(deps, freshGrant, {
|
|
3158
|
+
sessionId,
|
|
3159
|
+
operation: "session.stream.read",
|
|
3160
|
+
surface: "stream",
|
|
3161
|
+
});
|
|
3162
|
+
},
|
|
2037
3163
|
},
|
|
2038
3164
|
);
|
|
2039
3165
|
});
|
|
@@ -2055,6 +3181,27 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2055
3181
|
);
|
|
2056
3182
|
});
|
|
2057
3183
|
|
|
3184
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/mcp-credentials/rotate", async (c) => {
|
|
3185
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3186
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
3187
|
+
c,
|
|
3188
|
+
deps,
|
|
3189
|
+
workspaceId,
|
|
3190
|
+
"sessions:control",
|
|
3191
|
+
);
|
|
3192
|
+
const payload = await c.req.json().catch(() => null);
|
|
3193
|
+
c.header("cache-control", "private, no-store");
|
|
3194
|
+
return c.json(
|
|
3195
|
+
await rotateSessionMcpCredentialsForRequest(
|
|
3196
|
+
deps,
|
|
3197
|
+
authorization,
|
|
3198
|
+
c.req.param("sessionId"),
|
|
3199
|
+
payload,
|
|
3200
|
+
(tx) => requireFreshAccessGrant(c, { ...deps, db: tx }, workspaceId, "sessions:control"),
|
|
3201
|
+
),
|
|
3202
|
+
);
|
|
3203
|
+
});
|
|
3204
|
+
|
|
2058
3205
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/queue", async (c) => {
|
|
2059
3206
|
const workspaceId = c.req.param("workspaceId");
|
|
2060
3207
|
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -2069,7 +3216,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2069
3216
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2070
3217
|
const sessionId = c.req.param("sessionId");
|
|
2071
3218
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2072
|
-
const payload =
|
|
3219
|
+
const payload = await parseRequestJson(c, MoveSessionQueueItemRequest);
|
|
2073
3220
|
try {
|
|
2074
3221
|
const response = await moveHumanQueuePrompt(
|
|
2075
3222
|
deps,
|
|
@@ -2096,7 +3243,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2096
3243
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2097
3244
|
const sessionId = c.req.param("sessionId");
|
|
2098
3245
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2099
|
-
const payload =
|
|
3246
|
+
const payload = await parseRequestJson(c, EditSessionQueueItemRequest);
|
|
2100
3247
|
try {
|
|
2101
3248
|
const response = await editHumanQueuePrompt(
|
|
2102
3249
|
deps,
|
|
@@ -2123,7 +3270,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2123
3270
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2124
3271
|
const sessionId = c.req.param("sessionId");
|
|
2125
3272
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2126
|
-
const payload =
|
|
3273
|
+
const payload = await parseRequestJson(c, SteerSessionQueueItemRequest);
|
|
2127
3274
|
try {
|
|
2128
3275
|
const response = await steerHumanQueuePrompt(
|
|
2129
3276
|
deps,
|
|
@@ -2150,7 +3297,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2150
3297
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2151
3298
|
const sessionId = c.req.param("sessionId");
|
|
2152
3299
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2153
|
-
const payload =
|
|
3300
|
+
const payload = await parseRequestJson(c, DeleteSessionQueueItemRequest);
|
|
2154
3301
|
try {
|
|
2155
3302
|
const response = await deleteHumanQueuePrompt(
|
|
2156
3303
|
deps,
|
|
@@ -2190,7 +3337,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2190
3337
|
const workspaceId = c.req.param("workspaceId");
|
|
2191
3338
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2192
3339
|
const sessionId = c.req.param("sessionId");
|
|
2193
|
-
const payload =
|
|
3340
|
+
const payload = await parseRequestJson(c, SaveComposerDraftRequest);
|
|
2194
3341
|
try {
|
|
2195
3342
|
return c.json(
|
|
2196
3343
|
await saveHumanComposerDraft(
|
|
@@ -2209,6 +3356,112 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2209
3356
|
}
|
|
2210
3357
|
});
|
|
2211
3358
|
|
|
3359
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
|
|
3360
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3361
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
3362
|
+
try {
|
|
3363
|
+
return c.json(
|
|
3364
|
+
await getManagedHumanSandboxRecovery(
|
|
3365
|
+
deps,
|
|
3366
|
+
authorization,
|
|
3367
|
+
workspaceId,
|
|
3368
|
+
c.req.param("sessionId"),
|
|
3369
|
+
),
|
|
3370
|
+
);
|
|
3371
|
+
} catch (error) {
|
|
3372
|
+
throw sessionTenancyHttpError(error);
|
|
3373
|
+
}
|
|
3374
|
+
});
|
|
3375
|
+
|
|
3376
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
|
|
3377
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3378
|
+
const sessionId = c.req.param("sessionId");
|
|
3379
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
3380
|
+
const parsed = SandboxRecoveryRequest.safeParse(await c.req.json().catch(() => null));
|
|
3381
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid checkpoint consent" });
|
|
3382
|
+
let consentCommitted = false;
|
|
3383
|
+
try {
|
|
3384
|
+
const receipt = await consentManagedHumanSandboxRecovery(
|
|
3385
|
+
deps,
|
|
3386
|
+
authorization,
|
|
3387
|
+
workspaceId,
|
|
3388
|
+
sessionId,
|
|
3389
|
+
parsed.data,
|
|
3390
|
+
);
|
|
3391
|
+
consentCommitted = true;
|
|
3392
|
+
const projection = await getManagedHumanSandboxRecovery(
|
|
3393
|
+
deps,
|
|
3394
|
+
authorization,
|
|
3395
|
+
workspaceId,
|
|
3396
|
+
sessionId,
|
|
3397
|
+
);
|
|
3398
|
+
if (
|
|
3399
|
+
projection.operationId === receipt.operationId &&
|
|
3400
|
+
projection.status === "consent_accepted"
|
|
3401
|
+
) {
|
|
3402
|
+
// Establish/verify only. No failed-turn Retry, Send, tool callback or
|
|
3403
|
+
// command replay. The ordinary cold->warming election owns creation.
|
|
3404
|
+
const session = await getSession(db, workspaceId, sessionId);
|
|
3405
|
+
if (session) {
|
|
3406
|
+
try {
|
|
3407
|
+
await withChannelARead(
|
|
3408
|
+
channelAServices,
|
|
3409
|
+
{
|
|
3410
|
+
accountId: authorization.grant.accountId,
|
|
3411
|
+
workspaceId,
|
|
3412
|
+
session,
|
|
3413
|
+
subjectId: authorization.grant.subjectId,
|
|
3414
|
+
},
|
|
3415
|
+
async () => undefined,
|
|
3416
|
+
);
|
|
3417
|
+
} catch {
|
|
3418
|
+
// Consent committed. Never turn a provider failure into a false
|
|
3419
|
+
// rejection of consent or leak native provider bindings/messages.
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
return c.json({
|
|
3424
|
+
...receipt,
|
|
3425
|
+
recovery: await getManagedHumanSandboxRecovery(deps, authorization, workspaceId, sessionId),
|
|
3426
|
+
});
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
if (consentCommitted) {
|
|
3429
|
+
// A later authorization/read failure cannot revoke the committed
|
|
3430
|
+
// receipt. Return no session state, checkpoint or provider details.
|
|
3431
|
+
return c.json(
|
|
3432
|
+
{
|
|
3433
|
+
code: "upstream_unavailable",
|
|
3434
|
+
message:
|
|
3435
|
+
"Recovery status could not be confirmed. Check status without resubmitting consent.",
|
|
3436
|
+
outcomeUnknown: true,
|
|
3437
|
+
retryable: false,
|
|
3438
|
+
},
|
|
3439
|
+
503,
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
if (error instanceof SandboxRecoveryConflictError)
|
|
3443
|
+
return c.json({ code: error.code, message: error.message }, 409);
|
|
3444
|
+
if (error instanceof SessionCommandIdempotencyError) return commandConflictResponse(c, error);
|
|
3445
|
+
throw sessionTenancyHttpError(error);
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/retry", async (c) => {
|
|
3450
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3451
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
3452
|
+
const parsed = SessionRetryRequest.safeParse(await c.req.json().catch(() => null));
|
|
3453
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid session retry request" });
|
|
3454
|
+
try {
|
|
3455
|
+
return c.json(
|
|
3456
|
+
await retryFailedSession(deps, grant, workspaceId, c.req.param("sessionId"), parsed.data),
|
|
3457
|
+
);
|
|
3458
|
+
} catch (error) {
|
|
3459
|
+
if (error instanceof SessionRetryConflictError)
|
|
3460
|
+
return c.json({ code: error.code, message: error.message }, 409);
|
|
3461
|
+
return commandConflictResponse(c, error);
|
|
3462
|
+
}
|
|
3463
|
+
});
|
|
3464
|
+
|
|
2212
3465
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/control", async (c) => {
|
|
2213
3466
|
const workspaceId = c.req.param("workspaceId");
|
|
2214
3467
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
@@ -2246,37 +3499,91 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2246
3499
|
} catch (error) {
|
|
2247
3500
|
return commandConflictResponse(c, error);
|
|
2248
3501
|
}
|
|
2249
|
-
});
|
|
2250
|
-
|
|
2251
|
-
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/steer", async (c) => {
|
|
2252
|
-
const workspaceId = c.req.param("workspaceId");
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
3502
|
+
});
|
|
3503
|
+
|
|
3504
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/steer", async (c) => {
|
|
3505
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3506
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
3507
|
+
c,
|
|
3508
|
+
deps,
|
|
3509
|
+
workspaceId,
|
|
3510
|
+
"sessions:control",
|
|
3511
|
+
);
|
|
3512
|
+
const grant = authorization.grant;
|
|
3513
|
+
const sessionId = c.req.param("sessionId");
|
|
3514
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
3515
|
+
const payload = parseSteerSessionAdmission(await c.req.json().catch(() => null));
|
|
3516
|
+
let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
|
|
3517
|
+
try {
|
|
3518
|
+
result = await withSiteCommandOrigin(c, workspaceId, () =>
|
|
3519
|
+
acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
3520
|
+
text: payload.text,
|
|
3521
|
+
annotations: payload.annotations,
|
|
3522
|
+
modelContext: payload.modelContext ?? null,
|
|
3523
|
+
resources: payload.resources,
|
|
3524
|
+
model: payload.model ?? null,
|
|
3525
|
+
reasoningEffort: payload.reasoningEffort ?? null,
|
|
3526
|
+
latencyMode: payload.latencyMode ?? null,
|
|
3527
|
+
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
3528
|
+
connectionAccounts: payload.connectionAccounts,
|
|
3529
|
+
...(payload.personalResourceAttachment
|
|
3530
|
+
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
3531
|
+
: {}),
|
|
3532
|
+
authorization,
|
|
3533
|
+
delivery: "steer",
|
|
3534
|
+
origin: "human",
|
|
3535
|
+
...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
|
|
3536
|
+
...(payload.expectedDraftRevision !== undefined
|
|
3537
|
+
? { expectedDraftRevision: payload.expectedDraftRevision }
|
|
3538
|
+
: {}),
|
|
3539
|
+
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
3540
|
+
}),
|
|
3541
|
+
);
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
return commandConflictResponse(c, error);
|
|
3544
|
+
}
|
|
3545
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3546
|
+
return c.json(result, 202);
|
|
3547
|
+
});
|
|
3548
|
+
|
|
3549
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/composer-draft/submit", async (c) => {
|
|
3550
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3551
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
3552
|
+
c,
|
|
3553
|
+
deps,
|
|
3554
|
+
workspaceId,
|
|
3555
|
+
"sessions:control",
|
|
3556
|
+
);
|
|
3557
|
+
const grant = authorization.grant;
|
|
3558
|
+
const sessionId = c.req.param("sessionId");
|
|
3559
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
3560
|
+
const payload = parseRequestBody(
|
|
3561
|
+
SubmitComposerDraftRequest,
|
|
3562
|
+
await c.req.json().catch(() => null),
|
|
3563
|
+
);
|
|
3564
|
+
let result: Awaited<ReturnType<typeof submitComposerDraftForRequest>>;
|
|
3565
|
+
try {
|
|
3566
|
+
result = await withSiteCommandOrigin(c, workspaceId, () =>
|
|
3567
|
+
submitComposerDraftForRequest(deps, grant, workspaceId, sessionId, payload, {
|
|
3568
|
+
authorization,
|
|
3569
|
+
}),
|
|
3570
|
+
);
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
return commandConflictResponse(c, error);
|
|
3573
|
+
}
|
|
3574
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
2274
3575
|
return c.json(result, 202);
|
|
2275
3576
|
});
|
|
2276
3577
|
|
|
2277
3578
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
2278
3579
|
const workspaceId = c.req.param("workspaceId");
|
|
2279
|
-
const
|
|
3580
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
3581
|
+
c,
|
|
3582
|
+
deps,
|
|
3583
|
+
workspaceId,
|
|
3584
|
+
"sessions:control",
|
|
3585
|
+
);
|
|
3586
|
+
const grant = authorization.grant;
|
|
2280
3587
|
const sessionId = c.req.param("sessionId");
|
|
2281
3588
|
const event = parseSessionEventAdmission(await c.req.json().catch(() => null));
|
|
2282
3589
|
const refinedOperation =
|
|
@@ -2297,35 +3604,75 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2297
3604
|
}
|
|
2298
3605
|
}
|
|
2299
3606
|
if (event.type === "user.message") {
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
3607
|
+
let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
|
|
3608
|
+
try {
|
|
3609
|
+
result = await withSiteCommandOrigin(c, workspaceId, () =>
|
|
3610
|
+
acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
3611
|
+
text: event.payload.text,
|
|
3612
|
+
annotations: event.payload.annotations,
|
|
3613
|
+
modelContext: event.payload.modelContext ?? null,
|
|
3614
|
+
resources: event.payload.resources ?? [],
|
|
3615
|
+
model: event.payload.model ?? null,
|
|
3616
|
+
reasoningEffort: event.payload.reasoningEffort ?? null,
|
|
3617
|
+
latencyMode: event.payload.latencyMode ?? null,
|
|
3618
|
+
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
3619
|
+
connectionAccounts: event.payload.connectionAccounts,
|
|
3620
|
+
...(event.payload.personalResourceAttachment
|
|
3621
|
+
? { personalResourceAttachment: event.payload.personalResourceAttachment }
|
|
3622
|
+
: {}),
|
|
3623
|
+
authorization,
|
|
3624
|
+
...(event.payload.controlEtag !== undefined
|
|
3625
|
+
? { controlEtag: event.payload.controlEtag }
|
|
3626
|
+
: {}),
|
|
3627
|
+
...(event.payload.expectedDraftRevision !== undefined
|
|
3628
|
+
? { expectedDraftRevision: event.payload.expectedDraftRevision }
|
|
3629
|
+
: {}),
|
|
3630
|
+
...(event.clientEventId ? { clientEventId: event.clientEventId } : {}),
|
|
3631
|
+
}),
|
|
3632
|
+
);
|
|
3633
|
+
} catch (error) {
|
|
3634
|
+
return commandConflictResponse(c, error);
|
|
3635
|
+
}
|
|
3636
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3637
|
+
return c.json(result.accepted, 202);
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// Responses use the same header-only validation, permission and encryption
|
|
3641
|
+
// boundary as Send/Steer. Persistence belongs to the response transaction,
|
|
3642
|
+
// never a separate rotation that could commit without accepting the reply.
|
|
3643
|
+
const updates = event.payload.mcpCredentialUpdates ?? [];
|
|
3644
|
+
let mcpCredentialUpdates = [] as ReturnType<typeof validateSessionMcpCredentialUpdates>;
|
|
3645
|
+
if (updates.length) {
|
|
3646
|
+
const session = await getSession(db, workspaceId, sessionId);
|
|
3647
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
3648
|
+
mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
3649
|
+
settings,
|
|
3650
|
+
grant,
|
|
3651
|
+
session,
|
|
3652
|
+
updates,
|
|
2316
3653
|
});
|
|
2317
|
-
return c.json(accepted, 202);
|
|
2318
3654
|
}
|
|
2319
3655
|
|
|
2320
3656
|
if (event.type === "user.approvalDecision") {
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
3657
|
+
const { mcpCredentialUpdates: _writeOnlyCredentials, ...payload } = event.payload;
|
|
3658
|
+
let accepted;
|
|
3659
|
+
try {
|
|
3660
|
+
accepted = await acceptSessionApprovalDecision(db, {
|
|
3661
|
+
accountId: grant.accountId,
|
|
3662
|
+
workspaceId,
|
|
3663
|
+
sessionId,
|
|
3664
|
+
subjectId: grant.subjectId,
|
|
3665
|
+
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
3666
|
+
payload,
|
|
3667
|
+
mcpCredentialUpdates,
|
|
3668
|
+
clientEventId: event.clientEventId ?? null,
|
|
3669
|
+
});
|
|
3670
|
+
} catch (error) {
|
|
3671
|
+
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
3672
|
+
throw new HTTPException(422, { message: error.message });
|
|
3673
|
+
}
|
|
3674
|
+
throw error;
|
|
3675
|
+
}
|
|
2329
3676
|
if (accepted.action === "conflict") {
|
|
2330
3677
|
throw new HTTPException(409, {
|
|
2331
3678
|
message: `session is ${accepted.sessionStatus}; no unhandled approval is pending`,
|
|
@@ -2354,13 +3701,37 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2354
3701
|
requestId: event.payload.requestId,
|
|
2355
3702
|
response: event.payload.response,
|
|
2356
3703
|
respondedBy: grant.subjectId,
|
|
3704
|
+
canonicalHumanSession:
|
|
3705
|
+
authorization.canonicalManagedHumanSession ||
|
|
3706
|
+
authorization.canonicalLocalHumanSession ||
|
|
3707
|
+
isVerifiedDelegatedHumanAuthorization(authorization),
|
|
3708
|
+
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
2357
3709
|
clientEventId: event.clientEventId ?? null,
|
|
3710
|
+
mcpCredentialUpdates,
|
|
2358
3711
|
});
|
|
2359
3712
|
} catch (error) {
|
|
3713
|
+
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
3714
|
+
throw new HTTPException(422, { message: error.message });
|
|
3715
|
+
}
|
|
3716
|
+
if (error instanceof SkillHumanResponseError) {
|
|
3717
|
+
throw new HTTPException(
|
|
3718
|
+
error.code === "conflict" ? 409 : error.code === "forbidden" ? 403 : 422,
|
|
3719
|
+
{
|
|
3720
|
+
message: error.message,
|
|
3721
|
+
},
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
2360
3724
|
if (error instanceof HumanInputResponseValidationError) {
|
|
2361
|
-
throw new HTTPException(
|
|
2362
|
-
|
|
2363
|
-
|
|
3725
|
+
throw new HTTPException(
|
|
3726
|
+
error.code === "HUMAN_AUTH_REQUIRED"
|
|
3727
|
+
? 403
|
|
3728
|
+
: error.code === "SKIP_NOT_ALLOWED"
|
|
3729
|
+
? 409
|
|
3730
|
+
: 422,
|
|
3731
|
+
{
|
|
3732
|
+
message: error.message,
|
|
3733
|
+
},
|
|
3734
|
+
);
|
|
2364
3735
|
}
|
|
2365
3736
|
throw error;
|
|
2366
3737
|
}
|
|
@@ -2382,10 +3753,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2382
3753
|
}
|
|
2383
3754
|
if (accepted.action === "conflict") {
|
|
2384
3755
|
throw new HTTPException(409, {
|
|
2385
|
-
message:
|
|
3756
|
+
message: "human-input request is not currently actionable",
|
|
2386
3757
|
});
|
|
2387
3758
|
}
|
|
2388
|
-
return c.json(accepted.event, 202);
|
|
3759
|
+
return c.json(accepted.event, accepted.action === "completed" ? 200 : 202);
|
|
2389
3760
|
}
|
|
2390
3761
|
});
|
|
2391
3762
|
|
|
@@ -2481,7 +3852,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2481
3852
|
// would mint its relay terminal/desktop cells. Resolve the active target once
|
|
2482
3853
|
// and use the selfhosted liveness probe when it is a connected machine.
|
|
2483
3854
|
const activeSandbox = session.activeSandboxId
|
|
2484
|
-
? await getSandbox(
|
|
3855
|
+
? await getSandbox(
|
|
3856
|
+
db,
|
|
3857
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
3858
|
+
session.activeSandboxId,
|
|
3859
|
+
)
|
|
2485
3860
|
: null;
|
|
2486
3861
|
const selfhostedActive = activeSandbox?.kind === "selfhosted";
|
|
2487
3862
|
const lease = await readGroupLease(
|
|
@@ -2513,12 +3888,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2513
3888
|
// Human take-control: when the desktop is available + this policy is on
|
|
2514
3889
|
// (default), the cell is mode "interactive" — the noVNC viewer drives :0
|
|
2515
3890
|
// (x11vnc runs without -viewonly). Off → mode "read-only" (client disables
|
|
2516
|
-
// take-control).
|
|
3891
|
+
// take-control). Agent interaction uses managed ComputerSession tools.
|
|
2517
3892
|
desktopInteractive: settings.sandboxDesktopInteractive,
|
|
2518
|
-
// P4.3 computer-use: the agent drives the active display; availability
|
|
2519
|
-
// tracks the desktop tier + a desktop-capable backend.
|
|
2520
|
-
computerUseEnabled: settings.computerUseEnabled,
|
|
2521
|
-
computerUseReadOnly: settings.computerUseReadOnly,
|
|
2522
3893
|
// Graceful degrade when scoped stream credentials cannot be minted.
|
|
2523
3894
|
streamTokenSecretAvailable: !streamTokenDegraded(settings),
|
|
2524
3895
|
desktopAcknowledged: acknowledged,
|
|
@@ -2529,12 +3900,23 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2529
3900
|
|
|
2530
3901
|
let capabilities;
|
|
2531
3902
|
if (selfhostedActive && activeSandbox.enrollmentId) {
|
|
2532
|
-
const enrollment = await
|
|
3903
|
+
const [enrollment, liveConnection] = await Promise.all([
|
|
3904
|
+
getEnrollment(db, grant, activeSandbox.enrollmentId),
|
|
3905
|
+
getLiveEnrollmentConnection(db, grant, activeSandbox.enrollmentId),
|
|
3906
|
+
]);
|
|
3907
|
+
const reportedWorkspaceRoot = liveConnection?.workspaceRoot ?? enrollment?.workspaceRoot;
|
|
3908
|
+
const effectiveWorkspaceRoot = reportedWorkspaceRoot
|
|
3909
|
+
? resolveConnectedMachineWorkspaceRoot(reportedWorkspaceRoot, session.workingDir)
|
|
3910
|
+
: null;
|
|
2533
3911
|
let probeResponded = false;
|
|
2534
|
-
if (
|
|
3912
|
+
if (liveConnection?.connectionInstanceId) {
|
|
2535
3913
|
const machine = new SelfhostedSession({
|
|
2536
|
-
workspaceId,
|
|
2537
|
-
agentId:
|
|
3914
|
+
workspaceId: activeSandbox.workspaceId,
|
|
3915
|
+
agentId: liveConnection.id,
|
|
3916
|
+
connectionInstanceId: liveConnection.connectionInstanceId,
|
|
3917
|
+
// Capability negotiation only pings, so a pre-root agent may still
|
|
3918
|
+
// report upgrade guidance without exposing a false filesystem root.
|
|
3919
|
+
workspaceRoot: effectiveWorkspaceRoot ?? liveConnection.workspaceRoot ?? "/",
|
|
2538
3920
|
controlRpc: new NatsControlRpc(async () => bus.getRequestConnection()),
|
|
2539
3921
|
relay: relayConfigFromSettings(settings),
|
|
2540
3922
|
epoch: session.activeEpoch,
|
|
@@ -2550,7 +3932,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2550
3932
|
...commonNegotiation,
|
|
2551
3933
|
os: enrollment?.os ?? session.sandboxOs,
|
|
2552
3934
|
leaseEpoch: session.activeEpoch,
|
|
2553
|
-
enrollment
|
|
3935
|
+
enrollment:
|
|
3936
|
+
enrollment && effectiveWorkspaceRoot
|
|
3937
|
+
? { ...enrollment, workspaceRoot: effectiveWorkspaceRoot }
|
|
3938
|
+
: enrollment,
|
|
2554
3939
|
probeResponded,
|
|
2555
3940
|
});
|
|
2556
3941
|
} else {
|
|
@@ -2597,6 +3982,20 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2597
3982
|
// place (resolveActiveDesktopTransport), covering BOTH swap directions.
|
|
2598
3983
|
let responseCapabilities = {
|
|
2599
3984
|
...capabilities,
|
|
3985
|
+
FileSystem: {
|
|
3986
|
+
...capabilities.FileSystem,
|
|
3987
|
+
root: selfhostedActive
|
|
3988
|
+
? capabilities.FileSystem.root
|
|
3989
|
+
: selectBackend(
|
|
3990
|
+
(activeSandbox?.kind === "modal"
|
|
3991
|
+
? "modal"
|
|
3992
|
+
: session.sandboxBackend === "selfhosted" &&
|
|
3993
|
+
settings.sandboxBackend !== "selfhosted" &&
|
|
3994
|
+
settings.sandboxBackend !== "none"
|
|
3995
|
+
? settings.sandboxBackend
|
|
3996
|
+
: session.sandboxBackend) as SandboxBackend,
|
|
3997
|
+
).workspaceRoot,
|
|
3998
|
+
},
|
|
2600
3999
|
Git: {
|
|
2601
4000
|
...capabilities.Git,
|
|
2602
4001
|
repos: capabilities.Git.available ? repositoryRoots : [],
|
|
@@ -2612,6 +4011,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2612
4011
|
DesktopStream: { ...capabilities.DesktopStream, ...wire },
|
|
2613
4012
|
};
|
|
2614
4013
|
}
|
|
4014
|
+
if (selfhostedActive) {
|
|
4015
|
+
const currentPointer = await readActiveSandbox(db, workspaceId, sessionId);
|
|
4016
|
+
if (
|
|
4017
|
+
!currentPointer ||
|
|
4018
|
+
currentPointer.activeSandboxId !== session.activeSandboxId ||
|
|
4019
|
+
currentPointer.activeEpoch !== session.activeEpoch ||
|
|
4020
|
+
currentPointer.workingDir !== session.workingDir
|
|
4021
|
+
) {
|
|
4022
|
+
throw new ApiHttpError(409, {
|
|
4023
|
+
code: "conflict",
|
|
4024
|
+
message: "sandbox route changed while capabilities were being negotiated; retry",
|
|
4025
|
+
retryable: true,
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
2615
4029
|
return c.json(responseCapabilities);
|
|
2616
4030
|
});
|
|
2617
4031
|
|
|
@@ -2723,7 +4137,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2723
4137
|
// attachViewer (it warms the Modal group box — the wrong target). Synthesize a
|
|
2724
4138
|
// result shaped like ViewerAttachResult and mint relay cells directly.
|
|
2725
4139
|
const activeSandbox = session.activeSandboxId
|
|
2726
|
-
? await getSandbox(
|
|
4140
|
+
? await getSandbox(
|
|
4141
|
+
db,
|
|
4142
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
4143
|
+
session.activeSandboxId,
|
|
4144
|
+
)
|
|
2727
4145
|
: null;
|
|
2728
4146
|
const selfhostedActive = activeSandbox?.kind === "selfhosted";
|
|
2729
4147
|
|
|
@@ -2753,6 +4171,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2753
4171
|
stream = await mintDesktopStream(viewerServices, {
|
|
2754
4172
|
accountId: grant.accountId,
|
|
2755
4173
|
workspaceId,
|
|
4174
|
+
resourceSubjectId: grant.subjectId,
|
|
4175
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2756
4176
|
session,
|
|
2757
4177
|
viewerId,
|
|
2758
4178
|
// No Modal lease for selfhosted-active; the mint routes to the relay.
|
|
@@ -2762,6 +4182,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2762
4182
|
terminal = await mintTerminalStream(viewerServices, {
|
|
2763
4183
|
accountId: grant.accountId,
|
|
2764
4184
|
workspaceId,
|
|
4185
|
+
resourceSubjectId: grant.subjectId,
|
|
4186
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2765
4187
|
session,
|
|
2766
4188
|
viewerId,
|
|
2767
4189
|
// No Modal lease for selfhosted-active; the mint routes to the relay.
|
|
@@ -2773,6 +4195,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2773
4195
|
accountId: grant.accountId,
|
|
2774
4196
|
workspaceId,
|
|
2775
4197
|
session,
|
|
4198
|
+
viewerSubjectId: grant.subjectId,
|
|
2776
4199
|
waitSignal: c.req.raw.signal,
|
|
2777
4200
|
...(parsed.data.viewerId ? { viewerId: parsed.data.viewerId } : {}),
|
|
2778
4201
|
});
|
|
@@ -2800,6 +4223,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2800
4223
|
stream = await mintDesktopStream(viewerServices, {
|
|
2801
4224
|
accountId: grant.accountId,
|
|
2802
4225
|
workspaceId,
|
|
4226
|
+
resourceSubjectId: grant.subjectId,
|
|
4227
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2803
4228
|
session,
|
|
2804
4229
|
viewerId: result.viewerId,
|
|
2805
4230
|
lease,
|
|
@@ -2809,6 +4234,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2809
4234
|
terminal = await mintTerminalStream(viewerServices, {
|
|
2810
4235
|
accountId: grant.accountId,
|
|
2811
4236
|
workspaceId,
|
|
4237
|
+
resourceSubjectId: grant.subjectId,
|
|
4238
|
+
resourceSubjectDelegated: grant.metadata?.delegated === true,
|
|
2812
4239
|
session,
|
|
2813
4240
|
viewerId: result.viewerId,
|
|
2814
4241
|
lease,
|
|
@@ -2951,6 +4378,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2951
4378
|
// (rides files:read); Terminal exec + PTY ride terminal:attach.
|
|
2952
4379
|
|
|
2953
4380
|
type ChannelARouteCtx = ChannelAContext & {
|
|
4381
|
+
grant: AccessGrant;
|
|
2954
4382
|
waitSignal: AbortSignal;
|
|
2955
4383
|
operation: ChannelAOperation;
|
|
2956
4384
|
};
|
|
@@ -2971,6 +4399,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2971
4399
|
throw new HTTPException(404, { message: "session not found" });
|
|
2972
4400
|
}
|
|
2973
4401
|
return {
|
|
4402
|
+
grant,
|
|
2974
4403
|
accountId: grant.accountId,
|
|
2975
4404
|
workspaceId,
|
|
2976
4405
|
session,
|
|
@@ -3020,6 +4449,36 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3020
4449
|
return c.json(out);
|
|
3021
4450
|
});
|
|
3022
4451
|
|
|
4452
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/read-workspace", async (c) => {
|
|
4453
|
+
const ctx = await channelAPreamble(c, "files:read", "fs.read");
|
|
4454
|
+
const req = await parseChannelABody(c, FsReadRequest);
|
|
4455
|
+
const out = await withChannelARead(channelAServices, ctx, ({ service }) =>
|
|
4456
|
+
service.fsRead({ ...req, workspaceOnly: true }),
|
|
4457
|
+
);
|
|
4458
|
+
return c.json(out);
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4461
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/publish", async (c) => {
|
|
4462
|
+
const workspaceId = c.req.param("workspaceId");
|
|
4463
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
4464
|
+
c,
|
|
4465
|
+
deps,
|
|
4466
|
+
workspaceId,
|
|
4467
|
+
"files:upload",
|
|
4468
|
+
);
|
|
4469
|
+
const ctx = await channelAPreamble(c, "files:read", "artifact.publish");
|
|
4470
|
+
const request = await parseChannelABody(c, PublishSandboxFileArtifactRequest);
|
|
4471
|
+
return c.json(
|
|
4472
|
+
await publishSandboxFileArtifact(deps, {
|
|
4473
|
+
grant: ctx.grant,
|
|
4474
|
+
authorization,
|
|
4475
|
+
session: ctx.session,
|
|
4476
|
+
path: request.path,
|
|
4477
|
+
signal: ctx.waitSignal,
|
|
4478
|
+
}),
|
|
4479
|
+
);
|
|
4480
|
+
});
|
|
4481
|
+
|
|
3023
4482
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/write", async (c) => {
|
|
3024
4483
|
const ctx = await channelAPreamble(c, "files:write", "fs.write");
|
|
3025
4484
|
const req = await parseChannelABody(c, FsWriteRequest);
|
|
@@ -3142,7 +4601,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3142
4601
|
// the live/wake path — the feature degrades to today's behavior, never worse.
|
|
3143
4602
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture", async (c) => {
|
|
3144
4603
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
3145
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
4604
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
3146
4605
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
3147
4606
|
const lookup = await sessionLatestWorkspaceCapture(db, workspaceId, sessionId);
|
|
3148
4607
|
if (!lookup.sessionExists) {
|
|
@@ -3153,13 +4612,27 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3153
4612
|
return c.json({ available: false });
|
|
3154
4613
|
}
|
|
3155
4614
|
return c.json(
|
|
3156
|
-
await serveWorkspaceCapture(
|
|
4615
|
+
await serveWorkspaceCapture(
|
|
4616
|
+
lookup.capture,
|
|
4617
|
+
objectStorage,
|
|
4618
|
+
workspaceCaptureManifestCache,
|
|
4619
|
+
(fact) =>
|
|
4620
|
+
recordAuditEvent(db, {
|
|
4621
|
+
accountId: grant.accountId,
|
|
4622
|
+
workspaceId,
|
|
4623
|
+
subjectId: grant.subjectId,
|
|
4624
|
+
action: "file.signed_url.issued",
|
|
4625
|
+
targetType: "workspace_capture",
|
|
4626
|
+
targetId: sessionId,
|
|
4627
|
+
metadata: { sessionId, ...fact },
|
|
4628
|
+
}),
|
|
4629
|
+
),
|
|
3157
4630
|
);
|
|
3158
4631
|
});
|
|
3159
4632
|
|
|
3160
4633
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture/file", async (c) => {
|
|
3161
4634
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
3162
|
-
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
4635
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
3163
4636
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
3164
4637
|
const path = c.req.query("path");
|
|
3165
4638
|
if (!path) {
|
|
@@ -3188,7 +4661,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3188
4661
|
} else {
|
|
3189
4662
|
row = await latestWorkspaceCapture(db, workspaceId, sessionId);
|
|
3190
4663
|
}
|
|
3191
|
-
return c.json(
|
|
4664
|
+
return c.json(
|
|
4665
|
+
await serveWorkspaceCaptureFile(row, path, objectStorage, (fact) =>
|
|
4666
|
+
recordAuditEvent(db, {
|
|
4667
|
+
accountId: grant.accountId,
|
|
4668
|
+
workspaceId,
|
|
4669
|
+
subjectId: grant.subjectId,
|
|
4670
|
+
action: "file.signed_url.issued",
|
|
4671
|
+
targetType: "workspace_capture",
|
|
4672
|
+
targetId: sessionId,
|
|
4673
|
+
// The captured PATH is workspace-file identity (like a filename),
|
|
4674
|
+
// not content; the signed URL and object key are never recorded.
|
|
4675
|
+
metadata: { sessionId, path, ...fact },
|
|
4676
|
+
}),
|
|
4677
|
+
),
|
|
4678
|
+
);
|
|
3192
4679
|
});
|
|
3193
4680
|
|
|
3194
4681
|
// ── Terminal: synchronous exec ────────────────────────────────────────────
|
|
@@ -3510,15 +4997,30 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3510
4997
|
if (suffix === "") {
|
|
3511
4998
|
if (verb === "GET") return "session.read";
|
|
3512
4999
|
if (verb === "PATCH") return "session.title.write";
|
|
5000
|
+
if (verb === "DELETE") return "session.delete";
|
|
3513
5001
|
return null;
|
|
3514
5002
|
}
|
|
3515
5003
|
if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
|
|
5004
|
+
if (suffix === "/attention" && verb === "PUT") return "session.attention.write";
|
|
5005
|
+
if (suffix === "/archive" && verb === "PUT") return "session.archive.write";
|
|
5006
|
+
if (suffix === "/visibility" && verb === "PUT") return "session.visibility.write";
|
|
5007
|
+
if (suffix === "/forks" && verb === "POST") return "session.fork.create";
|
|
3516
5008
|
if (suffix === "/channel" && verb === "PUT") return "session.channel.write";
|
|
5009
|
+
if (suffix === "/variable-sets" && verb === "PUT") return "session.variable_sets.write";
|
|
3517
5010
|
if (suffix === "/tool-policy" && verb === "PUT") return "session.tool_policy.write";
|
|
5011
|
+
if (suffix === "/agent" && verb === "PUT") return "session.tool_policy.write";
|
|
5012
|
+
if (suffix === "/mcp-credentials/rotate" && verb === "POST")
|
|
5013
|
+
return "session.mcp.credentials.rotate";
|
|
3518
5014
|
if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
|
|
3519
5015
|
return "session.mcp.approval_policy.write";
|
|
3520
5016
|
}
|
|
3521
5017
|
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
5018
|
+
if (suffix === "/background-commands" && verb === "GET") return "session.read";
|
|
5019
|
+
if (suffix === "/model-context" && verb === "GET") return "session.read";
|
|
5020
|
+
if (suffix === "/codex-accounts" && verb === "GET") return "session.read";
|
|
5021
|
+
if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
|
|
5022
|
+
return "session.control";
|
|
5023
|
+
}
|
|
3522
5024
|
if (suffix === "/codex-account" && verb === "POST") {
|
|
3523
5025
|
return "session.codex_account.write";
|
|
3524
5026
|
}
|
|
@@ -3554,13 +5056,19 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3554
5056
|
: null;
|
|
3555
5057
|
}
|
|
3556
5058
|
if (suffix === "/goal/revisions" && verb === "GET") return "session.goal.read";
|
|
5059
|
+
if (suffix === "/goal/revisions/page" && verb === "GET") return "session.goal.read";
|
|
3557
5060
|
if (/^\/goal\/revisions\/[^/]+\/apply$/.test(suffix) && verb === "POST") {
|
|
3558
5061
|
return "session.goal.write";
|
|
3559
5062
|
}
|
|
5063
|
+
if (/^\/goal\/revisions\/[^/]+\/(reject|rollback)$/.test(suffix) && verb === "POST") {
|
|
5064
|
+
return "session.goal.write";
|
|
5065
|
+
}
|
|
3560
5066
|
if (suffix === "/context/clear" || suffix === "/context/compact") {
|
|
3561
5067
|
return verb === "POST" ? "session.context.write" : null;
|
|
3562
5068
|
}
|
|
3563
5069
|
if (suffix === "/events/stream" && verb === "GET") return "session.stream.read";
|
|
5070
|
+
if (/^\/events\/[^/]+\/message-preview$/.test(suffix) && verb === "GET")
|
|
5071
|
+
return "session.events.read";
|
|
3564
5072
|
if (suffix === "/events") {
|
|
3565
5073
|
if (verb === "GET") return "session.events.read";
|
|
3566
5074
|
if (verb === "POST") return "session.append";
|
|
@@ -3574,7 +5082,12 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3574
5082
|
if (verb === "PUT") return "session.composer.write";
|
|
3575
5083
|
return null;
|
|
3576
5084
|
}
|
|
5085
|
+
// Same HTTP gate as POST /events. Delivery `steer` is re-authorized inside
|
|
5086
|
+
// acceptSessionUserMessageWithOutcome after the body is parsed.
|
|
5087
|
+
if (suffix === "/composer-draft/submit" && verb === "POST") return "session.append";
|
|
3577
5088
|
if (suffix === "/control" && verb === "POST") return "session.control";
|
|
5089
|
+
if (suffix === "/retry" && verb === "POST") return "session.control";
|
|
5090
|
+
if (suffix === "/sandbox-recovery" && ["GET", "POST"].includes(verb)) return "session.control";
|
|
3578
5091
|
if (suffix === "/steer" && verb === "POST") return "session.steer";
|
|
3579
5092
|
if (suffix === "/human-input-requests" && verb === "GET") {
|
|
3580
5093
|
return "session.human_input.read";
|
|
@@ -3590,9 +5103,16 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
3590
5103
|
if (suffix.startsWith("/viewers/") && ["POST", "DELETE"].includes(verb)) {
|
|
3591
5104
|
return "session.viewer.control";
|
|
3592
5105
|
}
|
|
3593
|
-
if (
|
|
5106
|
+
if (
|
|
5107
|
+
suffix === "/fs/list" ||
|
|
5108
|
+
suffix === "/fs/list-batch" ||
|
|
5109
|
+
suffix === "/fs/read" ||
|
|
5110
|
+
suffix === "/fs/read-workspace"
|
|
5111
|
+
) {
|
|
3594
5112
|
return verb === "POST" ? "session.files.read" : null;
|
|
3595
5113
|
}
|
|
5114
|
+
if (suffix === "/artifacts/publish" && verb === "POST") return "session.files.write";
|
|
5115
|
+
if (suffix.startsWith("/artifact-associations/") && verb === "GET") return "session.read";
|
|
3596
5116
|
if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
|
|
3597
5117
|
return verb === "POST" ? "session.files.write" : null;
|
|
3598
5118
|
}
|
|
@@ -3621,6 +5141,70 @@ function sessionAuthorizationHttpError(error: unknown): HTTPException {
|
|
|
3621
5141
|
throw error;
|
|
3622
5142
|
}
|
|
3623
5143
|
|
|
5144
|
+
export function sessionTenancyHttpError(error: unknown): Error {
|
|
5145
|
+
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
5146
|
+
return new ApiHttpError(403, {
|
|
5147
|
+
code: "forbidden",
|
|
5148
|
+
message: "Session tenancy mutations require the owning managed-human session.",
|
|
5149
|
+
retryable: false,
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
5152
|
+
if (error instanceof SessionTenancyNotActivatedError) {
|
|
5153
|
+
return new ApiHttpError(409, {
|
|
5154
|
+
code: "conflict",
|
|
5155
|
+
message: "Session tenancy is not activated for this organization.",
|
|
5156
|
+
retryable: false,
|
|
5157
|
+
details: { reason: "not_activated" },
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
5160
|
+
if (error instanceof SessionTenancyConflictError) {
|
|
5161
|
+
return new ApiHttpError(409, {
|
|
5162
|
+
code: error.reason === "operation_reuse" ? "idempotency_conflict" : "conflict",
|
|
5163
|
+
message:
|
|
5164
|
+
error.reason === "not_quiescent"
|
|
5165
|
+
? "The session must be fully quiescent before this change."
|
|
5166
|
+
: error.reason === "authority_epoch"
|
|
5167
|
+
? "The session authority changed before this operation committed."
|
|
5168
|
+
: "The idempotency key was already used with different input.",
|
|
5169
|
+
retryable: false,
|
|
5170
|
+
details: {
|
|
5171
|
+
reason: error.reason,
|
|
5172
|
+
...(error.blocker ? { blocker: error.blocker } : {}),
|
|
5173
|
+
},
|
|
5174
|
+
});
|
|
5175
|
+
}
|
|
5176
|
+
if (
|
|
5177
|
+
error instanceof SessionTenancyAccessError ||
|
|
5178
|
+
error instanceof SessionAuthorizationDeniedError
|
|
5179
|
+
) {
|
|
5180
|
+
return new ApiHttpError(404, {
|
|
5181
|
+
code: "not_found",
|
|
5182
|
+
message: "Session not found.",
|
|
5183
|
+
retryable: false,
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
5186
|
+
if (error instanceof SessionTenancyInvalidRequestError) {
|
|
5187
|
+
return new ApiHttpError(422, {
|
|
5188
|
+
code: "validation_failed",
|
|
5189
|
+
message: "Invalid session tenancy request.",
|
|
5190
|
+
retryable: false,
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
if (error instanceof SessionTenancyPersistenceOutcomeUnknownError) {
|
|
5194
|
+
return new ApiHttpError(503, {
|
|
5195
|
+
code: "upstream_unavailable",
|
|
5196
|
+
message:
|
|
5197
|
+
"The session tenancy mutation outcome is unknown. Retry with the same idempotency key.",
|
|
5198
|
+
retryable: true,
|
|
5199
|
+
outcomeUnknown: true,
|
|
5200
|
+
});
|
|
5201
|
+
}
|
|
5202
|
+
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
5203
|
+
return sessionAuthorizationHttpError(error);
|
|
5204
|
+
}
|
|
5205
|
+
return error instanceof Error ? error : new Error("Unknown session tenancy error");
|
|
5206
|
+
}
|
|
5207
|
+
|
|
3624
5208
|
function eventEnumValue<T extends string>(
|
|
3625
5209
|
raw: string | undefined,
|
|
3626
5210
|
schema: { safeParse(value: unknown): { success: boolean; data?: T } },
|
|
@@ -3673,17 +5257,35 @@ function eventEnumList<T extends string>(
|
|
|
3673
5257
|
});
|
|
3674
5258
|
}
|
|
3675
5259
|
|
|
3676
|
-
function sessionListQuery(
|
|
5260
|
+
export function sessionListQuery(
|
|
3677
5261
|
query: Record<string, string>,
|
|
3678
5262
|
allowCursor = true,
|
|
3679
5263
|
): {
|
|
5264
|
+
originSiteId: string | undefined;
|
|
3680
5265
|
limit: string | undefined;
|
|
3681
5266
|
parentSessionId: string | null | undefined;
|
|
3682
5267
|
cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
|
|
3683
5268
|
search: string | undefined;
|
|
3684
5269
|
pinsOnly: boolean;
|
|
5270
|
+
includePinned: boolean;
|
|
5271
|
+
includeTotals: boolean;
|
|
5272
|
+
needsYouOnly: boolean;
|
|
5273
|
+
archivedOnly: boolean;
|
|
5274
|
+
sortBy: "updatedAt" | "createdAt" | "name" | undefined;
|
|
5275
|
+
archiveStatus: "active" | "archived" | "all" | undefined;
|
|
5276
|
+
channelId: string | null | undefined;
|
|
5277
|
+
createdBy: { kind: "subject" | "service"; subjectId: string } | undefined;
|
|
5278
|
+
updatedFrom: Date | undefined;
|
|
5279
|
+
updatedBefore: Date | undefined;
|
|
5280
|
+
createdFrom: Date | undefined;
|
|
5281
|
+
createdBefore: Date | undefined;
|
|
5282
|
+
scopeSubjectId: SessionScopeSubjectId | undefined;
|
|
5283
|
+
hasPageFilters: boolean;
|
|
3685
5284
|
} {
|
|
3686
5285
|
const parentSessionId = query.parentSessionId;
|
|
5286
|
+
const originSiteId = query.originSiteId;
|
|
5287
|
+
if (originSiteId !== undefined && !z.string().uuid().safeParse(originSiteId).success)
|
|
5288
|
+
throw new HTTPException(400, { message: "originSiteId must be a Site id" });
|
|
3687
5289
|
// "null" = roots only; a uuid = children of that session; anything else is
|
|
3688
5290
|
// a client error (an unvalidated value would surface as a Postgres uuid cast
|
|
3689
5291
|
// failure -> 500 rather than an honest 400).
|
|
@@ -3713,16 +5315,136 @@ function sessionListQuery(
|
|
|
3713
5315
|
});
|
|
3714
5316
|
}
|
|
3715
5317
|
const pinsOnly = query.pinsOnly === "true";
|
|
5318
|
+
if (query.includePinned !== undefined && !["true", "false"].includes(query.includePinned)) {
|
|
5319
|
+
throw new HTTPException(400, { message: 'includePinned must be "true" or "false"' });
|
|
5320
|
+
}
|
|
5321
|
+
const includePinned = query.includePinned !== "false";
|
|
5322
|
+
for (const key of ["includeTotals", "needsYouOnly"]) {
|
|
5323
|
+
if (query[key] !== undefined && !["true", "false"].includes(query[key]!))
|
|
5324
|
+
throw new HTTPException(400, { message: `${key} must be "true" or "false"` });
|
|
5325
|
+
}
|
|
5326
|
+
const includeTotals = query.includeTotals === "true";
|
|
5327
|
+
const needsYouOnly = query.needsYouOnly === "true";
|
|
5328
|
+
if (includeTotals && (!allowCursor || (parentSessionId !== "null" && !pinsOnly)))
|
|
5329
|
+
throw new HTTPException(400, { message: "includeTotals requires a root page" });
|
|
5330
|
+
if (needsYouOnly && !allowCursor)
|
|
5331
|
+
throw new HTTPException(400, { message: 'needsYouOnly requires view="page"' });
|
|
5332
|
+
if (pinsOnly && !includePinned) {
|
|
5333
|
+
throw new HTTPException(400, { message: "pinsOnly requires includePinned" });
|
|
5334
|
+
}
|
|
5335
|
+
if (query.archivedOnly !== undefined && query.archivedOnly !== "true") {
|
|
5336
|
+
throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
|
|
5337
|
+
}
|
|
5338
|
+
const archivedOnly = query.archivedOnly === "true";
|
|
5339
|
+
const sortBy = z.enum(["updatedAt", "createdAt", "name"]).optional().safeParse(query.sortBy);
|
|
5340
|
+
const archiveStatus = z
|
|
5341
|
+
.enum(["active", "archived", "all"])
|
|
5342
|
+
.optional()
|
|
5343
|
+
.safeParse(query.archiveStatus);
|
|
5344
|
+
if (!sortBy.success || !archiveStatus.success) {
|
|
5345
|
+
throw new HTTPException(400, { message: "Invalid session sortBy or archiveStatus" });
|
|
5346
|
+
}
|
|
5347
|
+
if (archivedOnly && archiveStatus.data !== undefined && archiveStatus.data !== "archived") {
|
|
5348
|
+
throw new HTTPException(400, { message: "archivedOnly conflicts with archiveStatus" });
|
|
5349
|
+
}
|
|
5350
|
+
if (pinsOnly && archiveStatus.data === "archived") {
|
|
5351
|
+
throw new HTTPException(400, { message: "pinsOnly cannot be combined with archived status" });
|
|
5352
|
+
}
|
|
5353
|
+
const channelId = query.channelId;
|
|
5354
|
+
if (
|
|
5355
|
+
channelId !== undefined &&
|
|
5356
|
+
channelId !== "null" &&
|
|
5357
|
+
!z.string().uuid().safeParse(channelId).success
|
|
5358
|
+
) {
|
|
5359
|
+
throw new HTTPException(400, {
|
|
5360
|
+
message: 'channelId must be a channel id or the literal "null"',
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
const createdByKind = query.createdByKind;
|
|
5364
|
+
const createdBySubjectId = query.createdBySubjectId;
|
|
5365
|
+
if ((createdByKind === undefined) !== (createdBySubjectId === undefined)) {
|
|
5366
|
+
throw new HTTPException(400, {
|
|
5367
|
+
message: "createdByKind and createdBySubjectId must be supplied together",
|
|
5368
|
+
});
|
|
5369
|
+
}
|
|
5370
|
+
if (createdByKind !== undefined && createdByKind !== "subject" && createdByKind !== "service") {
|
|
5371
|
+
throw new HTTPException(400, {
|
|
5372
|
+
message: 'createdByKind must be "subject" or "service"',
|
|
5373
|
+
});
|
|
5374
|
+
}
|
|
5375
|
+
if (
|
|
5376
|
+
createdBySubjectId !== undefined &&
|
|
5377
|
+
(createdBySubjectId.trim().length < 1 || createdBySubjectId.length > 1_024)
|
|
5378
|
+
) {
|
|
5379
|
+
throw new HTTPException(400, {
|
|
5380
|
+
message: "createdBySubjectId must be between 1 and 1024 characters",
|
|
5381
|
+
});
|
|
5382
|
+
}
|
|
5383
|
+
const parseDateBound = (name: string): Date | undefined => {
|
|
5384
|
+
const raw = query[name];
|
|
5385
|
+
if (raw === undefined) return undefined;
|
|
5386
|
+
const parsed = z.string().datetime({ offset: true }).safeParse(raw);
|
|
5387
|
+
if (!parsed.success) {
|
|
5388
|
+
throw new HTTPException(400, { message: `${name} must be an ISO-8601 timestamp` });
|
|
5389
|
+
}
|
|
5390
|
+
// Date-backed filters must not silently round accepted microseconds to an
|
|
5391
|
+
// earlier boundary. Clients can use any ISO offset with at most 3 decimals.
|
|
5392
|
+
if (/\.\d{4,}/.test(parsed.data)) {
|
|
5393
|
+
throw new HTTPException(400, { message: `${name} supports at most millisecond precision` });
|
|
5394
|
+
}
|
|
5395
|
+
return new Date(parsed.data);
|
|
5396
|
+
};
|
|
5397
|
+
const updatedFrom = parseDateBound("updatedFrom");
|
|
5398
|
+
const updatedBefore = parseDateBound("updatedBefore");
|
|
5399
|
+
const createdFrom = parseDateBound("createdFrom");
|
|
5400
|
+
const createdBefore = parseDateBound("createdBefore");
|
|
5401
|
+
if (updatedFrom && updatedBefore && updatedFrom >= updatedBefore) {
|
|
5402
|
+
throw new HTTPException(400, { message: "updatedFrom must be earlier than updatedBefore" });
|
|
5403
|
+
}
|
|
5404
|
+
if (createdFrom && createdBefore && createdFrom >= createdBefore) {
|
|
5405
|
+
throw new HTTPException(400, { message: "createdFrom must be earlier than createdBefore" });
|
|
5406
|
+
}
|
|
5407
|
+
// The opaque end-user label filter is an exact pair: one half alone is a
|
|
5408
|
+
// client error rather than a silently unfiltered list.
|
|
5409
|
+
if (query.endUserSource !== undefined || query.endUserId !== undefined) {
|
|
5410
|
+
throw new HTTPException(400, {
|
|
5411
|
+
message: "Use canonical scopeSubjectId, not an external-user label",
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5414
|
+
let scopeSubjectId: SessionScopeSubjectId | undefined;
|
|
5415
|
+
if (query.scopeSubjectId !== undefined) {
|
|
5416
|
+
const parsedEndUser = SessionScopeSubjectId.safeParse(query.scopeSubjectId);
|
|
5417
|
+
if (!parsedEndUser.success) {
|
|
5418
|
+
throw new HTTPException(400, {
|
|
5419
|
+
message: "scopeSubjectId must be a canonical Opengeni user subject",
|
|
5420
|
+
});
|
|
5421
|
+
}
|
|
5422
|
+
scopeSubjectId = parsedEndUser.data;
|
|
5423
|
+
}
|
|
5424
|
+
const hasPageFilters =
|
|
5425
|
+
needsYouOnly ||
|
|
5426
|
+
originSiteId !== undefined ||
|
|
5427
|
+
channelId !== undefined ||
|
|
5428
|
+
createdByKind !== undefined ||
|
|
5429
|
+
updatedFrom !== undefined ||
|
|
5430
|
+
updatedBefore !== undefined ||
|
|
5431
|
+
createdFrom !== undefined ||
|
|
5432
|
+
createdBefore !== undefined ||
|
|
5433
|
+
scopeSubjectId !== undefined;
|
|
3716
5434
|
if (pinsOnly && !allowCursor) {
|
|
3717
5435
|
throw new HTTPException(400, { message: 'pinsOnly requires view="page"' });
|
|
3718
5436
|
}
|
|
3719
|
-
if (pinsOnly && (rawCursor || parentSessionId !== undefined || search)) {
|
|
5437
|
+
if (pinsOnly && (rawCursor || parentSessionId !== undefined || search || hasPageFilters)) {
|
|
3720
5438
|
throw new HTTPException(400, {
|
|
3721
|
-
message: "pinsOnly cannot be combined with cursor, parentSessionId, or
|
|
5439
|
+
message: "pinsOnly cannot be combined with cursor, parentSessionId, search, or filters",
|
|
3722
5440
|
});
|
|
3723
5441
|
}
|
|
5442
|
+
if (pinsOnly && archivedOnly) {
|
|
5443
|
+
throw new HTTPException(400, { message: "pinsOnly cannot be combined with archivedOnly" });
|
|
5444
|
+
}
|
|
3724
5445
|
return {
|
|
3725
5446
|
limit: query.limit,
|
|
5447
|
+
originSiteId,
|
|
3726
5448
|
parentSessionId:
|
|
3727
5449
|
parentSessionId === undefined
|
|
3728
5450
|
? undefined
|
|
@@ -3732,17 +5454,40 @@ function sessionListQuery(
|
|
|
3732
5454
|
cursor,
|
|
3733
5455
|
search: search || undefined,
|
|
3734
5456
|
pinsOnly,
|
|
5457
|
+
includePinned,
|
|
5458
|
+
includeTotals,
|
|
5459
|
+
needsYouOnly,
|
|
5460
|
+
archivedOnly,
|
|
5461
|
+
sortBy: sortBy.data,
|
|
5462
|
+
archiveStatus: archiveStatus.data,
|
|
5463
|
+
channelId: channelId === undefined ? undefined : channelId === "null" ? null : channelId,
|
|
5464
|
+
createdBy:
|
|
5465
|
+
createdByKind && createdBySubjectId
|
|
5466
|
+
? { kind: createdByKind, subjectId: createdBySubjectId }
|
|
5467
|
+
: undefined,
|
|
5468
|
+
updatedFrom,
|
|
5469
|
+
updatedBefore,
|
|
5470
|
+
createdFrom,
|
|
5471
|
+
createdBefore,
|
|
5472
|
+
scopeSubjectId,
|
|
5473
|
+
hasPageFilters,
|
|
3735
5474
|
};
|
|
3736
5475
|
}
|
|
3737
5476
|
|
|
3738
5477
|
export type AgentTopologyCursorEnvelope = {
|
|
3739
5478
|
cursor: SessionDiscoveryCursor;
|
|
3740
|
-
parentSessionId: string | null;
|
|
3741
|
-
|
|
5479
|
+
parentSessionId: string | null | "all";
|
|
5480
|
+
rootSessionId: string | null;
|
|
5481
|
+
query: string | null;
|
|
5482
|
+
statuses: SessionStatus[];
|
|
5483
|
+
activeOnly: boolean;
|
|
5484
|
+
recentHours: number | null;
|
|
5485
|
+
subject: WorkClaimSubjectFilter | null;
|
|
5486
|
+
claimLimit: number | null;
|
|
3742
5487
|
};
|
|
3743
5488
|
|
|
3744
5489
|
export function encodeAgentTopologyCursor(value: AgentTopologyCursorEnvelope): string {
|
|
3745
|
-
return Buffer.from(JSON.stringify({ v:
|
|
5490
|
+
return Buffer.from(JSON.stringify({ v: 2, ...value }), "utf8").toString("base64url");
|
|
3746
5491
|
}
|
|
3747
5492
|
|
|
3748
5493
|
function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
@@ -3752,25 +5497,92 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
3752
5497
|
});
|
|
3753
5498
|
}
|
|
3754
5499
|
try {
|
|
3755
|
-
const
|
|
5500
|
+
const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as unknown;
|
|
5501
|
+
const legacy = z
|
|
3756
5502
|
.object({
|
|
3757
5503
|
v: z.literal(1),
|
|
3758
5504
|
parentSessionId: z.string().uuid().nullable(),
|
|
3759
|
-
search: z.string().max(
|
|
5505
|
+
search: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
5506
|
+
cursor: z.object({
|
|
5507
|
+
orderBy: z.literal("updatedAt"),
|
|
5508
|
+
sortRevision: z.string().max(64),
|
|
5509
|
+
sortAt: z.string().max(64),
|
|
5510
|
+
id: z.string().uuid(),
|
|
5511
|
+
snapshotAt: z.string().max(64),
|
|
5512
|
+
snapshotRevision: z.string().max(64),
|
|
5513
|
+
updatedAfter: z.null(),
|
|
5514
|
+
}),
|
|
5515
|
+
})
|
|
5516
|
+
.safeParse(decoded);
|
|
5517
|
+
if (legacy.success) {
|
|
5518
|
+
if (legacy.data.search !== null) {
|
|
5519
|
+
throw new Error("legacy search cursor is not relevance-fenced");
|
|
5520
|
+
}
|
|
5521
|
+
const cursor = {
|
|
5522
|
+
...legacy.data.cursor,
|
|
5523
|
+
sortRank: null,
|
|
5524
|
+
filterHash: null,
|
|
5525
|
+
} satisfies SessionDiscoveryCursor;
|
|
5526
|
+
return {
|
|
5527
|
+
cursor,
|
|
5528
|
+
parentSessionId: legacy.data.parentSessionId,
|
|
5529
|
+
rootSessionId: null,
|
|
5530
|
+
query: null,
|
|
5531
|
+
statuses: [],
|
|
5532
|
+
activeOnly: false,
|
|
5533
|
+
recentHours: null,
|
|
5534
|
+
subject: null,
|
|
5535
|
+
claimLimit: null,
|
|
5536
|
+
};
|
|
5537
|
+
}
|
|
5538
|
+
const parsed = z
|
|
5539
|
+
.object({
|
|
5540
|
+
v: z.literal(2),
|
|
5541
|
+
parentSessionId: z.union([z.string().uuid(), z.literal("all"), z.null()]),
|
|
5542
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
5543
|
+
query: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
5544
|
+
statuses: z
|
|
5545
|
+
.array(
|
|
5546
|
+
z.enum([
|
|
5547
|
+
"queued",
|
|
5548
|
+
"running",
|
|
5549
|
+
"idle",
|
|
5550
|
+
"requires_action",
|
|
5551
|
+
"recovering",
|
|
5552
|
+
"waiting_capacity",
|
|
5553
|
+
"failed",
|
|
5554
|
+
"cancelled",
|
|
5555
|
+
]),
|
|
5556
|
+
)
|
|
5557
|
+
.max(8),
|
|
5558
|
+
activeOnly: z.boolean(),
|
|
5559
|
+
recentHours: z.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).nullable(),
|
|
5560
|
+
subject: z
|
|
5561
|
+
.object({
|
|
5562
|
+
namespace: z.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
|
|
5563
|
+
type: WorkClaimSubjectType,
|
|
5564
|
+
canonicalKey: z.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
|
|
5565
|
+
})
|
|
5566
|
+
.strict()
|
|
5567
|
+
.nullable(),
|
|
5568
|
+
claimLimit: z.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).nullable(),
|
|
3760
5569
|
cursor: z.object({
|
|
3761
|
-
orderBy: z.enum(["
|
|
5570
|
+
orderBy: z.enum(["updatedAt", "relevance"]),
|
|
5571
|
+
sortRank: z.number().int().nonnegative().nullable(),
|
|
3762
5572
|
sortRevision: z.string().max(64),
|
|
3763
5573
|
sortAt: z.string().max(64),
|
|
3764
5574
|
id: z.string().uuid(),
|
|
3765
5575
|
snapshotAt: z.string().max(64),
|
|
3766
5576
|
snapshotRevision: z.string().max(64),
|
|
3767
|
-
updatedAfter: z.
|
|
5577
|
+
updatedAfter: z.null(),
|
|
5578
|
+
filterHash: z
|
|
5579
|
+
.string()
|
|
5580
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
5581
|
+
.nullable(),
|
|
3768
5582
|
}),
|
|
3769
5583
|
})
|
|
3770
|
-
.parse(
|
|
5584
|
+
.parse(decoded);
|
|
3771
5585
|
if (
|
|
3772
|
-
parsed.cursor.orderBy !== "updatedAt" ||
|
|
3773
|
-
parsed.cursor.updatedAfter !== null ||
|
|
3774
5586
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.sortRevision) ||
|
|
3775
5587
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.snapshotRevision) ||
|
|
3776
5588
|
BigInt(parsed.cursor.sortRevision) > 9_223_372_036_854_775_807n ||
|
|
@@ -3780,6 +5592,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
3780
5592
|
) {
|
|
3781
5593
|
throw new Error("invalid topology cursor fields");
|
|
3782
5594
|
}
|
|
5595
|
+
if (
|
|
5596
|
+
(parsed.cursor.orderBy === "relevance" &&
|
|
5597
|
+
(parsed.cursor.sortRank === null || parsed.cursor.filterHash === null)) ||
|
|
5598
|
+
(parsed.cursor.orderBy === "updatedAt" &&
|
|
5599
|
+
(parsed.cursor.sortRank !== null || parsed.cursor.filterHash !== null))
|
|
5600
|
+
) {
|
|
5601
|
+
throw new Error("invalid topology cursor relevance fields");
|
|
5602
|
+
}
|
|
3783
5603
|
return parsed;
|
|
3784
5604
|
} catch {
|
|
3785
5605
|
throw new HTTPException(400, {
|
|
@@ -3790,8 +5610,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
3790
5610
|
|
|
3791
5611
|
export function agentTopologyQuery(query: Record<string, string>): {
|
|
3792
5612
|
limit: number;
|
|
3793
|
-
parentSessionId: string | null;
|
|
3794
|
-
|
|
5613
|
+
parentSessionId: string | null | undefined;
|
|
5614
|
+
rootSessionId: string | undefined;
|
|
5615
|
+
query: string | undefined;
|
|
5616
|
+
statuses: SessionStatus[] | undefined;
|
|
5617
|
+
activeOnly: boolean;
|
|
5618
|
+
recentHours: number | undefined;
|
|
5619
|
+
subject: WorkClaimSubjectFilter | undefined;
|
|
5620
|
+
claimLimit: number | undefined;
|
|
3795
5621
|
cursor: SessionDiscoveryCursor | undefined;
|
|
3796
5622
|
} {
|
|
3797
5623
|
const rawLimit = query.limit;
|
|
@@ -3811,22 +5637,141 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
3811
5637
|
message: 'parentSessionId must be a session id or the literal "null"',
|
|
3812
5638
|
});
|
|
3813
5639
|
}
|
|
3814
|
-
const
|
|
3815
|
-
|
|
3816
|
-
|
|
5640
|
+
const rootSessionId = query.rootSessionId?.trim();
|
|
5641
|
+
if (rootSessionId && !z.string().uuid().safeParse(rootSessionId).success) {
|
|
5642
|
+
throw new HTTPException(400, { message: "rootSessionId must be a session id" });
|
|
5643
|
+
}
|
|
5644
|
+
const normalizeSearchQuery = (value: string | undefined): string | undefined => {
|
|
5645
|
+
if (value === undefined) return undefined;
|
|
5646
|
+
const canonical = value.normalize("NFKC");
|
|
5647
|
+
if (/[\u0000-\u001f\u007f-\u009f]/u.test(canonical)) {
|
|
5648
|
+
throw new HTTPException(400, { message: "query must not contain control characters" });
|
|
5649
|
+
}
|
|
5650
|
+
const normalized = canonical.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
5651
|
+
if (!normalized) return undefined;
|
|
5652
|
+
if (Array.from(normalized).length > WORK_DISCOVERY_QUERY_MAX_CHARS) {
|
|
5653
|
+
throw new HTTPException(400, {
|
|
5654
|
+
message: `query must be at most ${WORK_DISCOVERY_QUERY_MAX_CHARS} characters`,
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
return normalized;
|
|
5658
|
+
};
|
|
5659
|
+
const requestedQuery = normalizeSearchQuery(query.query);
|
|
5660
|
+
const legacySearch = normalizeSearchQuery(query.search);
|
|
5661
|
+
const searchQuery = requestedQuery ?? legacySearch;
|
|
5662
|
+
if (requestedQuery && legacySearch && requestedQuery !== legacySearch) {
|
|
5663
|
+
throw new HTTPException(400, { message: "query and legacy search must match" });
|
|
5664
|
+
}
|
|
5665
|
+
const statuses = query.statuses
|
|
5666
|
+
? [
|
|
5667
|
+
...new Set(
|
|
5668
|
+
query.statuses
|
|
5669
|
+
.split(",")
|
|
5670
|
+
.map((status) => status.trim())
|
|
5671
|
+
.filter(Boolean),
|
|
5672
|
+
),
|
|
5673
|
+
].sort()
|
|
5674
|
+
: [];
|
|
5675
|
+
const parsedStatuses = z
|
|
5676
|
+
.array(
|
|
5677
|
+
z.enum([
|
|
5678
|
+
"queued",
|
|
5679
|
+
"running",
|
|
5680
|
+
"idle",
|
|
5681
|
+
"requires_action",
|
|
5682
|
+
"recovering",
|
|
5683
|
+
"waiting_capacity",
|
|
5684
|
+
"failed",
|
|
5685
|
+
"cancelled",
|
|
5686
|
+
]),
|
|
5687
|
+
)
|
|
5688
|
+
.max(8)
|
|
5689
|
+
.safeParse(statuses);
|
|
5690
|
+
if (!parsedStatuses.success) {
|
|
5691
|
+
throw new HTTPException(400, { message: "statuses contains an unsupported lifecycle state" });
|
|
5692
|
+
}
|
|
5693
|
+
const activeOnly = query.activeOnly === "true";
|
|
5694
|
+
if (
|
|
5695
|
+
query.activeOnly !== undefined &&
|
|
5696
|
+
query.activeOnly !== "true" &&
|
|
5697
|
+
query.activeOnly !== "false"
|
|
5698
|
+
) {
|
|
5699
|
+
throw new HTTPException(400, { message: "activeOnly must be true or false" });
|
|
5700
|
+
}
|
|
5701
|
+
const recentHours = query.recentHours === undefined ? undefined : Number(query.recentHours);
|
|
5702
|
+
if (
|
|
5703
|
+
recentHours !== undefined &&
|
|
5704
|
+
(!Number.isSafeInteger(recentHours) ||
|
|
5705
|
+
recentHours < 1 ||
|
|
5706
|
+
recentHours > WORK_DISCOVERY_RECENT_HOURS_MAX)
|
|
5707
|
+
) {
|
|
3817
5708
|
throw new HTTPException(400, {
|
|
3818
|
-
message:
|
|
5709
|
+
message: `recentHours must be an integer between 1 and ${WORK_DISCOVERY_RECENT_HOURS_MAX}`,
|
|
5710
|
+
});
|
|
5711
|
+
}
|
|
5712
|
+
const subjectFields = [query.subjectNamespace, query.subjectType, query.subjectKey];
|
|
5713
|
+
if (subjectFields.some((value) => value !== undefined) && subjectFields.some((value) => !value)) {
|
|
5714
|
+
throw new HTTPException(400, {
|
|
5715
|
+
message: "subjectNamespace, subjectType, and subjectKey must be supplied together",
|
|
3819
5716
|
});
|
|
3820
5717
|
}
|
|
3821
|
-
|
|
5718
|
+
const parsedSubject = subjectFields.every((value) => value !== undefined)
|
|
5719
|
+
? WorkClaimSubjectFilterSchema.safeParse({
|
|
5720
|
+
namespace: normalizeWorkClaimNamespace(query.subjectNamespace!),
|
|
5721
|
+
type: query.subjectType,
|
|
5722
|
+
canonicalKey: normalizeWorkClaimCanonicalKey(query.subjectKey!),
|
|
5723
|
+
})
|
|
5724
|
+
: null;
|
|
5725
|
+
if (parsedSubject && !parsedSubject.success) {
|
|
5726
|
+
throw new HTTPException(400, { message: "exact subject filter is invalid" });
|
|
5727
|
+
}
|
|
5728
|
+
const subject = parsedSubject?.success ? parsedSubject.data : undefined;
|
|
5729
|
+
if (searchQuery && subject) {
|
|
5730
|
+
throw new HTTPException(400, { message: "query cannot be combined with an exact subject" });
|
|
5731
|
+
}
|
|
5732
|
+
const relevanceRequested = Boolean(searchQuery || subject);
|
|
5733
|
+
// An explicit root scopes the whole workstream; only an explicit parent
|
|
5734
|
+
// narrows it to one level. Ordinary browse still defaults to root sessions.
|
|
5735
|
+
const parentSessionId =
|
|
5736
|
+
rawParent === undefined
|
|
5737
|
+
? relevanceRequested || Boolean(rootSessionId)
|
|
5738
|
+
? undefined
|
|
5739
|
+
: null
|
|
5740
|
+
: rawParent === "null"
|
|
5741
|
+
? null
|
|
5742
|
+
: rawParent;
|
|
5743
|
+
const claimLimit = query.claimLimit === undefined ? undefined : Number(query.claimLimit);
|
|
5744
|
+
if (
|
|
5745
|
+
claimLimit !== undefined &&
|
|
5746
|
+
(!Number.isSafeInteger(claimLimit) || claimLimit < 1 || claimLimit > WORK_CLAIM_DISCOVERY_LIMIT)
|
|
5747
|
+
) {
|
|
3822
5748
|
throw new HTTPException(400, {
|
|
3823
|
-
message:
|
|
5749
|
+
message: `claimLimit must be an integer between 1 and ${WORK_CLAIM_DISCOVERY_LIMIT}`,
|
|
3824
5750
|
});
|
|
3825
5751
|
}
|
|
3826
5752
|
const envelope = query.cursor ? decodeAgentTopologyCursor(query.cursor) : undefined;
|
|
5753
|
+
const expectedEnvelope = {
|
|
5754
|
+
parentSessionId: parentSessionId === undefined ? "all" : parentSessionId,
|
|
5755
|
+
rootSessionId: rootSessionId ?? null,
|
|
5756
|
+
query: searchQuery || null,
|
|
5757
|
+
statuses: parsedStatuses.data,
|
|
5758
|
+
activeOnly,
|
|
5759
|
+
recentHours: recentHours ?? null,
|
|
5760
|
+
subject: subject ?? null,
|
|
5761
|
+
claimLimit: claimLimit ?? null,
|
|
5762
|
+
};
|
|
3827
5763
|
if (
|
|
3828
5764
|
envelope &&
|
|
3829
|
-
|
|
5765
|
+
JSON.stringify({
|
|
5766
|
+
parentSessionId: envelope.parentSessionId,
|
|
5767
|
+
rootSessionId: envelope.rootSessionId,
|
|
5768
|
+
query: envelope.query,
|
|
5769
|
+
statuses: envelope.statuses,
|
|
5770
|
+
activeOnly: envelope.activeOnly,
|
|
5771
|
+
recentHours: envelope.recentHours,
|
|
5772
|
+
subject: envelope.subject,
|
|
5773
|
+
claimLimit: envelope.claimLimit,
|
|
5774
|
+
}) !== JSON.stringify(expectedEnvelope)
|
|
3830
5775
|
) {
|
|
3831
5776
|
throw new HTTPException(400, {
|
|
3832
5777
|
message: "agent topology cursor does not match its filters",
|
|
@@ -3835,7 +5780,13 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
3835
5780
|
return {
|
|
3836
5781
|
limit,
|
|
3837
5782
|
parentSessionId,
|
|
3838
|
-
|
|
5783
|
+
rootSessionId: rootSessionId || undefined,
|
|
5784
|
+
query: searchQuery || undefined,
|
|
5785
|
+
statuses: parsedStatuses.data.length > 0 ? parsedStatuses.data : undefined,
|
|
5786
|
+
activeOnly,
|
|
5787
|
+
recentHours,
|
|
5788
|
+
subject,
|
|
5789
|
+
claimLimit,
|
|
3839
5790
|
cursor: envelope?.cursor,
|
|
3840
5791
|
};
|
|
3841
5792
|
}
|
|
@@ -3865,11 +5816,57 @@ function optionalEventSequence(raw: string | undefined): number | undefined {
|
|
|
3865
5816
|
|
|
3866
5817
|
/** Stable, value-free JSON errors for only the create-session boundary. */
|
|
3867
5818
|
export function sessionCreateErrorResponse(c: Context, error: unknown): Response {
|
|
5819
|
+
if (error instanceof UnsupportedLatencyModeError) {
|
|
5820
|
+
return c.json(
|
|
5821
|
+
{
|
|
5822
|
+
code: "SESSION_CREATE_REJECTED",
|
|
5823
|
+
message: error.message,
|
|
5824
|
+
details: {
|
|
5825
|
+
code: error.code,
|
|
5826
|
+
modelId: error.modelId,
|
|
5827
|
+
latencyMode: error.latencyMode,
|
|
5828
|
+
allowedLatencyModes: [...error.allowedLatencyModes],
|
|
5829
|
+
},
|
|
5830
|
+
},
|
|
5831
|
+
422,
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
if (error instanceof SessionCreateConnectionSelectionUnavailableError) {
|
|
5835
|
+
return c.json({ code: error.code, message: error.message, retryable: false }, 409);
|
|
5836
|
+
}
|
|
5837
|
+
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
5838
|
+
return c.json(
|
|
5839
|
+
{
|
|
5840
|
+
code: "SESSION_CREATE_FORBIDDEN",
|
|
5841
|
+
message: "Only managed-account users can create an Only-me session.",
|
|
5842
|
+
},
|
|
5843
|
+
403,
|
|
5844
|
+
);
|
|
5845
|
+
}
|
|
5846
|
+
if (error instanceof SessionTenancyNotActivatedError) {
|
|
5847
|
+
return c.json(
|
|
5848
|
+
{
|
|
5849
|
+
code: "SESSION_TENANCY_NOT_ACTIVATED",
|
|
5850
|
+
message: "Private sessions are not enabled for this organization.",
|
|
5851
|
+
},
|
|
5852
|
+
409,
|
|
5853
|
+
);
|
|
5854
|
+
}
|
|
3868
5855
|
if (error instanceof ChannelNotFoundError) {
|
|
3869
5856
|
// Covers the create-vs-channel-delete race the pre-validation cannot: the
|
|
3870
5857
|
// insert's FK rejection surfaces as the same 422 an unknown id gets.
|
|
3871
5858
|
return c.json({ code: "SESSION_CREATE_REJECTED", message: error.message }, 422);
|
|
3872
5859
|
}
|
|
5860
|
+
if (error instanceof SessionVariableSetSelectionUnavailableError) {
|
|
5861
|
+
return c.json(
|
|
5862
|
+
{
|
|
5863
|
+
code: "SESSION_CREATE_REJECTED",
|
|
5864
|
+
message: error.message,
|
|
5865
|
+
details: { variableSetIds: error.variableSetIds },
|
|
5866
|
+
},
|
|
5867
|
+
422,
|
|
5868
|
+
);
|
|
5869
|
+
}
|
|
3873
5870
|
if (error instanceof SessionSpawnDeniedError) {
|
|
3874
5871
|
return c.json(
|
|
3875
5872
|
sessionSpawnDenialEnvelope(error),
|
|
@@ -3885,6 +5882,38 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
3885
5882
|
422,
|
|
3886
5883
|
);
|
|
3887
5884
|
}
|
|
5885
|
+
if (
|
|
5886
|
+
error instanceof HTTPException &&
|
|
5887
|
+
error.status === 409 &&
|
|
5888
|
+
error.cause instanceof NewSessionDraftConflictError
|
|
5889
|
+
) {
|
|
5890
|
+
return c.json(
|
|
5891
|
+
{
|
|
5892
|
+
code: "NEW_SESSION_DRAFT_CONFLICT",
|
|
5893
|
+
message: error.cause.message,
|
|
5894
|
+
currentRevision: error.cause.currentRevision,
|
|
5895
|
+
},
|
|
5896
|
+
409,
|
|
5897
|
+
);
|
|
5898
|
+
}
|
|
5899
|
+
if (
|
|
5900
|
+
error instanceof HTTPException &&
|
|
5901
|
+
error.status === 422 &&
|
|
5902
|
+
error.cause instanceof AgentConfigError
|
|
5903
|
+
) {
|
|
5904
|
+
// Typed agent-configuration failure: the specific code is details.code.
|
|
5905
|
+
return c.json(
|
|
5906
|
+
{
|
|
5907
|
+
code: "SESSION_CREATE_REJECTED",
|
|
5908
|
+
message: error.message,
|
|
5909
|
+
details: {
|
|
5910
|
+
code: error.cause.code,
|
|
5911
|
+
...(error.cause.capability ? { capability: error.cause.capability } : {}),
|
|
5912
|
+
},
|
|
5913
|
+
},
|
|
5914
|
+
422,
|
|
5915
|
+
);
|
|
5916
|
+
}
|
|
3888
5917
|
if (error instanceof HTTPException && error.status === 422) {
|
|
3889
5918
|
return c.json(
|
|
3890
5919
|
{
|
|
@@ -3894,6 +5923,15 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
3894
5923
|
422,
|
|
3895
5924
|
);
|
|
3896
5925
|
}
|
|
5926
|
+
if (error instanceof HTTPException && error.status === 409) {
|
|
5927
|
+
return c.json(
|
|
5928
|
+
{
|
|
5929
|
+
code: "SESSION_CREATE_CONFLICT",
|
|
5930
|
+
message: error.message,
|
|
5931
|
+
},
|
|
5932
|
+
409,
|
|
5933
|
+
);
|
|
5934
|
+
}
|
|
3897
5935
|
throw error;
|
|
3898
5936
|
}
|
|
3899
5937
|
|
|
@@ -3943,18 +5981,20 @@ function commandConflictResponse(c: Context, error: unknown): Response {
|
|
|
3943
5981
|
type EffectivePolicyContext = {
|
|
3944
5982
|
workspaceServerIds: string[];
|
|
3945
5983
|
workspaceDefaultServerIds: string[];
|
|
5984
|
+
effectiveToolsContext: Awaited<ReturnType<typeof workspaceSessionEffectiveToolsContext>>;
|
|
3946
5985
|
};
|
|
3947
5986
|
|
|
3948
5987
|
async function loadEffectivePolicyContext(
|
|
3949
5988
|
deps: ApiRouteDeps,
|
|
3950
5989
|
workspaceId: string,
|
|
3951
5990
|
subjectId: string,
|
|
5991
|
+
sessions: readonly Session[],
|
|
3952
5992
|
): Promise<EffectivePolicyContext> {
|
|
3953
|
-
const [
|
|
3954
|
-
|
|
3955
|
-
|
|
5993
|
+
const [policy, effectiveToolsContext] = await Promise.all([
|
|
5994
|
+
workspaceSessionToolPolicyContext(deps.db, workspaceId, deps.settings, subjectId),
|
|
5995
|
+
workspaceSessionEffectiveToolsContext(deps, workspaceId, subjectId, sessions),
|
|
3956
5996
|
]);
|
|
3957
|
-
return {
|
|
5997
|
+
return { ...policy, effectiveToolsContext };
|
|
3958
5998
|
}
|
|
3959
5999
|
|
|
3960
6000
|
async function withEffectivePolicy(
|
|
@@ -3963,11 +6003,12 @@ async function withEffectivePolicy(
|
|
|
3963
6003
|
subjectId: string,
|
|
3964
6004
|
session: Session,
|
|
3965
6005
|
): Promise<Session> {
|
|
3966
|
-
const policy = await loadEffectivePolicyContext(deps, workspaceId, subjectId);
|
|
6006
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId, subjectId, [session]);
|
|
3967
6007
|
return sessionWithEffectiveToolPolicy(
|
|
3968
6008
|
session,
|
|
3969
6009
|
policy.workspaceServerIds,
|
|
3970
6010
|
policy.workspaceDefaultServerIds,
|
|
6011
|
+
policy.effectiveToolsContext,
|
|
3971
6012
|
);
|
|
3972
6013
|
}
|
|
3973
6014
|
|
|
@@ -3978,6 +6019,7 @@ function mapLineageNodes(nodes: LineageNode[], policy: EffectivePolicyContext):
|
|
|
3978
6019
|
node.session as Session,
|
|
3979
6020
|
policy.workspaceServerIds,
|
|
3980
6021
|
policy.workspaceDefaultServerIds,
|
|
6022
|
+
policy.effectiveToolsContext,
|
|
3981
6023
|
),
|
|
3982
6024
|
children: mapLineageNodes(node.children, policy),
|
|
3983
6025
|
}));
|