@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
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
resolveFirstPartyDelegationSecret,
|
|
4
|
+
resolveStreamTokenSecret,
|
|
5
|
+
sandboxWarmRateMicrosPerSecond,
|
|
6
|
+
} from "@opengeni/config";
|
|
3
7
|
import {
|
|
4
8
|
BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
|
|
5
9
|
BROWSER_CONTROL_PORT,
|
|
6
10
|
COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
7
|
-
COMPUTER_RFB_WEBSOCKET_PROTOCOL,
|
|
8
11
|
ComputerActionCommand,
|
|
9
12
|
ComputerActionRequest,
|
|
10
13
|
ComputerSessionAttachment,
|
|
11
14
|
ComputerSessionAttachmentRequest,
|
|
12
15
|
ComputerSessionHeartbeatResponse,
|
|
16
|
+
ComputerSessionInputPosture,
|
|
13
17
|
ComputerSessionLifecycleRequest,
|
|
14
18
|
ComputerSessionListResponse,
|
|
15
19
|
ComputerSessionMutationResponse,
|
|
@@ -25,18 +29,21 @@ import {
|
|
|
25
29
|
type SessionAuthorizationOperation,
|
|
26
30
|
} from "@opengeni/contracts";
|
|
27
31
|
import {
|
|
32
|
+
getSessionAuthorityEpoch,
|
|
28
33
|
acquireLease,
|
|
29
34
|
activateComputerSession,
|
|
30
35
|
completeComputerSessionEnd,
|
|
31
36
|
ComputerSessionNotFoundError,
|
|
32
37
|
ComputerSessionOperationConflictError,
|
|
33
38
|
ComputerSessionStateError,
|
|
39
|
+
SandboxPaidComputeAdmissionError,
|
|
34
40
|
dispatchComputerSessionOperation,
|
|
35
41
|
failComputerSessionOperation,
|
|
36
42
|
findComputerSessionControlRecordByOperation,
|
|
37
43
|
getAttachedBrowserDevice,
|
|
38
44
|
getComputerSessionControlRecord,
|
|
39
|
-
|
|
45
|
+
getLiveEnrollmentConnection,
|
|
46
|
+
getSandbox,
|
|
40
47
|
getSession,
|
|
41
48
|
listComputerSessions,
|
|
42
49
|
prepareComputerSessionCreate,
|
|
@@ -44,14 +51,17 @@ import {
|
|
|
44
51
|
releaseLeaseHolder,
|
|
45
52
|
readLease,
|
|
46
53
|
recordLeaseControllerDataPlaneUrl,
|
|
54
|
+
terminalizeStaleConnectedInteractionPlacement,
|
|
47
55
|
touchComputerSessionController,
|
|
48
56
|
type ComputerSessionControlRecord,
|
|
49
57
|
type LeaseSnapshot,
|
|
50
58
|
} from "@opengeni/db";
|
|
51
59
|
import {
|
|
60
|
+
hasPermission,
|
|
52
61
|
requireAccessGrant,
|
|
53
62
|
requireSessionAuthorization,
|
|
54
63
|
relayConfigFromSettings,
|
|
64
|
+
resolveSessionSandboxRuntime,
|
|
55
65
|
SessionAuthorizationDeniedError,
|
|
56
66
|
SessionAuthorizationUnavailableError,
|
|
57
67
|
type ApiRouteDeps,
|
|
@@ -64,6 +74,7 @@ import {
|
|
|
64
74
|
BrowserControlServerUnsupportedError,
|
|
65
75
|
BrowserControlTransportError,
|
|
66
76
|
BrowserControlUnsupportedError,
|
|
77
|
+
ComputerFrameEvidenceMismatchError,
|
|
67
78
|
buildSelfhostedBackendSession,
|
|
68
79
|
buildStreamUrl,
|
|
69
80
|
exposedPortEndpointFromUrl,
|
|
@@ -71,7 +82,11 @@ import {
|
|
|
71
82
|
NatsControlRpc,
|
|
72
83
|
NatsOpStreamTransport,
|
|
73
84
|
provisionBrowserControlClient,
|
|
85
|
+
validateComputerControlFrameEvidence,
|
|
86
|
+
renewSandboxProviderExpiration,
|
|
74
87
|
type BrowserControlPlacementSession,
|
|
88
|
+
type ComputerControlFrame,
|
|
89
|
+
type ExpectedComputerFrameEvidence,
|
|
75
90
|
} from "@opengeni/runtime/sandbox";
|
|
76
91
|
import type { Context, Hono } from "hono";
|
|
77
92
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -81,19 +96,109 @@ import {
|
|
|
81
96
|
deriveComputerSessionControllerTokens,
|
|
82
97
|
deriveComputerViewGrantToken,
|
|
83
98
|
} from "../browser-controller-authority";
|
|
84
|
-
import {
|
|
99
|
+
import { connectedMachineComputerAccessError } from "../connected-machine-computer-access";
|
|
100
|
+
import {
|
|
101
|
+
controllerCacheAllowsHostFetch,
|
|
102
|
+
controllerCachedUrlIsUsable,
|
|
103
|
+
shouldPersistControllerDataPlaneUrl,
|
|
104
|
+
withCachedController,
|
|
105
|
+
} from "../controller-data-plane";
|
|
106
|
+
import { filterInteractionSessionsForGrant } from "../interaction-agent-access";
|
|
85
107
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
108
|
+
import { validateInteractionRequestOrigin } from "../http/cors";
|
|
109
|
+
import { ApiHttpError } from "../http/api-error";
|
|
110
|
+
import { interactionControlApiError } from "../http/interaction-control-error";
|
|
111
|
+
import {
|
|
112
|
+
createInteractionFrameProxyAttachment,
|
|
113
|
+
placementUsesInteractionFrameProxy,
|
|
114
|
+
} from "../interaction-frame-proxy";
|
|
115
|
+
import {
|
|
116
|
+
observeComputerActionResult,
|
|
117
|
+
observeComputerFrameEvidenceMismatch,
|
|
118
|
+
observeLifecycleResult,
|
|
119
|
+
} from "../interaction-metrics";
|
|
88
120
|
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
121
|
+
import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
|
|
89
122
|
|
|
90
123
|
type ComputerPlacement = {
|
|
91
124
|
placement: InteractionPlacement;
|
|
92
125
|
placementInstanceId: string;
|
|
93
126
|
session: BrowserControlPlacementSession;
|
|
94
127
|
lease: LeaseSnapshot | null;
|
|
128
|
+
/** Current placement consent, separate from passive viewing authority. */
|
|
129
|
+
screenControlAllowed?: boolean;
|
|
95
130
|
};
|
|
96
131
|
|
|
132
|
+
const MODEL_COMPUTER_FRAME_MAX_BYTES = 256 * 1024;
|
|
133
|
+
|
|
134
|
+
type ComputerFrameCaptureClient = {
|
|
135
|
+
capture(
|
|
136
|
+
targetId: string,
|
|
137
|
+
options: {
|
|
138
|
+
format?: "jpeg" | "png";
|
|
139
|
+
quality?: number;
|
|
140
|
+
maxWidth?: number;
|
|
141
|
+
maxHeight?: number;
|
|
142
|
+
},
|
|
143
|
+
): Promise<ComputerControlFrame>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export function validateComputerFrameForApi(
|
|
147
|
+
frame: Pick<ComputerControlFrame, "data" | "mediaType" | "metadataHeader">,
|
|
148
|
+
expected: ExpectedComputerFrameEvidence,
|
|
149
|
+
): ComputerControlFrame {
|
|
150
|
+
return validateComputerControlFrameEvidence(frame, expected);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function captureModelComputerFrame(
|
|
154
|
+
sessionClient: ComputerFrameCaptureClient,
|
|
155
|
+
expected: ExpectedComputerFrameEvidence,
|
|
156
|
+
): Promise<ComputerControlFrame> {
|
|
157
|
+
let captured = validateComputerFrameForApi(
|
|
158
|
+
await sessionClient.capture(expected.targetId, {
|
|
159
|
+
format: "jpeg",
|
|
160
|
+
quality: 55,
|
|
161
|
+
maxWidth: 1_024,
|
|
162
|
+
maxHeight: 768,
|
|
163
|
+
}),
|
|
164
|
+
expected,
|
|
165
|
+
);
|
|
166
|
+
if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
|
|
167
|
+
captured = validateComputerFrameForApi(
|
|
168
|
+
await sessionClient.capture(expected.targetId, {
|
|
169
|
+
format: "jpeg",
|
|
170
|
+
quality: 30,
|
|
171
|
+
maxWidth: 640,
|
|
172
|
+
maxHeight: 480,
|
|
173
|
+
}),
|
|
174
|
+
expected,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
|
|
178
|
+
throw new BrowserControlProtocolError(
|
|
179
|
+
"computer screenshot could not honor the model image byte bound",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return captured;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** One validated screen frame, served as sandboxed, non-embeddable user content. */
|
|
186
|
+
export function computerScreenshotResponse(
|
|
187
|
+
frame: Pick<ComputerControlFrame, "data" | "mediaType" | "metadataHeader">,
|
|
188
|
+
): Response {
|
|
189
|
+
// Copy exactly the image bytes: a Node Buffer view may share a larger
|
|
190
|
+
// backing allocation, which `.slice().buffer` would expose whole.
|
|
191
|
+
return new Response(Uint8Array.from(frame.data).buffer, {
|
|
192
|
+
status: 200,
|
|
193
|
+
headers: {
|
|
194
|
+
"cache-control": "no-store",
|
|
195
|
+
"content-type": frame.mediaType,
|
|
196
|
+
"x-opengeni-computer-frame": frame.metadataHeader,
|
|
197
|
+
...USER_CONTENT_SECURITY_HEADERS,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
97
202
|
/** Public ComputerSession resource surface. Physical app/window authority stays
|
|
98
203
|
* in the same placement controller used by BrowserSession; this route owns only
|
|
99
204
|
* durable authorization, placement fencing, lifecycle receipts, and routing. */
|
|
@@ -108,13 +213,15 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
108
213
|
app.get("/v1/workspaces/:workspaceId/computer-sessions", async (context) => {
|
|
109
214
|
const workspaceId = context.req.param("workspaceId") ?? "";
|
|
110
215
|
const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
|
|
216
|
+
const listed = await listComputerSessions(deps.db, {
|
|
217
|
+
accountId: grant.accountId,
|
|
218
|
+
workspaceId,
|
|
219
|
+
});
|
|
111
220
|
return context.json(
|
|
112
|
-
ComputerSessionListResponse.parse(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}),
|
|
117
|
-
),
|
|
221
|
+
ComputerSessionListResponse.parse({
|
|
222
|
+
...listed,
|
|
223
|
+
sessions: await filterInteractionSessionsForGrant(deps, grant, listed.sessions),
|
|
224
|
+
}),
|
|
118
225
|
);
|
|
119
226
|
});
|
|
120
227
|
|
|
@@ -141,8 +248,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
141
248
|
const request = await parseJsonBody(context, CreateComputerSessionRequest);
|
|
142
249
|
const startedAtMs = performance.now();
|
|
143
250
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
144
|
-
const origin = requestOrigin(context, deps.settings
|
|
251
|
+
const origin = requestOrigin(context, deps.settings);
|
|
145
252
|
const authority = controllerAuthorityRoot(deps);
|
|
253
|
+
let prepared: Awaited<ReturnType<typeof prepareComputerSessionCreate>> | null = null;
|
|
146
254
|
|
|
147
255
|
try {
|
|
148
256
|
const existing = await findComputerSessionControlRecordByOperation(deps.db, {
|
|
@@ -156,7 +264,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
156
264
|
);
|
|
157
265
|
}
|
|
158
266
|
if (existing) assertCreateReplay(request, existing.session);
|
|
159
|
-
|
|
267
|
+
prepared = existing
|
|
160
268
|
? await prepareComputerSessionCreate(
|
|
161
269
|
deps.db,
|
|
162
270
|
computerCreateInput(grant, workspaceId, request, existing.session.placement),
|
|
@@ -264,26 +372,27 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
264
372
|
() => placement,
|
|
265
373
|
);
|
|
266
374
|
} catch (error) {
|
|
267
|
-
|
|
375
|
+
const retryableRequestFailure =
|
|
376
|
+
error instanceof BrowserControlRequestError &&
|
|
377
|
+
error.retryable &&
|
|
378
|
+
error.error.code !== "machine_locked";
|
|
379
|
+
const outcomeUnknown =
|
|
380
|
+
error instanceof BrowserControlProtocolError ||
|
|
268
381
|
error instanceof BrowserControlTransportError ||
|
|
269
|
-
(error
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
throw error;
|
|
275
|
-
}
|
|
382
|
+
isAbort(error);
|
|
383
|
+
const rethrowAfterFailure =
|
|
384
|
+
error instanceof BrowserControlTransportError ||
|
|
385
|
+
retryableRequestFailure ||
|
|
386
|
+
isAbort(error);
|
|
276
387
|
const failed = await failComputerSessionOperation(deps.db, {
|
|
277
388
|
accountId: grant.accountId,
|
|
278
389
|
workspaceId,
|
|
279
390
|
operationId: request.operationId,
|
|
280
391
|
computerSessionId: preparedSession.id,
|
|
281
|
-
...(
|
|
282
|
-
? { state: "outcome_unknown" as const }
|
|
283
|
-
: {}),
|
|
392
|
+
...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
|
|
284
393
|
error: interactionFailure(error),
|
|
285
394
|
});
|
|
286
|
-
if (interactionHeld && !
|
|
395
|
+
if (interactionHeld && !outcomeUnknown) {
|
|
287
396
|
await releaseInteractionHolder(
|
|
288
397
|
grant,
|
|
289
398
|
workspaceId,
|
|
@@ -291,6 +400,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
291
400
|
placement.placement,
|
|
292
401
|
).catch(() => undefined);
|
|
293
402
|
}
|
|
403
|
+
if (rethrowAfterFailure) throw error;
|
|
294
404
|
return failed;
|
|
295
405
|
}
|
|
296
406
|
// These facts come from the physical adapter after its native helper
|
|
@@ -320,10 +430,49 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
320
430
|
parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
|
|
321
431
|
);
|
|
322
432
|
} catch (error) {
|
|
433
|
+
if (prepared) {
|
|
434
|
+
// A concurrent dispatch owns its exact binding, even if this request's
|
|
435
|
+
// placement failed. Settle only the still-undispatched create receipt.
|
|
436
|
+
await failComputerSessionOperation(deps.db, {
|
|
437
|
+
accountId: grant.accountId,
|
|
438
|
+
workspaceId,
|
|
439
|
+
operationId: request.operationId,
|
|
440
|
+
computerSessionId: prepared.session.id,
|
|
441
|
+
onlyIfPreparedCreate: true,
|
|
442
|
+
error: interactionFailure(error),
|
|
443
|
+
}).catch(() => undefined);
|
|
444
|
+
}
|
|
323
445
|
throw computerRouteError(error);
|
|
324
446
|
}
|
|
325
447
|
});
|
|
326
448
|
|
|
449
|
+
app.get(
|
|
450
|
+
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/input-posture",
|
|
451
|
+
async (context) => {
|
|
452
|
+
const { workspaceId, grant, computerSessionId } = await routePreamble(
|
|
453
|
+
context,
|
|
454
|
+
"sessions:read",
|
|
455
|
+
);
|
|
456
|
+
const result = await withActiveComputerController(
|
|
457
|
+
context,
|
|
458
|
+
grant,
|
|
459
|
+
workspaceId,
|
|
460
|
+
computerSessionId,
|
|
461
|
+
"session.read",
|
|
462
|
+
"computer.read",
|
|
463
|
+
async ({ record, binding, placement }) =>
|
|
464
|
+
ComputerSessionInputPosture.parse({
|
|
465
|
+
computerSessionId,
|
|
466
|
+
controllerGeneration: binding.controllerGeneration,
|
|
467
|
+
inputAllowed: await authorizeHumanFrameInput(grant, record, placement),
|
|
468
|
+
}),
|
|
469
|
+
false,
|
|
470
|
+
);
|
|
471
|
+
context.header("cache-control", "no-store");
|
|
472
|
+
return context.json(result);
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
|
|
327
476
|
app.get(
|
|
328
477
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets",
|
|
329
478
|
async (context) => {
|
|
@@ -370,6 +519,40 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
370
519
|
},
|
|
371
520
|
);
|
|
372
521
|
|
|
522
|
+
app.get(
|
|
523
|
+
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
|
|
524
|
+
async (context) => {
|
|
525
|
+
const { workspaceId, grant, computerSessionId } = await routePreamble(
|
|
526
|
+
context,
|
|
527
|
+
"sessions:read",
|
|
528
|
+
);
|
|
529
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
530
|
+
const frame = await withActiveComputerController(
|
|
531
|
+
context,
|
|
532
|
+
grant,
|
|
533
|
+
workspaceId,
|
|
534
|
+
computerSessionId,
|
|
535
|
+
"session.read",
|
|
536
|
+
"computer.read",
|
|
537
|
+
async ({ sessionClient, binding }) => {
|
|
538
|
+
try {
|
|
539
|
+
return await captureModelComputerFrame(sessionClient, {
|
|
540
|
+
computerSessionId,
|
|
541
|
+
controllerGeneration: binding.controllerGeneration,
|
|
542
|
+
targetId,
|
|
543
|
+
});
|
|
544
|
+
} catch (error) {
|
|
545
|
+
if (error instanceof ComputerFrameEvidenceMismatchError) {
|
|
546
|
+
observeComputerFrameEvidenceMismatch(deps.observability, error.reason);
|
|
547
|
+
}
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
return computerScreenshotResponse(frame);
|
|
553
|
+
},
|
|
554
|
+
);
|
|
555
|
+
|
|
373
556
|
app.get(
|
|
374
557
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/clipboard",
|
|
375
558
|
async (context) => {
|
|
@@ -406,8 +589,15 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
406
589
|
computerSessionId,
|
|
407
590
|
"session.control",
|
|
408
591
|
"computer.action",
|
|
409
|
-
async ({ sessionClient, binding }) =>
|
|
410
|
-
|
|
592
|
+
async ({ sessionClient, binding, record }) => {
|
|
593
|
+
if (
|
|
594
|
+
record.session.placement.kind === "sandbox_group" &&
|
|
595
|
+
deps.settings.sandboxDesktopInteractive === false &&
|
|
596
|
+
grant.principalKind !== "agent_attempt"
|
|
597
|
+
) {
|
|
598
|
+
throw new HTTPException(403, { message: "Human desktop input is disabled" });
|
|
599
|
+
}
|
|
600
|
+
return await sessionClient.action(
|
|
411
601
|
ComputerActionCommand.parse({
|
|
412
602
|
protocolVersion: INTERACTION_PROTOCOL_VERSION,
|
|
413
603
|
operationId: request.operationId,
|
|
@@ -420,7 +610,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
420
610
|
actor: interactionActorForGrant(grant),
|
|
421
611
|
action: request.action,
|
|
422
612
|
}),
|
|
423
|
-
)
|
|
613
|
+
);
|
|
614
|
+
},
|
|
424
615
|
);
|
|
425
616
|
observeComputerActionResult(deps.observability, startedAtMs, request, result);
|
|
426
617
|
return context.json(result);
|
|
@@ -452,7 +643,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
452
643
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
|
|
453
644
|
async (context) => {
|
|
454
645
|
const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
|
|
455
|
-
const origin = requestOrigin(context, deps.settings
|
|
646
|
+
const origin = requestOrigin(context, deps.settings);
|
|
456
647
|
const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
|
|
457
648
|
const result = await withActiveComputerController(
|
|
458
649
|
context,
|
|
@@ -463,25 +654,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
463
654
|
"computer.attach",
|
|
464
655
|
async ({ client, sessionClient, record, binding, placement }) => {
|
|
465
656
|
if (origin) await client.addAllowedOrigins([origin]);
|
|
466
|
-
const grantId = randomUUID();
|
|
467
|
-
const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
|
|
468
|
-
const token = deriveComputerViewGrantToken({
|
|
469
|
-
rootSecret: controllerAuthorityRoot(deps),
|
|
470
|
-
accountId: grant.accountId,
|
|
471
|
-
workspaceId,
|
|
472
|
-
placement: record.session.placement,
|
|
473
|
-
placementInstanceId: placement.placementInstanceId,
|
|
474
|
-
computerSessionId,
|
|
475
|
-
controllerGeneration: binding.controllerGeneration,
|
|
476
|
-
tokenGeneration: record.tokenGeneration,
|
|
477
|
-
grantId,
|
|
478
|
-
expiresAt,
|
|
479
|
-
});
|
|
480
657
|
const reference = {
|
|
481
658
|
computerSessionId,
|
|
482
659
|
controllerGeneration: binding.controllerGeneration,
|
|
483
660
|
};
|
|
484
|
-
await client.createComputerViewGrant(reference, { grantId, token, expiresAt });
|
|
485
661
|
const target = (await sessionClient.listTargets()).find(
|
|
486
662
|
(candidate) => candidate.id === request.targetId,
|
|
487
663
|
);
|
|
@@ -492,6 +668,40 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
492
668
|
retryable: false,
|
|
493
669
|
});
|
|
494
670
|
}
|
|
671
|
+
if (target.kind === "app") {
|
|
672
|
+
throw new BrowserControlRequestError(409, {
|
|
673
|
+
code: "unsupported",
|
|
674
|
+
message:
|
|
675
|
+
"application targets support semantic control but do not provide a visual stream; select a window or screen",
|
|
676
|
+
retryable: false,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const grantId = randomUUID();
|
|
680
|
+
const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
|
|
681
|
+
const token = deriveComputerViewGrantToken({
|
|
682
|
+
rootSecret: controllerAuthorityRoot(deps),
|
|
683
|
+
accountId: grant.accountId,
|
|
684
|
+
workspaceId,
|
|
685
|
+
placement: record.session.placement,
|
|
686
|
+
placementInstanceId: placement.placementInstanceId,
|
|
687
|
+
computerSessionId,
|
|
688
|
+
controllerGeneration: binding.controllerGeneration,
|
|
689
|
+
tokenGeneration: record.tokenGeneration,
|
|
690
|
+
grantId,
|
|
691
|
+
expiresAt,
|
|
692
|
+
});
|
|
693
|
+
// Keep the old strict-key request. Human input uses canonical
|
|
694
|
+
// actions; this grant authorizes only the frame producer. An older
|
|
695
|
+
// controller's reusable RFB bearer must stay behind the proxy.
|
|
696
|
+
const viewGrant = await client.createComputerViewGrant(reference, {
|
|
697
|
+
grantId,
|
|
698
|
+
token,
|
|
699
|
+
expiresAt,
|
|
700
|
+
});
|
|
701
|
+
const managedLinuxPlacement =
|
|
702
|
+
record.session.placement.kind === "sandbox_group" &&
|
|
703
|
+
record.session.platform === "linux";
|
|
704
|
+
const inputAllowed = await authorizeHumanFrameInput(grant, record, placement);
|
|
495
705
|
const relaySecret = placement.session.openComputerFrames
|
|
496
706
|
? resolveStreamTokenSecret(deps.settings)
|
|
497
707
|
: null;
|
|
@@ -510,15 +720,31 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
510
720
|
...(request.stream ? { stream: request.stream } : {}),
|
|
511
721
|
});
|
|
512
722
|
} catch (error) {
|
|
723
|
+
const publicFailure = interactionControlApiError(error, "computer");
|
|
513
724
|
console.error("computer frame relay open failed", {
|
|
514
725
|
computerSessionId,
|
|
515
726
|
targetId: request.targetId,
|
|
516
|
-
|
|
727
|
+
failureCode: publicFailure?.details?.controlFailureCode ?? "unclassified",
|
|
728
|
+
retryable: publicFailure?.retryable ?? false,
|
|
729
|
+
outcomeUnknown: publicFailure?.outcomeUnknown ?? false,
|
|
730
|
+
controlRequestId: publicFailure?.details?.controlRequestId ?? null,
|
|
517
731
|
});
|
|
518
732
|
throw error;
|
|
519
733
|
}
|
|
520
734
|
const stream = relayed
|
|
521
735
|
? await (async () => {
|
|
736
|
+
// 0281: stamp the authenticated viewer subject and the live
|
|
737
|
+
// session authority epoch into the relay stream token.
|
|
738
|
+
const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
|
|
739
|
+
accountId: grant.accountId,
|
|
740
|
+
workspaceId,
|
|
741
|
+
sessionId: record.sourceSessionId,
|
|
742
|
+
});
|
|
743
|
+
if (relayAuthorityEpoch === null) {
|
|
744
|
+
throw new BrowserControlUnsupportedError(
|
|
745
|
+
"stream authority is unavailable for this session",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
522
748
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
523
749
|
workspaceId,
|
|
524
750
|
sessionId: record.sourceSessionId,
|
|
@@ -526,6 +752,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
526
752
|
leaseEpoch: record.tokenGeneration,
|
|
527
753
|
port: relayed.channel.port,
|
|
528
754
|
ttlSeconds: request.expiresInSeconds,
|
|
755
|
+
subjectId: grant.subjectId,
|
|
756
|
+
authorityEpoch: relayAuthorityEpoch,
|
|
529
757
|
});
|
|
530
758
|
return {
|
|
531
759
|
kind: "relay" as const,
|
|
@@ -540,34 +768,48 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
540
768
|
},
|
|
541
769
|
};
|
|
542
770
|
})()
|
|
543
|
-
:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
771
|
+
: await (async () => {
|
|
772
|
+
const protocols = [
|
|
773
|
+
COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
774
|
+
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
775
|
+
];
|
|
776
|
+
const upstreamUrl = await client.computerFrameStreamUrl(
|
|
777
|
+
reference,
|
|
778
|
+
request.targetId,
|
|
779
|
+
request.stream,
|
|
780
|
+
);
|
|
781
|
+
const attachment =
|
|
782
|
+
(!viewGrant.scopedRfbInput && managedLinuxPlacement) ||
|
|
783
|
+
placementUsesInteractionFrameProxy(placement.lease?.backend, {
|
|
784
|
+
openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
785
|
+
...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
|
|
786
|
+
? {
|
|
787
|
+
openSandboxInteractionFrameProxy:
|
|
788
|
+
deps.settings.openSandboxInteractionFrameProxy,
|
|
789
|
+
}
|
|
790
|
+
: {}),
|
|
791
|
+
})
|
|
792
|
+
? createInteractionFrameProxyAttachment({
|
|
793
|
+
requestUrl: context.req.url,
|
|
794
|
+
publicBaseUrl: deps.settings.publicBaseUrl,
|
|
795
|
+
webBaseUrl: deps.settings.webBaseUrl,
|
|
796
|
+
forwardedProto: context.req.header("x-forwarded-proto"),
|
|
797
|
+
forwardedHost:
|
|
798
|
+
context.req.header("x-forwarded-host") ?? context.req.header("host"),
|
|
799
|
+
rootSecret: controllerAuthorityRoot(deps),
|
|
800
|
+
upstreamUrl,
|
|
801
|
+
upstreamProtocols: protocols,
|
|
802
|
+
origin,
|
|
803
|
+
expiresAt,
|
|
804
|
+
})
|
|
805
|
+
: { url: upstreamUrl, protocols };
|
|
806
|
+
return { kind: "direct_websocket" as const, ...attachment };
|
|
807
|
+
})();
|
|
567
808
|
return ComputerSessionAttachment.parse({
|
|
568
809
|
computerSessionId,
|
|
569
810
|
controllerGeneration: binding.controllerGeneration,
|
|
570
811
|
targetId: request.targetId,
|
|
812
|
+
inputAllowed,
|
|
571
813
|
stream,
|
|
572
814
|
expiresAt,
|
|
573
815
|
});
|
|
@@ -577,6 +819,40 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
577
819
|
},
|
|
578
820
|
);
|
|
579
821
|
|
|
822
|
+
async function authorizeHumanFrameInput(
|
|
823
|
+
grant: AccessGrant,
|
|
824
|
+
record: ComputerSessionControlRecord,
|
|
825
|
+
placement: ComputerPlacement,
|
|
826
|
+
): Promise<boolean> {
|
|
827
|
+
if (
|
|
828
|
+
((record.session.placement.kind === "connected_machine" ||
|
|
829
|
+
record.session.placement.kind === "attached_device") &&
|
|
830
|
+
placement.screenControlAllowed !== true) ||
|
|
831
|
+
(record.session.placement.kind === "sandbox_group" &&
|
|
832
|
+
deps.settings.sandboxDesktopInteractive === false) ||
|
|
833
|
+
grant.principalKind === "agent_attempt" ||
|
|
834
|
+
!hasPermission(grant.permissions, "sessions:control")
|
|
835
|
+
)
|
|
836
|
+
return false;
|
|
837
|
+
try {
|
|
838
|
+
await requireSessionAuthorization(deps, grant, {
|
|
839
|
+
sessionId: record.sourceSessionId,
|
|
840
|
+
operation: "session.control",
|
|
841
|
+
surface: "http",
|
|
842
|
+
});
|
|
843
|
+
return true;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
// Input availability cannot invalidate an independently authorized view.
|
|
846
|
+
// Every action reauthorizes the live source instead of trusting this hint.
|
|
847
|
+
if (
|
|
848
|
+
error instanceof SessionAuthorizationDeniedError ||
|
|
849
|
+
error instanceof SessionAuthorizationUnavailableError
|
|
850
|
+
)
|
|
851
|
+
return false;
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
580
856
|
app.post(
|
|
581
857
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/heartbeat",
|
|
582
858
|
async (context) => {
|
|
@@ -614,7 +890,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
614
890
|
const computerSessionId = requireUuidParam(context, "computerSessionId");
|
|
615
891
|
const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
|
|
616
892
|
const startedAtMs = performance.now();
|
|
617
|
-
const origin = requestOrigin(context, deps.settings
|
|
893
|
+
const origin = requestOrigin(context, deps.settings);
|
|
618
894
|
try {
|
|
619
895
|
const before = await getComputerSessionControlRecord(deps.db, {
|
|
620
896
|
accountId: grant.accountId,
|
|
@@ -765,23 +1041,42 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
765
1041
|
if (device.state !== "connected") {
|
|
766
1042
|
throw new ComputerSessionStateError("Attached browser machine is disconnected");
|
|
767
1043
|
}
|
|
768
|
-
const enrollment = await
|
|
1044
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
769
1045
|
deps.db,
|
|
770
1046
|
sourceSession.workspaceId,
|
|
771
1047
|
device.enrollmentId,
|
|
772
1048
|
);
|
|
773
|
-
if (!enrollment || enrollment.status !== "active") {
|
|
1049
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
774
1050
|
throw new ComputerSessionStateError("Attached browser machine is unavailable");
|
|
775
1051
|
}
|
|
776
|
-
|
|
1052
|
+
if (!enrollment.workspaceRoot) {
|
|
1053
|
+
throw new ComputerSessionStateError(
|
|
1054
|
+
"Attached browser machine has not reported an absolute workspace root",
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
if (operation !== "computer.end") {
|
|
1058
|
+
assertConnectedMachineComputerAccess(enrollment, operation);
|
|
1059
|
+
}
|
|
1060
|
+
const placementInstanceId = attachedEndPlacementInstanceId(
|
|
1061
|
+
operation,
|
|
1062
|
+
expectedPlacementInstanceId,
|
|
1063
|
+
device.connectionGeneration,
|
|
1064
|
+
);
|
|
777
1065
|
const built = await buildSelfhostedBackendSession({
|
|
778
1066
|
workspaceId: sourceSession.workspaceId,
|
|
779
1067
|
agentId: device.enrollmentId,
|
|
1068
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
1069
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
780
1070
|
relay: relayConfigFromSettings(deps.settings),
|
|
781
1071
|
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
782
1072
|
epoch: 0,
|
|
783
1073
|
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
784
1074
|
execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
|
|
1075
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
1076
|
+
operationResourcePolicySupported:
|
|
1077
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
1078
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
1079
|
+
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
785
1080
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
786
1081
|
enrollment.opStream === true &&
|
|
787
1082
|
deps.bus.getOpStreamConnection
|
|
@@ -797,9 +1092,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
797
1092
|
waitSignal.throwIfAborted();
|
|
798
1093
|
return await callback({
|
|
799
1094
|
placement: expectedPlacement,
|
|
800
|
-
placementInstanceId
|
|
1095
|
+
placementInstanceId,
|
|
801
1096
|
session: built.session as unknown as BrowserControlPlacementSession,
|
|
802
1097
|
lease: null,
|
|
1098
|
+
screenControlAllowed: enrollment.allowScreenControl === true,
|
|
803
1099
|
});
|
|
804
1100
|
}
|
|
805
1101
|
const runWithChannelA =
|
|
@@ -819,6 +1115,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
819
1115
|
waitSignal,
|
|
820
1116
|
operation,
|
|
821
1117
|
retryControllerTransport: operation === "computer.read" || operation === "computer.action",
|
|
1118
|
+
...(expectedPlacementInstanceId && expectedPlacement?.kind === "sandbox_group"
|
|
1119
|
+
? { retainedInstanceId: expectedPlacementInstanceId }
|
|
1120
|
+
: {}),
|
|
822
1121
|
},
|
|
823
1122
|
async (handle) => {
|
|
824
1123
|
if (expectedPlacement?.kind === "sandbox_group") {
|
|
@@ -838,13 +1137,38 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
838
1137
|
}
|
|
839
1138
|
|
|
840
1139
|
const resolved = await handle.routingSession.prime();
|
|
1140
|
+
let screenControlAllowed = false;
|
|
1141
|
+
if (operation !== "computer.end" && resolved.kind === "selfhosted" && resolved.sandboxId) {
|
|
1142
|
+
const sandbox = await getSandbox(deps.db, grant, resolved.sandboxId);
|
|
1143
|
+
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
1144
|
+
throw new ComputerSessionStateError("Connected Machine placement is unavailable");
|
|
1145
|
+
}
|
|
1146
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
1147
|
+
deps.db,
|
|
1148
|
+
grant,
|
|
1149
|
+
sandbox.enrollmentId,
|
|
1150
|
+
);
|
|
1151
|
+
if (!enrollment?.connectionInstanceId) {
|
|
1152
|
+
throw new ComputerSessionStateError("Connected Machine is unavailable");
|
|
1153
|
+
}
|
|
1154
|
+
assertConnectedMachineComputerAccess(enrollment, operation);
|
|
1155
|
+
screenControlAllowed = enrollment.allowScreenControl === true;
|
|
1156
|
+
}
|
|
841
1157
|
if (expectedPlacement?.kind === "connected_machine") {
|
|
842
1158
|
if (
|
|
843
1159
|
resolved.kind !== "selfhosted" ||
|
|
844
1160
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
845
1161
|
) {
|
|
846
|
-
|
|
847
|
-
|
|
1162
|
+
if (operation === "computer.create") {
|
|
1163
|
+
throw new HTTPException(422, {
|
|
1164
|
+
message:
|
|
1165
|
+
"The requested Connected Machine is not this session's current placement. Move the session to that machine before creating an interaction resource.",
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
return await throwComputerSourcePlacementChanged(
|
|
1169
|
+
grant,
|
|
1170
|
+
sourceSession.id,
|
|
1171
|
+
expectedPlacement.sandboxId,
|
|
848
1172
|
);
|
|
849
1173
|
}
|
|
850
1174
|
const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
|
|
@@ -854,6 +1178,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
854
1178
|
placementInstanceId,
|
|
855
1179
|
session: resolved.session as unknown as BrowserControlPlacementSession,
|
|
856
1180
|
lease: null,
|
|
1181
|
+
screenControlAllowed,
|
|
857
1182
|
});
|
|
858
1183
|
}
|
|
859
1184
|
if (resolved.sandboxId === null) {
|
|
@@ -878,10 +1203,14 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
878
1203
|
}
|
|
879
1204
|
if (resolved.kind === "selfhosted") {
|
|
880
1205
|
return await callback({
|
|
881
|
-
placement: {
|
|
1206
|
+
placement: {
|
|
1207
|
+
kind: "connected_machine",
|
|
1208
|
+
sandboxId: resolved.sandboxId,
|
|
1209
|
+
},
|
|
882
1210
|
placementInstanceId: resolved.providerInstanceId ?? resolved.sandboxId,
|
|
883
1211
|
session: resolved.session as unknown as BrowserControlPlacementSession,
|
|
884
1212
|
lease: null,
|
|
1213
|
+
screenControlAllowed,
|
|
885
1214
|
});
|
|
886
1215
|
}
|
|
887
1216
|
throw new BrowserControlUnsupportedError(
|
|
@@ -914,7 +1243,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
914
1243
|
!lease ||
|
|
915
1244
|
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
916
1245
|
lease.instanceId !== binding.placementInstanceId ||
|
|
917
|
-
!lease.controllerDataPlaneUrl
|
|
1246
|
+
!lease.controllerDataPlaneUrl ||
|
|
1247
|
+
(lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
|
|
1248
|
+
!controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
|
|
918
1249
|
) {
|
|
919
1250
|
return null;
|
|
920
1251
|
}
|
|
@@ -938,8 +1269,29 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
938
1269
|
) {
|
|
939
1270
|
return placement;
|
|
940
1271
|
}
|
|
1272
|
+
if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
|
|
1273
|
+
if (!placement.lease.controllerDataPlaneUrl) return placement;
|
|
1274
|
+
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
1275
|
+
accountId: grant.accountId,
|
|
1276
|
+
workspaceId,
|
|
1277
|
+
sandboxGroupId: placement.placement.sandboxGroupId,
|
|
1278
|
+
expectedEpoch: placement.lease.leaseEpoch,
|
|
1279
|
+
expectedInstanceId: placement.lease.instanceId,
|
|
1280
|
+
controllerDataPlaneUrl: null,
|
|
1281
|
+
});
|
|
1282
|
+
return lease ? { ...placement, lease } : placement;
|
|
1283
|
+
}
|
|
941
1284
|
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
942
1285
|
const url = buildStreamUrl(endpoint);
|
|
1286
|
+
if (
|
|
1287
|
+
!shouldPersistControllerDataPlaneUrl({
|
|
1288
|
+
backend: placement.lease.backend,
|
|
1289
|
+
signedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
1290
|
+
url,
|
|
1291
|
+
})
|
|
1292
|
+
) {
|
|
1293
|
+
return placement;
|
|
1294
|
+
}
|
|
943
1295
|
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
944
1296
|
accountId: grant.accountId,
|
|
945
1297
|
workspaceId,
|
|
@@ -949,6 +1301,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
949
1301
|
controllerDataPlaneUrl: url,
|
|
950
1302
|
});
|
|
951
1303
|
if (!lease) return placement;
|
|
1304
|
+
if (!controllerCacheAllowsHostFetch(url)) {
|
|
1305
|
+
return { ...placement, lease };
|
|
1306
|
+
}
|
|
952
1307
|
return { ...placement, session: controllerOnlySession(url), lease };
|
|
953
1308
|
}
|
|
954
1309
|
|
|
@@ -964,7 +1319,13 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
964
1319
|
const sandboxGroupId =
|
|
965
1320
|
placement.placement.kind === "sandbox_group" ? placement.placement.sandboxGroupId : null;
|
|
966
1321
|
return await withCachedController({
|
|
967
|
-
cachedUrl:
|
|
1322
|
+
cachedUrl:
|
|
1323
|
+
sandboxGroupId &&
|
|
1324
|
+
cachedUrl &&
|
|
1325
|
+
!(placement.lease?.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) &&
|
|
1326
|
+
controllerCachedUrlIsUsable(cachedUrl)
|
|
1327
|
+
? cachedUrl
|
|
1328
|
+
: null,
|
|
968
1329
|
createCachedClient: (url) =>
|
|
969
1330
|
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
970
1331
|
prepareCachedClient: async (client) => {
|
|
@@ -1016,6 +1377,17 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1016
1377
|
computerSessionId,
|
|
1017
1378
|
});
|
|
1018
1379
|
await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
|
|
1380
|
+
let sourceSession: Session | null = null;
|
|
1381
|
+
if (record.session.placement?.kind === "connected_machine") {
|
|
1382
|
+
sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
1383
|
+
if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
|
|
1384
|
+
return await throwComputerSourcePlacementChanged(
|
|
1385
|
+
grant,
|
|
1386
|
+
record.sourceSessionId,
|
|
1387
|
+
record.session.placement.sandboxId,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1019
1391
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
1020
1392
|
throw new ComputerSessionStateError("ComputerSession is not active");
|
|
1021
1393
|
}
|
|
@@ -1096,7 +1468,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1096
1468
|
}
|
|
1097
1469
|
}
|
|
1098
1470
|
|
|
1099
|
-
|
|
1471
|
+
sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
1100
1472
|
return await withComputerPlacement(
|
|
1101
1473
|
sourceSession,
|
|
1102
1474
|
grant,
|
|
@@ -1116,6 +1488,25 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1116
1488
|
}
|
|
1117
1489
|
}
|
|
1118
1490
|
|
|
1491
|
+
async function throwComputerSourcePlacementChanged(
|
|
1492
|
+
grant: AccessGrant,
|
|
1493
|
+
sourceSessionId: string,
|
|
1494
|
+
connectedSandboxId: string,
|
|
1495
|
+
): Promise<never> {
|
|
1496
|
+
const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
|
|
1497
|
+
accountId: grant.accountId,
|
|
1498
|
+
workspaceId: grant.workspaceId,
|
|
1499
|
+
sourceSessionId,
|
|
1500
|
+
connectedSandboxId,
|
|
1501
|
+
});
|
|
1502
|
+
if (reconciliation.sourcePlacementChanged) {
|
|
1503
|
+
throw sourcePlacementChangedApiError("computer_session");
|
|
1504
|
+
}
|
|
1505
|
+
throw new ComputerSessionStateError(
|
|
1506
|
+
"ComputerSession source placement changed during the request; retry",
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1119
1510
|
async function ensureInteractionHolder(
|
|
1120
1511
|
grant: AccessGrant,
|
|
1121
1512
|
sourceSession: Session,
|
|
@@ -1127,6 +1518,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1127
1518
|
if (!placement.lease?.instanceId) {
|
|
1128
1519
|
throw new ComputerSessionStateError("ComputerSession lease placement is unavailable");
|
|
1129
1520
|
}
|
|
1521
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(
|
|
1522
|
+
deps.db,
|
|
1523
|
+
deps.settings,
|
|
1524
|
+
sourceSession,
|
|
1525
|
+
);
|
|
1130
1526
|
const acquired = await acquireLease(deps.db, {
|
|
1131
1527
|
accountId: grant.accountId,
|
|
1132
1528
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -1135,9 +1531,14 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1135
1531
|
holderId: interactionHolderId(computerSessionId),
|
|
1136
1532
|
subjectId: sourceSession.id,
|
|
1137
1533
|
backend: placement.lease.backend,
|
|
1534
|
+
warmBilling: {
|
|
1535
|
+
mode: deps.settings.sandboxWarmBillingMode,
|
|
1536
|
+
rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
|
|
1537
|
+
},
|
|
1138
1538
|
os: placement.lease.os,
|
|
1139
|
-
image:
|
|
1140
|
-
|
|
1539
|
+
image: sandboxRuntime.image,
|
|
1540
|
+
imagePolicy: "new_creates_only",
|
|
1541
|
+
rigVersionId: sourceSession.rigVersionId,
|
|
1141
1542
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
1142
1543
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
1143
1544
|
waitSignal,
|
|
@@ -1158,6 +1559,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1158
1559
|
).catch(() => undefined);
|
|
1159
1560
|
throw new ComputerSessionStateError("ComputerSession placement fence changed; retry");
|
|
1160
1561
|
}
|
|
1562
|
+
await renewSandboxProviderExpiration({
|
|
1563
|
+
backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
|
|
1564
|
+
settings: deps.settings,
|
|
1565
|
+
instanceId: acquired.lease.instanceId,
|
|
1566
|
+
}).catch(() => false);
|
|
1161
1567
|
return true;
|
|
1162
1568
|
}
|
|
1163
1569
|
|
|
@@ -1273,7 +1679,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1273
1679
|
async function routePreamble(
|
|
1274
1680
|
context: Context,
|
|
1275
1681
|
permission: "sessions:read" | "sessions:control" | "stream:view",
|
|
1276
|
-
): Promise<{
|
|
1682
|
+
): Promise<{
|
|
1683
|
+
workspaceId: string;
|
|
1684
|
+
grant: AccessGrant;
|
|
1685
|
+
computerSessionId: string;
|
|
1686
|
+
}> {
|
|
1277
1687
|
const workspaceId = context.req.param("workspaceId") ?? "";
|
|
1278
1688
|
const grant = await requireAccessGrant(context, deps, workspaceId, permission);
|
|
1279
1689
|
return {
|
|
@@ -1284,6 +1694,21 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
1284
1694
|
}
|
|
1285
1695
|
}
|
|
1286
1696
|
|
|
1697
|
+
function sourcePlacementChangedApiError(interactionResource: "computer_session"): ApiHttpError {
|
|
1698
|
+
return new ApiHttpError(409, {
|
|
1699
|
+
code: "conflict",
|
|
1700
|
+
message:
|
|
1701
|
+
"This Desktop belonged to a previous task placement and was retired. Open a new Desktop.",
|
|
1702
|
+
retryable: false,
|
|
1703
|
+
outcomeUnknown: false,
|
|
1704
|
+
details: {
|
|
1705
|
+
interactionResource,
|
|
1706
|
+
interactionFailureCode: "source_placement_changed",
|
|
1707
|
+
interactionLifecycle: "lost",
|
|
1708
|
+
},
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1287
1712
|
function computerCreateInput(
|
|
1288
1713
|
grant: AccessGrant,
|
|
1289
1714
|
workspaceId: string,
|
|
@@ -1400,7 +1825,10 @@ async function authorizeSourceSession(
|
|
|
1400
1825
|
});
|
|
1401
1826
|
} catch (error) {
|
|
1402
1827
|
if (error instanceof SessionAuthorizationDeniedError) {
|
|
1403
|
-
throw new HTTPException(404, {
|
|
1828
|
+
throw new HTTPException(404, {
|
|
1829
|
+
message: "session not found",
|
|
1830
|
+
cause: error,
|
|
1831
|
+
});
|
|
1404
1832
|
}
|
|
1405
1833
|
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
1406
1834
|
throw new HTTPException(503, {
|
|
@@ -1456,26 +1884,8 @@ function requireOpaqueParam(context: Context, name: string): string {
|
|
|
1456
1884
|
return value;
|
|
1457
1885
|
}
|
|
1458
1886
|
|
|
1459
|
-
function requestOrigin(context: Context,
|
|
1460
|
-
|
|
1461
|
-
if (!value) return null;
|
|
1462
|
-
let url: URL;
|
|
1463
|
-
try {
|
|
1464
|
-
url = new URL(value);
|
|
1465
|
-
} catch {
|
|
1466
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
1467
|
-
}
|
|
1468
|
-
if (
|
|
1469
|
-
url.origin === "null" ||
|
|
1470
|
-
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
1471
|
-
url.origin !== value
|
|
1472
|
-
) {
|
|
1473
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
1474
|
-
}
|
|
1475
|
-
if (!allowedCorsOrigin(allowedPattern, url.origin)) {
|
|
1476
|
-
throw new HTTPException(403, { message: "request origin is not allowed" });
|
|
1477
|
-
}
|
|
1478
|
-
return url.origin;
|
|
1887
|
+
function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
|
|
1888
|
+
return validateInteractionRequestOrigin(context.req.header("origin"), settings);
|
|
1479
1889
|
}
|
|
1480
1890
|
|
|
1481
1891
|
function interactionActorForGrant(grant: AccessGrant): ReturnType<typeof InteractionActor.parse> {
|
|
@@ -1501,6 +1911,39 @@ function assertPlacementInstance(expected: string | null, actual: string): void
|
|
|
1501
1911
|
}
|
|
1502
1912
|
}
|
|
1503
1913
|
|
|
1914
|
+
/** End must still reach the live agent with the session's original token
|
|
1915
|
+
* fence. A later Chrome generation must not block ScreenCaptureKit teardown. */
|
|
1916
|
+
function attachedEndPlacementInstanceId(
|
|
1917
|
+
operation: ChannelAOperation,
|
|
1918
|
+
expectedPlacementInstanceId: string | null,
|
|
1919
|
+
liveGeneration: string,
|
|
1920
|
+
): string {
|
|
1921
|
+
if (operation === "computer.end" && expectedPlacementInstanceId) {
|
|
1922
|
+
return expectedPlacementInstanceId;
|
|
1923
|
+
}
|
|
1924
|
+
assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
|
|
1925
|
+
return liveGeneration;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function assertConnectedMachineComputerAccess(
|
|
1929
|
+
enrollment: {
|
|
1930
|
+
hasDisplay: boolean;
|
|
1931
|
+
desktopUnavailableReason: string | null;
|
|
1932
|
+
allowScreenControl: boolean;
|
|
1933
|
+
},
|
|
1934
|
+
operation: ChannelAOperation,
|
|
1935
|
+
): void {
|
|
1936
|
+
const error = connectedMachineComputerAccessError(
|
|
1937
|
+
enrollment,
|
|
1938
|
+
operation === "computer.action" || operation === "computer.control",
|
|
1939
|
+
);
|
|
1940
|
+
if (!error) return;
|
|
1941
|
+
if (error.status === 403) {
|
|
1942
|
+
throw new HTTPException(403, { message: error.message });
|
|
1943
|
+
}
|
|
1944
|
+
throw new ComputerSessionStateError(error.message);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1504
1947
|
function isTerminalOperation(state: string): boolean {
|
|
1505
1948
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
1506
1949
|
}
|
|
@@ -1521,7 +1964,11 @@ function interactionFailure(error: unknown) {
|
|
|
1521
1964
|
error instanceof BrowserControlUnsupportedError ||
|
|
1522
1965
|
error instanceof BrowserControlServerUnsupportedError
|
|
1523
1966
|
) {
|
|
1524
|
-
return {
|
|
1967
|
+
return {
|
|
1968
|
+
code: "unsupported" as const,
|
|
1969
|
+
message: error.message,
|
|
1970
|
+
retryable: false,
|
|
1971
|
+
};
|
|
1525
1972
|
}
|
|
1526
1973
|
if (error instanceof BrowserControlServerError) {
|
|
1527
1974
|
return {
|
|
@@ -1541,7 +1988,11 @@ function interactionFailure(error: unknown) {
|
|
|
1541
1988
|
}
|
|
1542
1989
|
|
|
1543
1990
|
function computerRouteError(error: unknown): HTTPException {
|
|
1991
|
+
const connectedMachineError = interactionControlApiError(error, "computer");
|
|
1992
|
+
if (connectedMachineError) return connectedMachineError;
|
|
1544
1993
|
if (error instanceof HTTPException) return error;
|
|
1994
|
+
if (error instanceof SandboxPaidComputeAdmissionError)
|
|
1995
|
+
return new HTTPException(402, { message: error.message, cause: error });
|
|
1545
1996
|
if (error instanceof ComputerSessionNotFoundError) {
|
|
1546
1997
|
return new HTTPException(404, { message: error.message, cause: error });
|
|
1547
1998
|
}
|