@opengeni/api-router 3.0.1 → 3.1.2
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 +8 -1
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.d.ts +50 -3
- package/dist/app.js +10 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/attached-browser-viewer.d.ts +11 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
- package/dist/auth/managed-auth.d.ts +72 -5
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/azure-live.d.ts +28 -0
- package/dist/browser-action-fences.d.ts +13 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-lost-runtime-end.d.ts +24 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-OT56OY4V.js} +80503 -39869
- package/dist/chunk-OT56OY4V.js.map +1 -0
- package/dist/claude-default-models.d.ts +19 -0
- package/dist/claude-subscription-core.d.ts +50 -0
- package/dist/claude-subscription-oauth.d.ts +36 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +6 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +40 -5
- package/dist/codex-realtime.d.ts +29 -3
- package/dist/codex-session-pointers.d.ts +19 -0
- package/dist/connection-ownership.d.ts +6 -3
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +5 -3
- package/dist/fatal-process-boundary.d.ts +9 -1
- package/dist/github-access.d.ts +7 -1
- package/dist/github-browser-flow.d.ts +1 -1
- package/dist/http/acting-person.d.ts +36 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +50 -1
- package/dist/http/auth.d.ts +8 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/deprecation.d.ts +42 -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/rejection-telemetry.d.ts +34 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/sse.d.ts +12 -1
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/variable-sets-removed.d.ts +41 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +1279 -108
- package/dist/index.js.map +1 -1
- package/dist/integration-connect-metrics.d.ts +35 -0
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +37 -66
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/integrations/oauth-client.d.ts +171 -12
- package/dist/integrations/oauth-profiles.d.ts +42 -13
- package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
- package/dist/integrations/slack-bot.d.ts +285 -7
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-interactions.d.ts +89 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +1 -1
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/slack-tool-review.d.ts +5 -0
- package/dist/interaction-agent-access.d.ts +13 -8
- package/dist/interaction-agent-version.d.ts +21 -0
- package/dist/interaction-holder-heartbeat.d.ts +5 -0
- package/dist/interaction-metrics.d.ts +27 -2
- package/dist/interaction-route-stages.d.ts +25 -0
- package/dist/interaction-secrets-vault.d.ts +20 -0
- package/dist/interaction-secrets.d.ts +103 -0
- package/dist/mcp/action-catalog-types.d.ts +14 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/action-descriptions.d.ts +4 -0
- package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
- package/dist/mcp/contract-input.d.ts +13 -0
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
- package/dist/mcp/notification-tools.d.ts +26 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/scheduled-task-input.d.ts +1309 -0
- package/dist/mcp/scheduled-task-view.d.ts +217 -3
- package/dist/mcp/secret-tools.d.ts +14 -0
- package/dist/mcp/server.d.ts +108 -5
- package/dist/mcp/session-admin-access-tools.d.ts +33 -0
- package/dist/mcp/session-event-errors.d.ts +28 -0
- package/dist/mcp/session-event-view.d.ts +62 -0
- package/dist/mcp/session-view.d.ts +15 -0
- package/dist/mcp/session-wait.d.ts +18 -0
- package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
- package/dist/mcp/site-data-tools.d.ts +22 -0
- package/dist/mcp/site-publication-tools.d.ts +19 -0
- package/dist/mcp/site-sql-tools.d.ts +27 -0
- package/dist/mcp-oauth.d.ts +37 -4
- package/dist/model-calls/chat-completions.d.ts +87 -0
- package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
- package/dist/model-calls/service.d.ts +36 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/native-push-dispatch.d.ts +69 -0
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +82 -0
- package/dist/prepared-mcp-actor.d.ts +77 -0
- package/dist/prepared-mcp-permissions.d.ts +8 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +11 -0
- package/dist/routes/api-keys.d.ts +7 -7
- package/dist/routes/automations.d.ts +2 -0
- package/dist/routes/billing.d.ts +59 -0
- package/dist/routes/browser-sessions.d.ts +65 -2
- package/dist/routes/chat-completions.d.ts +11 -0
- package/dist/routes/claude-core-usage.d.ts +20 -0
- 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 +182 -0
- package/dist/routes/codex-core.d.ts +401 -0
- package/dist/routes/codex.d.ts +64 -2
- package/dist/routes/computer-sessions.d.ts +6 -1
- package/dist/routes/desktop-distribution.d.ts +32 -0
- package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +27 -0
- package/dist/routes/inbox.d.ts +13 -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 +28 -0
- package/dist/routes/interaction-placement-message.d.ts +7 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-account-choice.d.ts +13 -0
- package/dist/routes/model-account-settings.d.ts +19 -0
- package/dist/routes/native-app-auth.d.ts +12 -0
- package/dist/routes/organization-integration-policy.d.ts +8 -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-defaults.d.ts +9 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/personal-api-keys.d.ts +3 -0
- package/dist/routes/session-admin-access.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +20 -0
- package/dist/routes/sessions.d.ts +38 -2
- package/dist/routes/site-data.d.ts +18 -0
- package/dist/routes/site-publishing.d.ts +5 -0
- package/dist/routes/site-runtime.d.ts +31 -0
- package/dist/routes/site-sql.d.ts +16 -0
- package/dist/routes/slack-bot-access.d.ts +3 -0
- package/dist/routes/subscription-account-pools.d.ts +21 -0
- package/dist/routes/subscription-core-http.d.ts +5 -0
- package/dist/routes/subscription-pool-access.d.ts +37 -0
- package/dist/routes/subscription-pool-core.d.ts +153 -0
- package/dist/routes/supergrok.d.ts +6 -6
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/variable-sets-removed.d.ts +2 -0
- package/dist/routes/vault-providers.d.ts +34 -0
- package/dist/routes/vaults.d.ts +3 -0
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +11 -2
- package/dist/sandbox/agent-update.d.ts +64 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +15 -1
- package/dist/sandbox/enrollment.d.ts +26 -4
- package/dist/sandbox/machine-audience.d.ts +29 -0
- package/dist/sandbox/machines.d.ts +15 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -2
- package/dist/sandbox/screen-control.d.ts +85 -0
- package/dist/sandbox/viewer.d.ts +5 -1
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/dist/scheduled-task-blockers.d.ts +39 -0
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/scheduled-task-lifecycle.d.ts +14 -0
- package/dist/secret-answers.d.ts +40 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-36GO3IL7.js +528 -0
- package/dist/session-event-view-36GO3IL7.js.map +1 -0
- package/dist/signed-state-secret.d.ts +18 -0
- package/dist/site-agent-guard.d.ts +53 -0
- package/dist/site-data/operations.d.ts +103 -0
- package/dist/site-data/service.d.ts +203 -0
- package/dist/site-frame-guard.d.ts +32 -0
- package/dist/site-publishing/agent-data-tools.d.ts +84 -0
- package/dist/site-publishing/agent-model.d.ts +58 -0
- package/dist/site-publishing/agent-session.d.ts +16 -0
- package/dist/site-publishing/content-scanner.d.ts +31 -0
- package/dist/site-publishing/edge-sync.d.ts +48 -0
- package/dist/site-publishing/gateway-signature.d.ts +56 -0
- package/dist/site-publishing/limits.d.ts +63 -0
- package/dist/site-publishing/page-tool-authority.d.ts +30 -0
- package/dist/site-publishing/platform-usage.d.ts +113 -0
- package/dist/site-publishing/platform.d.ts +21 -0
- package/dist/site-publishing/readiness.d.ts +62 -0
- package/dist/site-publishing/runtime.d.ts +179 -0
- package/dist/site-publishing/service.d.ts +264 -0
- package/dist/site-publishing/session-events.d.ts +22 -0
- package/dist/site-publishing/session-views.d.ts +36 -0
- package/dist/site-publishing/viewer.d.ts +40 -0
- package/dist/site-session-origin.d.ts +31 -6
- package/dist/site-sql/format.d.ts +47 -0
- package/dist/site-sql/gateway.d.ts +50 -0
- package/dist/site-sql/service.d.ts +213 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/billing-refusal.d.ts +8 -0
- package/dist/transcription/normalize.d.ts +23 -0
- package/dist/transcription/providers/azure-mai.d.ts +12 -0
- package/dist/transcription/providers/azure-openai.d.ts +4 -0
- package/dist/transcription/providers/openai.d.ts +16 -0
- package/dist/transcription/providers/subscription-core.d.ts +39 -0
- package/dist/transcription/service.d.ts +10 -1
- package/dist/transcription/wav-duration.d.ts +8 -0
- package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
- package/dist/workspace-tool-gateway-observability.d.ts +16 -0
- package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
- package/dist/workspace-tool-gateway.d.ts +102 -6
- package/dist/workspace-tool-target-fence.d.ts +116 -0
- package/dist/workspace-tool-target.d.ts +225 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/dist/xai-subscription-core.d.ts +60 -0
- package/package.json +25 -20
- package/src/access-grant-rls.ts +64 -14
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +1684 -252
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/attached-browser-viewer.ts +26 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +64 -29
- package/src/auth/managed-auth.ts +303 -35
- package/src/auth/managed-email.ts +18 -1
- 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 +3 -3
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/azure-live.ts +103 -0
- package/src/browser-action-fences.ts +45 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-lost-runtime-end.ts +71 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-default-models.ts +37 -0
- package/src/claude-subscription-core.ts +154 -0
- package/src/claude-subscription-oauth.ts +352 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +51 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +236 -39
- package/src/codex-realtime.ts +181 -53
- package/src/codex-session-pointers.ts +80 -0
- package/src/company-brain-okf.ts +1 -1
- package/src/connected-machine-computer-access.ts +2 -1
- package/src/connection-ownership.ts +19 -13
- package/src/controller-data-plane.ts +34 -0
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/editable-artifact-production.ts +10 -0
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +16 -3
- package/src/fatal-process-boundary.ts +67 -3
- package/src/gateway-realtime.ts +12 -8
- package/src/github-access.ts +31 -5
- package/src/github-browser-flow.ts +1 -1
- package/src/http/acting-person.ts +105 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +210 -2
- package/src/http/auth.ts +68 -4
- package/src/http/cors.ts +37 -5
- package/src/http/deprecation.ts +118 -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/rejection-telemetry.ts +120 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +88 -28
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/variable-sets-removed.ts +122 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +107 -57
- package/src/integration-connect-metrics.ts +228 -0
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +81 -7
- package/src/integrations/atlassian.ts +202 -83
- package/src/integrations/fiken.ts +66 -38
- package/src/integrations/github-app-connect.ts +64 -18
- package/src/integrations/github-lens-connect.ts +8 -29
- package/src/integrations/google-drive.ts +448 -152
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/oauth-client.ts +733 -193
- package/src/integrations/oauth-profiles.ts +116 -45
- package/src/integrations/onepassword-vault-provider.ts +16 -0
- package/src/integrations/personal-github-repositories.ts +1 -1
- package/src/integrations/personal-github.ts +61 -35
- package/src/integrations/provider-oauth.ts +67 -85
- package/src/integrations/slack-app-home.ts +5 -5
- package/src/integrations/slack-bot.ts +893 -201
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +7 -7
- package/src/integrations/slack-interactions.ts +1589 -299
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +2 -2
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/slack-tool-review.ts +65 -0
- package/src/integrations/social-oauth.ts +90 -58
- package/src/interaction-agent-access.ts +40 -20
- package/src/interaction-agent-version.ts +85 -0
- package/src/interaction-holder-heartbeat.ts +24 -8
- package/src/interaction-metrics.ts +164 -2
- package/src/interaction-route-stages.ts +94 -0
- package/src/interaction-secrets-vault.ts +213 -0
- package/src/interaction-secrets.ts +426 -0
- package/src/mcp/action-catalog-types.ts +14 -0
- package/src/mcp/action-catalog.gen.ts +8014 -0
- package/src/mcp/action-descriptions.ts +299 -0
- package/src/mcp/action-schema-imports.gen.ts +304 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/company-profile-agent-admin.ts +13 -0
- package/src/mcp/contract-input.ts +165 -0
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/editable-artifacts.ts +29 -11
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +7 -285
- package/src/mcp/knowledge-entries.ts +108 -34
- package/src/mcp/notification-tools.ts +480 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/scheduled-task-input.ts +89 -0
- package/src/mcp/scheduled-task-view.ts +63 -3
- package/src/mcp/secret-tools.ts +185 -0
- package/src/mcp/server.ts +2213 -1116
- package/src/mcp/session-admin-access-tools.ts +163 -0
- package/src/mcp/session-event-errors.ts +123 -0
- package/src/mcp/session-event-view.ts +395 -13
- package/src/mcp/session-view.ts +3 -0
- package/src/mcp/session-wait.ts +67 -7
- package/src/mcp/site-agent-clamp-tools.ts +140 -0
- package/src/mcp/site-data-tools.ts +339 -0
- package/src/mcp/site-publication-tools.ts +201 -0
- package/src/mcp/site-sql-tools.ts +427 -0
- package/src/mcp-oauth.ts +635 -45
- package/src/model-calls/chat-completions.ts +352 -0
- package/src/model-calls/pending-usage-settlements.ts +125 -0
- package/src/model-calls/service.ts +694 -0
- package/src/model-catalog.ts +68 -17
- package/src/native-push-dispatch.ts +457 -0
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +485 -0
- package/src/prepared-mcp-actor.ts +154 -0
- package/src/prepared-mcp-permissions.ts +38 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +268 -9
- package/src/routes/api-keys.ts +164 -25
- package/src/routes/artifact-catalog.ts +111 -0
- package/src/routes/automations.ts +163 -90
- package/src/routes/billing.ts +535 -59
- package/src/routes/browser-sessions.ts +966 -342
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +112 -5
- package/src/routes/channels.ts +4 -3
- package/src/routes/chat-completions.ts +235 -0
- package/src/routes/claude-core-usage.ts +147 -0
- package/src/routes/claude-subscription-accounts.ts +233 -0
- package/src/routes/claude-subscription-oauth.ts +175 -0
- package/src/routes/client-errors.ts +394 -0
- package/src/routes/codex-core.ts +687 -0
- package/src/routes/codex.ts +1277 -1119
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +319 -85
- package/src/routes/connect.ts +277 -175
- package/src/routes/connections.ts +690 -363
- package/src/routes/desktop-distribution.ts +217 -0
- package/src/routes/documents.ts +13 -7
- package/src/routes/editable-artifacts.ts +74 -4
- package/src/routes/enrollments.ts +110 -16
- package/src/routes/external-identity-links.ts +13 -12
- package/src/routes/files.ts +41 -16
- package/src/routes/github-browser-fonts.gen.ts +12 -0
- package/src/routes/github-browser-pages.ts +258 -0
- package/src/routes/github.ts +200 -75
- package/src/routes/inbox.ts +353 -0
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +103 -8
- package/src/routes/install.ts +51 -10
- package/src/routes/integration-facets.ts +70 -28
- package/src/routes/interaction-placement-message.ts +12 -0
- package/src/routes/knowledge.ts +136 -27
- package/src/routes/machines.ts +189 -135
- package/src/routes/managed-auth-session-sets.ts +43 -10
- package/src/routes/managed-onboarding.ts +39 -19
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-account-choice.ts +194 -0
- package/src/routes/model-account-settings.ts +235 -0
- package/src/routes/model-connection-access.ts +210 -48
- package/src/routes/native-app-auth.ts +296 -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 +183 -0
- package/src/routes/organization-memberships.ts +205 -19
- package/src/routes/organization-model-defaults.ts +110 -0
- package/src/routes/organization-model-providers.ts +74 -3
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +21 -5
- package/src/routes/personal-api-keys.ts +184 -0
- package/src/routes/personal-github-git-broker.ts +2 -2
- package/src/routes/personal-github.ts +9 -4
- package/src/routes/plugins.ts +46 -7
- package/src/routes/pr-review-github.ts +105 -118
- package/src/routes/pr-review.ts +88 -82
- package/src/routes/preference-registry.ts +18 -0
- package/src/routes/rigs.ts +48 -13
- package/src/routes/scheduled-tasks.ts +347 -27
- package/src/routes/session-admin-access.ts +271 -0
- package/src/routes/session-artifact-associations.ts +164 -0
- package/src/routes/session-history-imports.ts +305 -0
- package/src/routes/sessions.ts +1682 -363
- package/src/routes/site-data.ts +340 -0
- package/src/routes/site-publishing.ts +584 -0
- package/src/routes/site-runtime.ts +1944 -0
- package/src/routes/site-sql.ts +263 -0
- package/src/routes/skill-content.ts +72 -7
- package/src/routes/skills.ts +5 -4
- package/src/routes/slack-bot-access.ts +108 -0
- package/src/routes/social.ts +27 -15
- package/src/routes/subscription-account-pools.ts +209 -0
- package/src/routes/subscription-core-http.ts +18 -0
- package/src/routes/subscription-pool-access.ts +180 -0
- package/src/routes/subscription-pool-core.ts +552 -0
- package/src/routes/supergrok.ts +174 -655
- package/src/routes/transcription-recordings.ts +71 -4
- package/src/routes/transcriptions.ts +26 -2
- package/src/routes/usage-allowances.ts +381 -0
- package/src/routes/variable-sets-removed.ts +19 -0
- package/src/routes/vault-providers.ts +782 -0
- package/src/routes/vaults.ts +421 -0
- package/src/routes/video-generation.ts +30 -12
- package/src/routes/workspace-artifacts.ts +165 -53
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +625 -117
- package/src/sandbox/agent-update.ts +315 -0
- package/src/sandbox/auth-callout.ts +104 -21
- package/src/sandbox/channel-a.ts +165 -55
- package/src/sandbox/enrollment.ts +131 -22
- package/src/sandbox/machine-audience.ts +192 -0
- package/src/sandbox/machines.ts +75 -31
- package/src/sandbox/metrics-ingestion.ts +133 -2
- package/src/sandbox/rematerialize.ts +51 -21
- package/src/sandbox/screen-control.ts +289 -0
- package/src/sandbox/viewer.ts +33 -58
- package/src/sandbox-file-artifacts.ts +53 -27
- package/src/scheduled-task-blockers.ts +293 -0
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/scheduled-task-lifecycle.ts +84 -0
- package/src/secret-answers.ts +119 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +26 -4
- package/src/signed-state-secret.ts +58 -0
- package/src/site-agent-guard.ts +261 -0
- package/src/site-data/operations.ts +777 -0
- package/src/site-data/service.ts +1043 -0
- package/src/site-frame-guard.ts +107 -0
- package/src/site-publishing/agent-data-tools.ts +250 -0
- package/src/site-publishing/agent-model.ts +127 -0
- package/src/site-publishing/agent-session.ts +148 -0
- package/src/site-publishing/content-scanner.ts +93 -0
- package/src/site-publishing/edge-sync.ts +148 -0
- package/src/site-publishing/gateway-signature.ts +198 -0
- package/src/site-publishing/limits.ts +315 -0
- package/src/site-publishing/page-tool-authority.ts +111 -0
- package/src/site-publishing/platform-usage.ts +479 -0
- package/src/site-publishing/platform.ts +98 -0
- package/src/site-publishing/readiness.ts +330 -0
- package/src/site-publishing/runtime.ts +831 -0
- package/src/site-publishing/service.ts +1282 -0
- package/src/site-publishing/session-events.ts +262 -0
- package/src/site-publishing/session-views.ts +262 -0
- package/src/site-publishing/viewer.ts +75 -0
- package/src/site-session-origin.ts +91 -6
- package/src/site-sql/format.ts +134 -0
- package/src/site-sql/gateway.ts +168 -0
- package/src/site-sql/service.ts +1636 -0
- package/src/site-uploads.ts +27 -5
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/billing-refusal.ts +14 -0
- package/src/transcription/normalize.ts +77 -0
- package/src/transcription/providers/azure-mai.ts +98 -0
- package/src/transcription/providers/azure-openai.ts +16 -7
- package/src/transcription/providers/codex-subscription.ts +190 -67
- package/src/transcription/providers/openai.ts +79 -5
- package/src/transcription/providers/subscription-core.ts +125 -0
- package/src/transcription/providers/xai-subscription.ts +81 -83
- package/src/transcription/segmenter.ts +7 -1
- package/src/transcription/service.ts +239 -10
- package/src/transcription/wav-duration.ts +39 -0
- package/src/workspace-deletion.ts +25 -1
- package/src/workspace-tool-gateway-attestations.ts +133 -0
- package/src/workspace-tool-gateway-observability.ts +64 -0
- package/src/workspace-tool-gateway-secrets.ts +104 -0
- package/src/workspace-tool-gateway.ts +921 -103
- package/src/workspace-tool-target-fence.ts +380 -0
- package/src/workspace-tool-target.ts +554 -0
- package/src/workspace-webhook-dispatch.ts +225 -0
- package/src/xai-realtime.ts +85 -81
- package/src/xai-subscription-core.ts +172 -0
- package/dist/browser-auth-broker.d.ts +0 -20
- package/dist/chunk-AJ62T5QN.js.map +0 -1
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/environments.d.ts +0 -5
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/dist/session-event-view-MU3HMNV3.js +0 -264
- package/dist/session-event-view-MU3HMNV3.js.map +0 -1
- package/dist/xai-subscription-auth.d.ts +0 -23
- package/src/browser-auth-broker.ts +0 -155
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/environments.ts +0 -388
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
- package/src/xai-subscription-auth.ts +0 -139
package/src/routes/sessions.ts
CHANGED
|
@@ -1,7 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
legacyVariableSetFields,
|
|
3
|
+
rejectLegacyVariableSetFields,
|
|
4
|
+
variableSetsRemovedResponse,
|
|
5
|
+
withoutEmptyLegacyVariableSetFields,
|
|
6
|
+
} from "../http/variable-sets-removed";
|
|
7
|
+
import { AZURE_LIVE_MODEL_ID, buildSessionAzureLiveBroker } from "../azure-live";
|
|
1
8
|
import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
9
|
+
import { assertGoalResumeAllowed, GoalResumeBlockedError } from "@opengeni/core";
|
|
10
|
+
import {
|
|
11
|
+
agentConfigDeploymentPolicy,
|
|
12
|
+
resolveFirstPartyMcpToolPolicy,
|
|
13
|
+
UnsupportedLatencyModeError,
|
|
14
|
+
} from "@opengeni/config";
|
|
15
|
+
import { siteAgentDataToolNames, siteAgentSiteSqlOn } from "../site-publishing/agent-data-tools";
|
|
16
|
+
import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
|
|
17
|
+
import { isVaultError, vaultErrorFromDatabase } from "@opengeni/db";
|
|
18
|
+
import { listSessionEventSlices } from "@opengeni/db/session-event-slices";
|
|
19
|
+
import * as sessionPreviewSchema from "@opengeni/db/schema";
|
|
20
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
21
|
+
import {
|
|
22
|
+
FIRST_PARTY_MCP_TOOL_CAPABILITIES,
|
|
23
|
+
FirstPartyMcpToolName,
|
|
24
|
+
resolveWorkspaceAgentDefaults,
|
|
25
|
+
SessionMessageSearchRequest,
|
|
26
|
+
type AgentCapabilities,
|
|
27
|
+
type ToolRef,
|
|
28
|
+
} from "@opengeni/contracts";
|
|
29
|
+
import {
|
|
30
|
+
SITE_FRAME_SESSION_CREATE_FIELDS,
|
|
31
|
+
siteFrameBodyRefusal,
|
|
32
|
+
} from "@opengeni/contracts/site-session-http";
|
|
33
|
+
import { requireWorkspace, scheduledSessionIds } from "@opengeni/db";
|
|
34
|
+
import {
|
|
35
|
+
currentSiteSessionOrigin,
|
|
36
|
+
isVerifiedDelegatedHumanAuthorization,
|
|
37
|
+
withSiteSessionOrigin,
|
|
38
|
+
} from "@opengeni/core";
|
|
39
|
+
import { freezeSessionRealtimeConnectionAccounts } from "@opengeni/core";
|
|
40
|
+
import {
|
|
41
|
+
createRealtimeVoiceBilling,
|
|
42
|
+
RealtimeVoiceUnavailableError,
|
|
43
|
+
TranscriptionBillingRefusedError,
|
|
44
|
+
} from "@opengeni/core";
|
|
45
|
+
import type { CreditDebitAttribution, SessionForkPointRefusal } from "@opengeni/db";
|
|
46
|
+
import { creditDebitAttributionForGrant } from "../access-grant-rls";
|
|
47
|
+
import { transcriptionBillingRefusal } from "../transcription/billing-refusal";
|
|
48
|
+
import { resolveSiteSessionOrigin, withOptionalSiteCommandOrigin } from "../site-session-origin";
|
|
49
|
+
import { SandboxRecoveryRequest } from "@opengeni/contracts";
|
|
50
|
+
import { getManagedHumanSandboxRecovery, consentManagedHumanSandboxRecovery } from "@opengeni/core";
|
|
51
|
+
import { SandboxRecoveryConflictError } from "@opengeni/db";
|
|
52
|
+
import { codexAccountJson } from "./codex";
|
|
53
|
+
import {
|
|
54
|
+
deliverSubscriptionCoreCodexWake,
|
|
55
|
+
getSubscriptionCoreSessionCodexAccounts,
|
|
56
|
+
pinSubscriptionCoreSessionCodexAccount,
|
|
57
|
+
} from "@opengeni/db";
|
|
58
|
+
import { codexRouteDisposition } from "./codex-core";
|
|
59
|
+
import {
|
|
60
|
+
loadCodexSessionPointerProjection,
|
|
61
|
+
type CodexSessionPointerProjection,
|
|
62
|
+
} from "../codex-session-pointers";
|
|
63
|
+
import { getToolActionReview, getToolReviewDetailsPage } from "@opengeni/db";
|
|
5
64
|
import {
|
|
6
65
|
AcknowledgeStreamRequest,
|
|
7
66
|
ApplySessionGoalRevisionRequest,
|
|
@@ -43,6 +102,7 @@ import {
|
|
|
43
102
|
RenewSessionRealtimeRequest,
|
|
44
103
|
SyncSessionRealtimeLedgerRequest,
|
|
45
104
|
SessionControlRequest,
|
|
105
|
+
SessionRetryRequest,
|
|
46
106
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
47
107
|
SessionEventPayloadMode,
|
|
48
108
|
SessionEventReadDirection,
|
|
@@ -64,13 +124,18 @@ import {
|
|
|
64
124
|
UpdateSessionChannelRequest,
|
|
65
125
|
UpdateSessionAttentionRequest,
|
|
66
126
|
UpdateSessionArchiveRequest,
|
|
127
|
+
UpdateSessionRetentionRequest,
|
|
128
|
+
SetSessionUsageLimitRequest,
|
|
67
129
|
UpdateSessionPinRequest,
|
|
68
130
|
UpdateSessionGoalRequest,
|
|
69
131
|
UpdateSessionMcpApprovalPolicyRequest,
|
|
70
132
|
UpdateSessionRequest,
|
|
71
|
-
UpdateSessionVariableSetsRequest,
|
|
72
133
|
UpdateSessionVisibilityRequest,
|
|
134
|
+
SessionVisibility,
|
|
73
135
|
UpdateSessionToolPolicyRequest,
|
|
136
|
+
UpdateSessionAgentRequest,
|
|
137
|
+
UpdateSessionSkillsRequest,
|
|
138
|
+
AgentConfigError,
|
|
74
139
|
ViewerHeartbeatRequest,
|
|
75
140
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
76
141
|
WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
|
|
@@ -98,7 +163,8 @@ import {
|
|
|
98
163
|
type TerminalPtyExitedPayload,
|
|
99
164
|
type TerminalPtyOutputDeltaPayload,
|
|
100
165
|
type TerminalPtyStartedPayload,
|
|
101
|
-
type
|
|
166
|
+
type VaultApiError,
|
|
167
|
+
VAULT_ERROR_HTTP_STATUS,
|
|
102
168
|
} from "@opengeni/contracts";
|
|
103
169
|
import { streamTokenDegraded } from "@opengeni/config";
|
|
104
170
|
import {
|
|
@@ -112,6 +178,7 @@ import {
|
|
|
112
178
|
getLiveEnrollmentConnection,
|
|
113
179
|
getRetainedProcess,
|
|
114
180
|
getSandbox,
|
|
181
|
+
getPublishedSiteVersion,
|
|
115
182
|
getSession,
|
|
116
183
|
readActiveSandbox,
|
|
117
184
|
deleteSessionTreeIfQuiescent,
|
|
@@ -131,18 +198,18 @@ import {
|
|
|
131
198
|
listSessionGoalRevisions,
|
|
132
199
|
rejectSessionGoalRevisionWithEvent,
|
|
133
200
|
listSessionIdsInGroup,
|
|
201
|
+
countAllSessionsInGroup,
|
|
134
202
|
listSessionDiscoverySummaries,
|
|
135
203
|
listSessionDiscoveryAncestorPaths,
|
|
136
204
|
listSessionsForSubject,
|
|
205
|
+
listSessionEntriesForSubject,
|
|
137
206
|
getLatestStartedSessionTurn,
|
|
138
207
|
listSessionTurns,
|
|
139
208
|
projectEffectiveControlForRelatedAccess,
|
|
140
209
|
projectSessionForRelatedAccess,
|
|
141
210
|
recordStreamAcknowledgment,
|
|
142
211
|
requestSessionCompaction,
|
|
143
|
-
switchSessionCodexAccount,
|
|
144
212
|
setSessionChannel,
|
|
145
|
-
updateSessionVariableSets,
|
|
146
213
|
ChannelNotFoundError,
|
|
147
214
|
setSessionAttention,
|
|
148
215
|
setSessionArchive,
|
|
@@ -160,6 +227,7 @@ import {
|
|
|
160
227
|
upsertSessionGoalWithEvent,
|
|
161
228
|
updatePtySessionActivity,
|
|
162
229
|
QueueCommandConflictError,
|
|
230
|
+
PersonalSessionInitiatorRequiredError,
|
|
163
231
|
beginSessionRealtimeInTransaction,
|
|
164
232
|
activateSessionRealtimeConnectionInTransaction,
|
|
165
233
|
claimSessionRealtimeConnectionInTransaction,
|
|
@@ -173,17 +241,20 @@ import {
|
|
|
173
241
|
SessionToolPolicyVersionConflictError,
|
|
174
242
|
SessionContextBusyError,
|
|
175
243
|
SessionVariableSetSelectionUnavailableError,
|
|
244
|
+
SessionCreateConnectionSelectionUnavailableError,
|
|
176
245
|
workspaceControlRequestLockTimeoutMs,
|
|
177
246
|
SessionTenancyAccessError,
|
|
178
247
|
SessionTenancyConflictError,
|
|
179
248
|
SessionTenancyInvalidRequestError,
|
|
180
249
|
SessionTenancyNotActivatedError,
|
|
250
|
+
SessionVisibilityWorkConflictError,
|
|
181
251
|
HumanInputResponseValidationError,
|
|
182
252
|
SkillHumanResponseError,
|
|
183
253
|
latestWorkspaceCapture,
|
|
184
254
|
sessionLatestWorkspaceCapture,
|
|
185
255
|
renewSessionRealtimeInTransaction,
|
|
186
256
|
syncSessionRealtimeLedgerInTransaction,
|
|
257
|
+
setSessionKeepLive,
|
|
187
258
|
withWorkspaceSessionActivityRls,
|
|
188
259
|
withWorkspaceRls,
|
|
189
260
|
workspaceCaptureAtRevision,
|
|
@@ -257,6 +328,7 @@ import {
|
|
|
257
328
|
type ResolvedSessionAuthorization,
|
|
258
329
|
} from "@opengeni/core";
|
|
259
330
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
331
|
+
import { SessionRetryConflictError } from "@opengeni/db";
|
|
260
332
|
import {
|
|
261
333
|
attachViewer,
|
|
262
334
|
detachViewer,
|
|
@@ -273,7 +345,9 @@ import {
|
|
|
273
345
|
import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../codex-realtime";
|
|
274
346
|
import {
|
|
275
347
|
acceptSessionUserMessage,
|
|
348
|
+
validateSessionMcpCredentialUpdates,
|
|
276
349
|
controlHumanSessionWorkstream,
|
|
350
|
+
retryFailedSession,
|
|
277
351
|
createSessionForRequest,
|
|
278
352
|
deleteHumanQueuePrompt,
|
|
279
353
|
editHumanQueuePrompt,
|
|
@@ -291,16 +365,29 @@ import {
|
|
|
291
365
|
submitComposerDraftForRequest,
|
|
292
366
|
updateSessionMcpApprovalPolicy,
|
|
293
367
|
updateManagedHumanSessionVisibility,
|
|
368
|
+
previewManagedHumanSessionVisibilityChange,
|
|
294
369
|
updateSessionToolPolicy,
|
|
370
|
+
updateSessionAgent,
|
|
371
|
+
updateSessionSkills,
|
|
295
372
|
updateSessionTitle,
|
|
373
|
+
setSessionRetention,
|
|
296
374
|
workflowIdForSession,
|
|
297
375
|
sessionWithEffectiveToolPolicy,
|
|
298
|
-
|
|
299
|
-
|
|
376
|
+
workspaceSessionEffectiveToolsContext,
|
|
377
|
+
workspaceSessionToolPolicyContext,
|
|
300
378
|
relayConfigFromSettings,
|
|
301
|
-
|
|
379
|
+
getSessionUsageLimitForAccess,
|
|
380
|
+
SessionUsageLimitHttpError,
|
|
381
|
+
sessionUsageLimitForRead,
|
|
382
|
+
setSessionUsageLimitForAccess,
|
|
302
383
|
} from "@opengeni/core";
|
|
303
384
|
import { assertSessionExists, boundedLimit } from "../http/common";
|
|
385
|
+
import { isAgentActingAsPerson } from "../http/acting-person";
|
|
386
|
+
import {
|
|
387
|
+
humanSecretWriteContext,
|
|
388
|
+
prepareSecretAnswers,
|
|
389
|
+
splitSecretAnswers,
|
|
390
|
+
} from "../secret-answers";
|
|
304
391
|
import { browserSseDeliveryOptions, sseSessionStream } from "../http/sse";
|
|
305
392
|
import {
|
|
306
393
|
serveWorkspaceCapture,
|
|
@@ -310,6 +397,9 @@ import {
|
|
|
310
397
|
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
311
398
|
import { ApiHttpError } from "../http/api-error";
|
|
312
399
|
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
400
|
+
import { recordAcceptedApiAdmission } from "../admission-trace";
|
|
401
|
+
import { parseRequestBody, parseRequestJson } from "../http/request-body";
|
|
402
|
+
import { measureSessionCreatePhase } from "../session-create-observability";
|
|
313
403
|
|
|
314
404
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
315
405
|
|
|
@@ -350,6 +440,66 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
350
440
|
observability: deps.observability,
|
|
351
441
|
};
|
|
352
442
|
const workspaceCaptureManifestCache = new WorkspaceCaptureManifestCache();
|
|
443
|
+
const realtimeVoiceBilling = createRealtimeVoiceBilling({ db, settings });
|
|
444
|
+
/** Credit refusal / unavailable deployment voice as an HTTP response, else null. */
|
|
445
|
+
const realtimeVoiceRefusalResponse = (c: Context, error: unknown): Response | null => {
|
|
446
|
+
if (error instanceof TranscriptionBillingRefusedError) {
|
|
447
|
+
return transcriptionBillingRefusal(c, error);
|
|
448
|
+
}
|
|
449
|
+
if (error instanceof RealtimeVoiceUnavailableError) {
|
|
450
|
+
return c.json(
|
|
451
|
+
{
|
|
452
|
+
error: {
|
|
453
|
+
status: 409,
|
|
454
|
+
code: "realtime_voice_unavailable",
|
|
455
|
+
message: error.message,
|
|
456
|
+
retryable: false,
|
|
457
|
+
details: { reason: error.code },
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
409,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Bill every started minute observed so far for a deployment-funded call.
|
|
467
|
+
* Owner attribution only when the caller is the voice owner. A settlement
|
|
468
|
+
* failure is logged and retried idempotently on the next heartbeat.
|
|
469
|
+
*/
|
|
470
|
+
const settleRealtimeVoice = async (input: {
|
|
471
|
+
grant: { subjectId: string };
|
|
472
|
+
attribution: CreditDebitAttribution;
|
|
473
|
+
workspaceId: string;
|
|
474
|
+
sessionId: string;
|
|
475
|
+
realtimeId: string;
|
|
476
|
+
}) => {
|
|
477
|
+
try {
|
|
478
|
+
return await realtimeVoiceBilling.settle({
|
|
479
|
+
workspaceId: input.workspaceId,
|
|
480
|
+
sessionId: input.sessionId,
|
|
481
|
+
realtimeId: input.realtimeId,
|
|
482
|
+
callerSubjectId: input.grant.subjectId,
|
|
483
|
+
attribution: input.attribution,
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error("realtime voice settlement failed", {
|
|
487
|
+
workspaceId: input.workspaceId,
|
|
488
|
+
sessionId: input.sessionId,
|
|
489
|
+
realtimeId: input.realtimeId,
|
|
490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
491
|
+
});
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const withSiteCommandOrigin = <T>(c: Context, grant: AccessGrant, run: () => Promise<T>) =>
|
|
496
|
+
withOptionalSiteCommandOrigin(
|
|
497
|
+
db,
|
|
498
|
+
grant,
|
|
499
|
+
c.req.header("x-opengeni-site-id"),
|
|
500
|
+
c.req.header("x-opengeni-site-version"),
|
|
501
|
+
run,
|
|
502
|
+
);
|
|
353
503
|
const ptyIdentity = (pty: SandboxOpenPtySessionRow): SandboxPtyProcessIdentity => ({
|
|
354
504
|
leaseId: pty.leaseId,
|
|
355
505
|
sandboxGroupId: pty.sandboxGroupId,
|
|
@@ -477,7 +627,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
477
627
|
});
|
|
478
628
|
|
|
479
629
|
// Every deployment has one fail-closed authorization seam for every HTTP
|
|
480
|
-
// session surface. The core boundary always enforces durable
|
|
630
|
+
// session surface. The core boundary always enforces durable Opengeni-owned
|
|
481
631
|
// private-session rules; an embedding host port can add narrower policy.
|
|
482
632
|
// Register it before the routes so a newly added path cannot accidentally
|
|
483
633
|
// inherit workspace access without an explicit operation classification. The
|
|
@@ -557,16 +707,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
557
707
|
|
|
558
708
|
app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
559
709
|
const workspaceId = c.req.param("workspaceId");
|
|
560
|
-
const authorization = await
|
|
561
|
-
c,
|
|
562
|
-
deps,
|
|
563
|
-
workspaceId,
|
|
564
|
-
"sessions:create",
|
|
710
|
+
const authorization = await measureSessionCreatePhase(deps.observability, "authorization", () =>
|
|
711
|
+
requireAccessGrantAuthorization(c, deps, workspaceId, "sessions:create"),
|
|
565
712
|
);
|
|
566
713
|
const grant = authorization.grant;
|
|
567
714
|
let payload: unknown;
|
|
568
715
|
try {
|
|
569
|
-
payload = await
|
|
716
|
+
payload = await measureSessionCreatePhase(deps.observability, "body_read", () =>
|
|
717
|
+
c.req.json(),
|
|
718
|
+
);
|
|
570
719
|
} catch {
|
|
571
720
|
return c.json(
|
|
572
721
|
{
|
|
@@ -578,23 +727,93 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
578
727
|
}
|
|
579
728
|
let session: Session;
|
|
580
729
|
try {
|
|
730
|
+
const legacyFields = legacyVariableSetFields(payload);
|
|
731
|
+
if (legacyFields.length > 0) return variableSetsRemovedResponse(c, 422, legacyFields);
|
|
732
|
+
payload = withoutEmptyLegacyVariableSetFields(payload);
|
|
581
733
|
CreateSessionRequest.parse(payload);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
c
|
|
734
|
+
// only a person present in the app or the host's server sets a
|
|
735
|
+
// task usage limit; an agent acting for a person cannot, even null.
|
|
736
|
+
if (
|
|
737
|
+
(payload as { usageLimitMicros?: unknown }).usageLimitMicros !== undefined &&
|
|
738
|
+
isAgentActingAsPerson(c)
|
|
739
|
+
) {
|
|
740
|
+
throw new SessionUsageLimitHttpError(
|
|
741
|
+
403,
|
|
742
|
+
"session_usage_limit_forbidden",
|
|
743
|
+
"Only a person or the application's server can set a task usage limit.",
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const origin = await measureSessionCreatePhase(deps.observability, "site_origin", () =>
|
|
747
|
+
resolveSiteSessionOrigin(
|
|
748
|
+
db,
|
|
749
|
+
grant,
|
|
750
|
+
c.req.header("x-opengeni-site-id"),
|
|
751
|
+
c.req.header("x-opengeni-site-version"),
|
|
752
|
+
// A frame without a published Site (inline HTML, a preview) is
|
|
753
|
+
// recorded as that frame, so its later requests reach this session.
|
|
754
|
+
{
|
|
755
|
+
marker: c.req.header("x-opengeni-site-frame"),
|
|
756
|
+
frameId: c.req.header("x-opengeni-site-frame-id"),
|
|
757
|
+
},
|
|
758
|
+
),
|
|
587
759
|
);
|
|
760
|
+
// A Site page may start a conversation with a message and a model, and
|
|
761
|
+
// nothing else: the session gets the minimal Site profile. Only the Site
|
|
762
|
+
// itself, running its published agent as `site:<siteId>`, chooses tools.
|
|
763
|
+
const siteOrigin = origin ?? currentSiteSessionOrigin();
|
|
764
|
+
if (siteOrigin && grant.subjectId !== `site:${siteOrigin.siteId}`) {
|
|
765
|
+
const refused = siteFrameBodyRefusal("sessions.create", "/sessions", payload);
|
|
766
|
+
if (refused)
|
|
767
|
+
return c.json(
|
|
768
|
+
{
|
|
769
|
+
code: "SITE_SESSION_TOOLS_FORBIDDEN",
|
|
770
|
+
message:
|
|
771
|
+
"A Site page can start a session with a message and a model only, not choose its tools, servers, connections, permissions, vaults, sandbox, resources, Skills or learning",
|
|
772
|
+
details: { code: "site_frame_field_forbidden", field: refused },
|
|
773
|
+
},
|
|
774
|
+
403,
|
|
775
|
+
);
|
|
776
|
+
payload = await siteFrameSessionProfile(deps, workspaceId, payload, {
|
|
777
|
+
accountId: grant.accountId,
|
|
778
|
+
siteId: siteOrigin.siteId,
|
|
779
|
+
versionId: siteOrigin.versionId ?? null,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
588
782
|
const create = () =>
|
|
589
783
|
createSessionForRequest(deps, grant, workspaceId, payload, authorization);
|
|
590
|
-
session = await (
|
|
784
|
+
session = await measureSessionCreatePhase(deps.observability, "core_create", () =>
|
|
785
|
+
origin ? withSiteSessionOrigin(origin, create) : create(),
|
|
786
|
+
);
|
|
591
787
|
} catch (error) {
|
|
592
788
|
return sessionCreateErrorResponse(c, error);
|
|
593
789
|
}
|
|
790
|
+
// A creator may exempt its new session from the idle-session archive (a
|
|
791
|
+
// persistent agent, for example). Creation committed; this only marks it.
|
|
792
|
+
if ((payload as { keepLive?: unknown }).keepLive === true) {
|
|
793
|
+
const kept = await setSessionKeepLive(db, {
|
|
794
|
+
workspaceId,
|
|
795
|
+
sessionId: session.id,
|
|
796
|
+
keepLive: true,
|
|
797
|
+
});
|
|
798
|
+
if (kept.status === "updated") session = { ...session, retention: kept.retention };
|
|
799
|
+
}
|
|
800
|
+
// Echo a requested usage limit so clients can tell this server applied it
|
|
801
|
+
// (older servers strip the unknown field and would run unlimited).
|
|
802
|
+
if ((payload as { usageLimitMicros?: unknown }).usageLimitMicros !== undefined) {
|
|
803
|
+
const usageLimit = await sessionUsageLimitForRead(db, session, {
|
|
804
|
+
subjectId: grant.subjectId,
|
|
805
|
+
});
|
|
806
|
+
if (usageLimit) session = { ...session, usageLimit };
|
|
807
|
+
}
|
|
594
808
|
// Creation has committed by this point. Keep response projection outside
|
|
595
809
|
// the create-rejection boundary so a post-commit policy read cannot be
|
|
596
810
|
// misreported as though the session itself was rejected.
|
|
597
|
-
return c.json(
|
|
811
|
+
return c.json(
|
|
812
|
+
await measureSessionCreatePhase(deps.observability, "response_projection", () =>
|
|
813
|
+
withEffectivePolicy(deps, workspaceId, grant.subjectId, session),
|
|
814
|
+
),
|
|
815
|
+
202,
|
|
816
|
+
);
|
|
598
817
|
});
|
|
599
818
|
|
|
600
819
|
app.get("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
|
|
@@ -681,6 +900,59 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
681
900
|
}
|
|
682
901
|
});
|
|
683
902
|
|
|
903
|
+
app.get("/v1/workspaces/:workspaceId/session-message-search", async (c) => {
|
|
904
|
+
const workspaceId = c.req.param("workspaceId");
|
|
905
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
906
|
+
c,
|
|
907
|
+
deps,
|
|
908
|
+
workspaceId,
|
|
909
|
+
"sessions:read",
|
|
910
|
+
);
|
|
911
|
+
const grant = authorization.grant;
|
|
912
|
+
// A single-session filter is not authority: both searches use the complete
|
|
913
|
+
// live host/agent list scope, plus ordinary private/archive/RLS boundaries.
|
|
914
|
+
let authorizationScope;
|
|
915
|
+
try {
|
|
916
|
+
authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
|
|
917
|
+
} catch (error) {
|
|
918
|
+
throw sessionAuthorizationHttpError(error);
|
|
919
|
+
}
|
|
920
|
+
const raw = c.req.query();
|
|
921
|
+
const parsed = SessionMessageSearchRequest.safeParse({
|
|
922
|
+
...raw,
|
|
923
|
+
...(raw.limit !== undefined ? { limit: Number(raw.limit) } : {}),
|
|
924
|
+
});
|
|
925
|
+
if (!parsed.success)
|
|
926
|
+
throw new HTTPException(400, { message: "Invalid session message search request" });
|
|
927
|
+
try {
|
|
928
|
+
return c.json(
|
|
929
|
+
await searchSessionMessagesForSubject(
|
|
930
|
+
db,
|
|
931
|
+
workspaceId,
|
|
932
|
+
parsed.data,
|
|
933
|
+
{
|
|
934
|
+
subjectId: grant.subjectId,
|
|
935
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
936
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
937
|
+
},
|
|
938
|
+
{ signal: c.req.raw.signal },
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (error instanceof SessionListAccessError)
|
|
943
|
+
throw new HTTPException(403, { message: error.message });
|
|
944
|
+
if (error instanceof SessionMessageSearchCursorError)
|
|
945
|
+
throw new HTTPException(400, { message: error.message });
|
|
946
|
+
// Clients are told to cancel superseded searches, so an abort here is a
|
|
947
|
+
// routine client disconnect, never a server failure. Match the existing
|
|
948
|
+
// client-gone convention (mcp/request-abort.ts): 499 with an empty body,
|
|
949
|
+
// which a departed client never reads and monitors never count as 5xx.
|
|
950
|
+
if (c.req.raw.signal.aborted || (error instanceof Error && error.name === "AbortError"))
|
|
951
|
+
return new Response(null, { status: 499 });
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
|
|
684
956
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
685
957
|
const workspaceId = c.req.param("workspaceId");
|
|
686
958
|
const authorization = await requireAccessGrantAuthorization(
|
|
@@ -698,31 +970,51 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
698
970
|
}
|
|
699
971
|
const pageView = c.req.query("view") === "page";
|
|
700
972
|
const query = sessionListQuery(c.req.query(), pageView);
|
|
701
|
-
|
|
973
|
+
const projection = c.req.query("projection");
|
|
974
|
+
if (projection !== undefined && projection !== "summary")
|
|
975
|
+
throw new HTTPException(400, { message: 'projection must be "summary"' });
|
|
976
|
+
if (projection && !pageView)
|
|
977
|
+
throw new HTTPException(400, { message: 'summary projection requires view="page"' });
|
|
978
|
+
const summary = projection === "summary";
|
|
979
|
+
let page: Awaited<
|
|
980
|
+
ReturnType<typeof listSessionsForSubject | typeof listSessionEntriesForSubject>
|
|
981
|
+
>;
|
|
702
982
|
try {
|
|
703
|
-
page = await
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
983
|
+
page = await (summary ? listSessionEntriesForSubject : listSessionsForSubject)(
|
|
984
|
+
db,
|
|
985
|
+
workspaceId,
|
|
986
|
+
{
|
|
987
|
+
subjectId: grant.subjectId,
|
|
988
|
+
limit: boundedLimit(query.limit),
|
|
989
|
+
materializeSnapshot: pageView,
|
|
990
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
991
|
+
...(query.search ? { search: query.search } : {}),
|
|
992
|
+
...(query.pinsOnly ? { pinsOnly: true } : {}),
|
|
993
|
+
...(query.includeTotals ? { includeTotals: true } : {}),
|
|
994
|
+
...(query.needsYouOnly ? { needsYouOnly: true } : {}),
|
|
995
|
+
...(query.contentArchivedOnly ? { contentArchivedOnly: true } : {}),
|
|
996
|
+
...(query.includePinned === false ? { includePinned: false } : {}),
|
|
997
|
+
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
998
|
+
...(query.sortBy ? { sortBy: query.sortBy } : {}),
|
|
999
|
+
...(query.archiveStatus ? { archiveStatus: query.archiveStatus } : {}),
|
|
1000
|
+
...(query.parentSessionId !== undefined
|
|
1001
|
+
? { parentSessionId: query.parentSessionId }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
|
|
1004
|
+
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
1005
|
+
...(query.createdBy ? { createdBy: query.createdBy } : {}),
|
|
1006
|
+
...(query.updatedFrom ? { updatedFrom: query.updatedFrom } : {}),
|
|
1007
|
+
...(query.updatedBefore ? { updatedBefore: query.updatedBefore } : {}),
|
|
1008
|
+
...(query.createdFrom ? { createdFrom: query.createdFrom } : {}),
|
|
1009
|
+
...(query.createdBefore ? { createdBefore: query.createdBefore } : {}),
|
|
1010
|
+
...(query.scopeSubjectId ? { scopeSubjectId: query.scopeSubjectId } : {}),
|
|
1011
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
1012
|
+
// A managed human's own personal workspace has no membership row, so
|
|
1013
|
+
// the list's removal fence must fall back to the organization-membership
|
|
1014
|
+
// pointer, only with verified native or external owning-user provenance.
|
|
1015
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1016
|
+
},
|
|
1017
|
+
);
|
|
726
1018
|
} catch (error) {
|
|
727
1019
|
if (error instanceof SessionListAccessError) {
|
|
728
1020
|
throw new HTTPException(403, { message: error.message });
|
|
@@ -743,7 +1035,6 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
743
1035
|
// body for older clients while still making its older-pin omission visible
|
|
744
1036
|
// to raw HTTP consumers without changing that response shape.
|
|
745
1037
|
c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
|
|
746
|
-
const policy = await loadEffectivePolicyContext(deps, workspaceId, grant.subjectId);
|
|
747
1038
|
const commandActivity = await backgroundCommandActivityForSessions(db, {
|
|
748
1039
|
accountId: grant.accountId,
|
|
749
1040
|
workspaceId,
|
|
@@ -758,17 +1049,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
758
1049
|
[...page.pinned, ...page.sessions].map((session) => session.id),
|
|
759
1050
|
)
|
|
760
1051
|
: new Set<string>();
|
|
1052
|
+
if ("projection" in page) {
|
|
1053
|
+
const decorateEntry = (session: (typeof page.sessions)[number]) => ({
|
|
1054
|
+
...session,
|
|
1055
|
+
hasSchedules: scheduleTargets.has(session.id),
|
|
1056
|
+
...(commandActivity.has(session.id)
|
|
1057
|
+
? { backgroundCommandActivity: commandActivity.get(session.id)! }
|
|
1058
|
+
: {}),
|
|
1059
|
+
});
|
|
1060
|
+
return c.json({
|
|
1061
|
+
...page,
|
|
1062
|
+
...(query.hasPageFilters ? { filtersApplied: true as const } : {}),
|
|
1063
|
+
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
1064
|
+
pinned: page.pinned.map(decorateEntry),
|
|
1065
|
+
sessions: page.sessions.map(decorateEntry),
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId, grant.subjectId, [
|
|
1069
|
+
...page.pinned,
|
|
1070
|
+
...page.sessions,
|
|
1071
|
+
]);
|
|
761
1072
|
const decorate = (session: Session): Session => {
|
|
762
1073
|
const activity = commandActivity.get(session.id);
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
policy.workspaceServerIds,
|
|
770
|
-
policy.workspaceDefaultServerIds,
|
|
771
|
-
);
|
|
1074
|
+
const decorated = {
|
|
1075
|
+
...session,
|
|
1076
|
+
hasSchedules: scheduleTargets.has(session.id),
|
|
1077
|
+
...(activity ? { backgroundCommandActivity: activity } : {}),
|
|
1078
|
+
};
|
|
1079
|
+
return sessionResponse(decorated, policy);
|
|
772
1080
|
};
|
|
773
1081
|
if (pageView) {
|
|
774
1082
|
return c.json({
|
|
@@ -937,6 +1245,48 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
937
1245
|
}
|
|
938
1246
|
});
|
|
939
1247
|
|
|
1248
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/tool-reviews/:approvalId", async (c) => {
|
|
1249
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1250
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1251
|
+
const input = {
|
|
1252
|
+
accountId: grant.accountId,
|
|
1253
|
+
workspaceId,
|
|
1254
|
+
sessionId: c.req.param("sessionId"),
|
|
1255
|
+
approvalId: c.req.param("approvalId"),
|
|
1256
|
+
};
|
|
1257
|
+
const review = await getToolActionReview(db, input);
|
|
1258
|
+
if (!review) throw new HTTPException(404, { message: "Review not found" });
|
|
1259
|
+
c.header("Cache-Control", "private, no-store");
|
|
1260
|
+
return c.json(review);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
app.get(
|
|
1264
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/tool-reviews/:approvalId/details",
|
|
1265
|
+
async (c) => {
|
|
1266
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1267
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1268
|
+
const query = z
|
|
1269
|
+
.object({
|
|
1270
|
+
actionDigest: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1271
|
+
path: z.string().max(2048).default(""),
|
|
1272
|
+
offset: z.coerce.number().int().min(0).max(4_194_304).default(0),
|
|
1273
|
+
})
|
|
1274
|
+
.safeParse(c.req.query());
|
|
1275
|
+
if (!query.success) throw new HTTPException(400, { message: "Invalid review page" });
|
|
1276
|
+
const input = {
|
|
1277
|
+
accountId: grant.accountId,
|
|
1278
|
+
workspaceId,
|
|
1279
|
+
sessionId: c.req.param("sessionId"),
|
|
1280
|
+
approvalId: c.req.param("approvalId"),
|
|
1281
|
+
...query.data,
|
|
1282
|
+
};
|
|
1283
|
+
const page = await getToolReviewDetailsPage(db, input);
|
|
1284
|
+
if (!page) throw new HTTPException(404, { message: "Review details unavailable" });
|
|
1285
|
+
c.header("Cache-Control", "private, no-store");
|
|
1286
|
+
return c.json(page);
|
|
1287
|
+
},
|
|
1288
|
+
);
|
|
1289
|
+
|
|
940
1290
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
941
1291
|
const workspaceId = c.req.param("workspaceId");
|
|
942
1292
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -954,22 +1304,38 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
954
1304
|
if (!session) {
|
|
955
1305
|
throw new HTTPException(404, { message: "session not found" });
|
|
956
1306
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1307
|
+
// Independent reads of the already-authorized session: the effective
|
|
1308
|
+
// policy context depends only on the session row, not on the activity or
|
|
1309
|
+
// schedule annotations, so all three are read concurrently.
|
|
1310
|
+
const [activity, scheduleTargets, policy] = await Promise.all([
|
|
1311
|
+
backgroundCommandActivityForSessions(db, {
|
|
1312
|
+
accountId: grant.accountId,
|
|
1313
|
+
workspaceId,
|
|
1314
|
+
sessionIds: [sessionId],
|
|
1315
|
+
}),
|
|
963
1316
|
hasPermission(grant.permissions, "scheduled_tasks:run") &&
|
|
964
1317
|
hasPermission(grant.permissions, "sessions:control")
|
|
965
|
-
?
|
|
966
|
-
: new Set<string>()
|
|
1318
|
+
? scheduledSessionIds(db, workspaceId, [sessionId])
|
|
1319
|
+
: Promise.resolve(new Set<string>()),
|
|
1320
|
+
loadEffectivePolicyContext(deps, workspaceId, grant.subjectId, [session]),
|
|
1321
|
+
]);
|
|
1322
|
+
// the task's usage projection. A failed read is `unavailable`,
|
|
1323
|
+
// never $0 or unlimited; it does not fail the session read.
|
|
1324
|
+
const usageLimit = await sessionUsageLimitForRead(db, session, {
|
|
1325
|
+
subjectId: grant.subjectId,
|
|
1326
|
+
});
|
|
967
1327
|
return c.json(
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1328
|
+
sessionResponse(
|
|
1329
|
+
{
|
|
1330
|
+
...session,
|
|
1331
|
+
...(usageLimit ? { usageLimit } : {}),
|
|
1332
|
+
hasSchedules: scheduleTargets.has(sessionId),
|
|
1333
|
+
...(activity.get(sessionId)
|
|
1334
|
+
? { backgroundCommandActivity: activity.get(sessionId) }
|
|
1335
|
+
: {}),
|
|
1336
|
+
},
|
|
1337
|
+
policy,
|
|
1338
|
+
),
|
|
973
1339
|
);
|
|
974
1340
|
});
|
|
975
1341
|
|
|
@@ -1065,11 +1431,42 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1065
1431
|
}
|
|
1066
1432
|
});
|
|
1067
1433
|
|
|
1434
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/visibility/preview", async (c) => {
|
|
1435
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1436
|
+
const sessionId = c.req.param("sessionId");
|
|
1437
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1438
|
+
const target = SessionVisibility.safeParse(c.req.query("visibility"));
|
|
1439
|
+
if (!target.success) {
|
|
1440
|
+
throw new ApiHttpError(422, {
|
|
1441
|
+
code: "validation_failed",
|
|
1442
|
+
message: "Invalid session visibility preview request.",
|
|
1443
|
+
retryable: false,
|
|
1444
|
+
details: { fields: zodErrorFields(target.error) },
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
return c.json(
|
|
1449
|
+
await previewManagedHumanSessionVisibilityChange(
|
|
1450
|
+
deps,
|
|
1451
|
+
authorization,
|
|
1452
|
+
workspaceId,
|
|
1453
|
+
sessionId,
|
|
1454
|
+
target.data,
|
|
1455
|
+
"http",
|
|
1456
|
+
),
|
|
1457
|
+
);
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
throw sessionTenancyHttpError(error);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1068
1463
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/forks", async (c) => {
|
|
1069
1464
|
const workspaceId = c.req.param("workspaceId");
|
|
1070
1465
|
const sessionId = c.req.param("sessionId");
|
|
1071
1466
|
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
1072
|
-
const
|
|
1467
|
+
const forkLegacy = rejectLegacyVariableSetFields(c, await c.req.json().catch(() => null));
|
|
1468
|
+
if ("response" in forkLegacy) return forkLegacy.response;
|
|
1469
|
+
const parsed = ForkSessionRequest.safeParse(forkLegacy.payload);
|
|
1073
1470
|
if (!parsed.success) {
|
|
1074
1471
|
throw new ApiHttpError(422, {
|
|
1075
1472
|
code: "validation_failed",
|
|
@@ -1131,15 +1528,35 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1131
1528
|
});
|
|
1132
1529
|
}
|
|
1133
1530
|
try {
|
|
1134
|
-
|
|
1135
|
-
|
|
1531
|
+
await realtimeVoiceBilling.admit({
|
|
1532
|
+
accountId: grant.accountId,
|
|
1533
|
+
workspaceId,
|
|
1534
|
+
model: parsed.data.model,
|
|
1535
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1536
|
+
});
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
const refused = realtimeVoiceRefusalResponse(c, error);
|
|
1539
|
+
if (refused) return refused;
|
|
1540
|
+
throw error;
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) => {
|
|
1544
|
+
const accounts = await freezeSessionRealtimeConnectionAccounts({
|
|
1545
|
+
db: scopedDb as unknown as Database,
|
|
1546
|
+
settings,
|
|
1547
|
+
grant,
|
|
1548
|
+
workspaceId,
|
|
1549
|
+
sessionId,
|
|
1550
|
+
});
|
|
1551
|
+
return beginSessionRealtimeInTransaction(scopedDb, {
|
|
1136
1552
|
accountId: grant.accountId,
|
|
1137
1553
|
workspaceId,
|
|
1138
1554
|
sessionId,
|
|
1139
1555
|
ownerSubjectId: grant.subjectId,
|
|
1140
1556
|
...parsed.data,
|
|
1141
|
-
|
|
1142
|
-
|
|
1557
|
+
...accounts,
|
|
1558
|
+
});
|
|
1559
|
+
});
|
|
1143
1560
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
1144
1561
|
c.header("cache-control", "private, no-store");
|
|
1145
1562
|
return c.json({ mode: result.mode, replay: result.replay }, result.replay ? 200 : 201);
|
|
@@ -1170,6 +1587,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1170
1587
|
});
|
|
1171
1588
|
}
|
|
1172
1589
|
try {
|
|
1590
|
+
// Settle observed minutes before renewing; out of credits keeps the
|
|
1591
|
+
// current lease so the call drains and ends instead of running free.
|
|
1592
|
+
const settlement = await settleRealtimeVoice({
|
|
1593
|
+
grant,
|
|
1594
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1595
|
+
workspaceId,
|
|
1596
|
+
sessionId,
|
|
1597
|
+
realtimeId,
|
|
1598
|
+
});
|
|
1599
|
+
const stop = settlement?.stop ?? null;
|
|
1173
1600
|
const result = await withWorkspaceSessionActivityRls(db, workspaceId, async (scopedDb) =>
|
|
1174
1601
|
renewSessionRealtimeInTransaction(scopedDb, {
|
|
1175
1602
|
workspaceId,
|
|
@@ -1177,11 +1604,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1177
1604
|
realtimeId,
|
|
1178
1605
|
ownerSubjectId: grant.subjectId,
|
|
1179
1606
|
...parsed.data,
|
|
1607
|
+
extendLease: stop === null,
|
|
1180
1608
|
}),
|
|
1181
1609
|
);
|
|
1182
1610
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
1183
1611
|
c.header("cache-control", "private, no-store");
|
|
1184
|
-
return c.json({
|
|
1612
|
+
return c.json({
|
|
1613
|
+
mode: result.mode,
|
|
1614
|
+
replay: result.replay,
|
|
1615
|
+
...(stop && result.mode.state === "active"
|
|
1616
|
+
? { stop: { code: stop.code, message: stop.message } }
|
|
1617
|
+
: {}),
|
|
1618
|
+
});
|
|
1185
1619
|
} catch (error) {
|
|
1186
1620
|
throw sessionRealtimeHttpError(error);
|
|
1187
1621
|
}
|
|
@@ -1214,10 +1648,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1214
1648
|
sessionId,
|
|
1215
1649
|
realtimeId,
|
|
1216
1650
|
ownerSubjectId: grant.subjectId,
|
|
1651
|
+
ownerSubjectLabel: grant.subjectLabel ?? null,
|
|
1217
1652
|
...parsed.data,
|
|
1218
1653
|
}),
|
|
1219
1654
|
);
|
|
1220
1655
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
1656
|
+
await settleRealtimeVoice({
|
|
1657
|
+
grant,
|
|
1658
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1659
|
+
workspaceId,
|
|
1660
|
+
sessionId,
|
|
1661
|
+
realtimeId,
|
|
1662
|
+
});
|
|
1221
1663
|
c.header("cache-control", "private, no-store");
|
|
1222
1664
|
return c.json({ mode: result.mode, replay: result.replay });
|
|
1223
1665
|
} catch (error) {
|
|
@@ -1253,21 +1695,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1253
1695
|
...providerRequest
|
|
1254
1696
|
} = parsed.data;
|
|
1255
1697
|
const claim = await withWorkspaceRls(db, workspaceId, async (scopedDb) =>
|
|
1256
|
-
scopedDb.transaction(async (tx) =>
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1698
|
+
scopedDb.transaction(async (tx) => {
|
|
1699
|
+
const claimed = await claimSessionRealtimeConnectionInTransaction(
|
|
1700
|
+
tx as unknown as Database,
|
|
1701
|
+
{
|
|
1702
|
+
workspaceId,
|
|
1703
|
+
sessionId,
|
|
1704
|
+
realtimeId,
|
|
1705
|
+
operationId,
|
|
1706
|
+
ownerSubjectId: grant.subjectId,
|
|
1707
|
+
browserInstanceId,
|
|
1708
|
+
ownerKey,
|
|
1709
|
+
expectedVersion,
|
|
1710
|
+
expectedConnectionEpoch,
|
|
1711
|
+
rotate,
|
|
1712
|
+
promotionMode: browserActivation === "required" ? "staged" : "legacy",
|
|
1713
|
+
},
|
|
1714
|
+
);
|
|
1715
|
+
if (
|
|
1716
|
+
claimed.mode.model !== "gpt-live-1-boulder-alpha" &&
|
|
1717
|
+
claimed.mode.model !== AZURE_LIVE_MODEL_ID
|
|
1718
|
+
) {
|
|
1719
|
+
throw new CodexRealtimeBrokerError(
|
|
1720
|
+
"invalid_request",
|
|
1721
|
+
"This voice model does not use WebRTC negotiation",
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return claimed;
|
|
1725
|
+
}),
|
|
1271
1726
|
);
|
|
1272
1727
|
if (claim.replay) {
|
|
1273
1728
|
if (
|
|
@@ -1302,7 +1757,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1302
1757
|
return c.json({
|
|
1303
1758
|
sdp: claim.connection.sdpAnswer,
|
|
1304
1759
|
version: "v3" as const,
|
|
1305
|
-
model:
|
|
1760
|
+
model: claim.mode.model,
|
|
1306
1761
|
connectionId: claim.connection.id,
|
|
1307
1762
|
connectionEpoch: claim.connection.connectionEpoch,
|
|
1308
1763
|
startupFenceSequence: claim.connection.startupFenceSequence,
|
|
@@ -1310,13 +1765,40 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1310
1765
|
replay: true,
|
|
1311
1766
|
});
|
|
1312
1767
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1768
|
+
try {
|
|
1769
|
+
await realtimeVoiceBilling.admit({
|
|
1770
|
+
accountId: grant.accountId,
|
|
1771
|
+
workspaceId,
|
|
1772
|
+
model: claim.mode.model,
|
|
1773
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1774
|
+
});
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
const refused = realtimeVoiceRefusalResponse(c, error);
|
|
1777
|
+
if (!refused) throw error;
|
|
1778
|
+
await withWorkspaceRls(db, workspaceId, async (scopedDb) =>
|
|
1779
|
+
scopedDb.transaction(async (tx) =>
|
|
1780
|
+
failSessionRealtimeConnectionInTransaction(tx as unknown as Database, {
|
|
1781
|
+
workspaceId,
|
|
1782
|
+
sessionId,
|
|
1783
|
+
realtimeId,
|
|
1784
|
+
connectionId: claim.connection.id,
|
|
1785
|
+
operationId,
|
|
1786
|
+
connectionEpoch: claim.connection.connectionEpoch,
|
|
1787
|
+
failureCode: "billing_refused",
|
|
1788
|
+
}),
|
|
1789
|
+
),
|
|
1790
|
+
).catch(() => undefined);
|
|
1791
|
+
return refused;
|
|
1792
|
+
}
|
|
1793
|
+
const broker =
|
|
1794
|
+
claim.mode.model === AZURE_LIVE_MODEL_ID
|
|
1795
|
+
? buildSessionAzureLiveBroker(db, settings, workspaceId, sessionId, deps.codexFetch)
|
|
1796
|
+
: buildSessionCodexRealtimeBroker(
|
|
1797
|
+
db,
|
|
1798
|
+
settings,
|
|
1799
|
+
{ accountId: grant.accountId, workspaceId, sessionId },
|
|
1800
|
+
deps.codexFetch,
|
|
1801
|
+
);
|
|
1320
1802
|
try {
|
|
1321
1803
|
const answer = await broker({
|
|
1322
1804
|
request: providerRequest,
|
|
@@ -1355,6 +1837,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1355
1837
|
),
|
|
1356
1838
|
)
|
|
1357
1839
|
: null;
|
|
1840
|
+
await settleRealtimeVoice({
|
|
1841
|
+
grant,
|
|
1842
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1843
|
+
workspaceId,
|
|
1844
|
+
sessionId,
|
|
1845
|
+
realtimeId,
|
|
1846
|
+
});
|
|
1358
1847
|
return c.json({
|
|
1359
1848
|
...answer,
|
|
1360
1849
|
connectionId: completed.connection.id,
|
|
@@ -1455,6 +1944,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1455
1944
|
"Realtime Gateway tokens are single-use; reconnect with a new operation",
|
|
1456
1945
|
);
|
|
1457
1946
|
}
|
|
1947
|
+
await realtimeVoiceBilling.admit({
|
|
1948
|
+
accountId: grant.accountId,
|
|
1949
|
+
workspaceId,
|
|
1950
|
+
model: claim.mode.model,
|
|
1951
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1952
|
+
});
|
|
1458
1953
|
const secret = await createGatewayRealtimeConnectionSecret({
|
|
1459
1954
|
db,
|
|
1460
1955
|
settings,
|
|
@@ -1478,6 +1973,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1478
1973
|
),
|
|
1479
1974
|
);
|
|
1480
1975
|
connectionCompleted = true;
|
|
1976
|
+
await settleRealtimeVoice({
|
|
1977
|
+
grant,
|
|
1978
|
+
attribution: creditDebitAttributionForGrant(grant),
|
|
1979
|
+
workspaceId,
|
|
1980
|
+
sessionId,
|
|
1981
|
+
realtimeId,
|
|
1982
|
+
});
|
|
1481
1983
|
return c.json({
|
|
1482
1984
|
...secret,
|
|
1483
1985
|
connectionId: completed.connection.id,
|
|
@@ -1499,11 +2001,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1499
2001
|
operationId,
|
|
1500
2002
|
connectionEpoch: claimed.connection.connectionEpoch,
|
|
1501
2003
|
failureCode:
|
|
1502
|
-
error instanceof GatewayRealtimeBrokerError
|
|
2004
|
+
error instanceof GatewayRealtimeBrokerError
|
|
2005
|
+
? error.code
|
|
2006
|
+
: error instanceof TranscriptionBillingRefusedError ||
|
|
2007
|
+
error instanceof RealtimeVoiceUnavailableError
|
|
2008
|
+
? "billing_refused"
|
|
2009
|
+
: "gateway_error",
|
|
1503
2010
|
}),
|
|
1504
2011
|
),
|
|
1505
2012
|
).catch(() => undefined);
|
|
1506
2013
|
}
|
|
2014
|
+
const refused = realtimeVoiceRefusalResponse(c, error);
|
|
2015
|
+
if (refused) return refused;
|
|
1507
2016
|
if (error instanceof SessionRealtimeConflictError) throw sessionRealtimeHttpError(error);
|
|
1508
2017
|
if (!(error instanceof GatewayRealtimeBrokerError)) throw error;
|
|
1509
2018
|
const status = error.code === "credential_unavailable" ? 409 : 502;
|
|
@@ -1712,29 +2221,38 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1712
2221
|
});
|
|
1713
2222
|
}
|
|
1714
2223
|
try {
|
|
1715
|
-
const result = await withWorkspaceSessionActivityRls(
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
2224
|
+
const result = await withWorkspaceSessionActivityRls(
|
|
2225
|
+
db,
|
|
2226
|
+
workspaceId,
|
|
2227
|
+
async (scopedDb) => {
|
|
2228
|
+
// Acquire origin authority before inference/session locks. A deferred
|
|
2229
|
+
// realtime session has no initial worker turn; capture its exact
|
|
2230
|
+
// linked actor when the ledger actually admits ordinary agent work.
|
|
2231
|
+
await beforeCommit?.(scopedDb as unknown as Database);
|
|
2232
|
+
return syncSessionRealtimeLedgerInTransaction(
|
|
2233
|
+
scopedDb,
|
|
2234
|
+
{
|
|
2235
|
+
workspaceId,
|
|
2236
|
+
sessionId,
|
|
2237
|
+
realtimeId,
|
|
2238
|
+
ownerSubjectId: grant.subjectId,
|
|
2239
|
+
ownerSubjectLabel: grant.subjectLabel ?? null,
|
|
2240
|
+
...parsed.data,
|
|
2241
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
2242
|
+
},
|
|
2243
|
+
captureLinked
|
|
2244
|
+
? {
|
|
2245
|
+
afterDelegationAdmission: async ({ turnId }) =>
|
|
2246
|
+
captureLinked(scopedDb as unknown as Database, sessionId, turnId),
|
|
2247
|
+
}
|
|
2248
|
+
: {},
|
|
2249
|
+
);
|
|
2250
|
+
},
|
|
2251
|
+
undefined,
|
|
2252
|
+
// External reauthorization takes the organization-membership lock,
|
|
2253
|
+
// which precedes the tenancy fence and the canonical session prefix.
|
|
2254
|
+
Boolean(beforeCommit),
|
|
2255
|
+
);
|
|
1738
2256
|
await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
|
|
1739
2257
|
c.header("cache-control", "private, no-store");
|
|
1740
2258
|
return c.json({ accepted: result.accepted, outbound: result.outbound });
|
|
@@ -1858,6 +2376,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1858
2376
|
|
|
1859
2377
|
// Personal chat organization: archived roots leave the ordinary rail only
|
|
1860
2378
|
// for this member and remain recoverable through the archived list view.
|
|
2379
|
+
// Archiving a chat you started that still has work also pauses it (see
|
|
2380
|
+
// pauseOwnArchivedWork); archiving anyone else's chat only hides it for you.
|
|
1861
2381
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
|
|
1862
2382
|
const workspaceId = c.req.param("workspaceId");
|
|
1863
2383
|
const authorization = await requireAccessGrantAuthorization(
|
|
@@ -1873,14 +2393,22 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1873
2393
|
throw new HTTPException(400, { message: "invalid session archive request" });
|
|
1874
2394
|
}
|
|
1875
2395
|
try {
|
|
1876
|
-
const
|
|
2396
|
+
const archived = await setSessionArchive(db, {
|
|
1877
2397
|
workspaceId,
|
|
1878
2398
|
subjectId: grant.subjectId,
|
|
1879
2399
|
sessionId,
|
|
1880
2400
|
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
1881
2401
|
...parsed.data,
|
|
1882
2402
|
});
|
|
1883
|
-
if (!
|
|
2403
|
+
if (!archived) throw new HTTPException(404, { message: "session not found" });
|
|
2404
|
+
let session = archived;
|
|
2405
|
+
if (parsed.data.archived) {
|
|
2406
|
+
try {
|
|
2407
|
+
session = await pauseOwnArchivedWork(deps, grant, workspaceId, archived);
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
return commandConflictResponse(c, error);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
1884
2412
|
return c.json(
|
|
1885
2413
|
await withEffectivePolicy(
|
|
1886
2414
|
deps,
|
|
@@ -1911,9 +2439,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1911
2439
|
c.req.param("sessionId"),
|
|
1912
2440
|
...lineage.ancestors.map((session) => session.id),
|
|
1913
2441
|
];
|
|
2442
|
+
const lineageSessions = [...lineage.ancestors];
|
|
1914
2443
|
const collect = (nodes: LineageNode[]) => {
|
|
1915
2444
|
for (const node of nodes) {
|
|
1916
2445
|
sessionIds.push(node.session.id);
|
|
2446
|
+
lineageSessions.push(node.session as Session);
|
|
1917
2447
|
collect(node.children);
|
|
1918
2448
|
}
|
|
1919
2449
|
};
|
|
@@ -1929,23 +2459,56 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1929
2459
|
session: { ...node.session, hasSchedules: targets.has(node.session.id) },
|
|
1930
2460
|
children: decorateNodes(node.children),
|
|
1931
2461
|
}));
|
|
1932
|
-
const policy = await loadEffectivePolicyContext(
|
|
2462
|
+
const policy = await loadEffectivePolicyContext(
|
|
2463
|
+
deps,
|
|
2464
|
+
workspaceId,
|
|
2465
|
+
grant.subjectId,
|
|
2466
|
+
lineageSessions,
|
|
2467
|
+
);
|
|
1933
2468
|
return c.json({
|
|
1934
2469
|
...lineage,
|
|
1935
2470
|
sessionHasSchedules: targets.has(c.req.param("sessionId")),
|
|
1936
2471
|
ancestors: lineage.ancestors.map((session) =>
|
|
1937
|
-
|
|
1938
|
-
{ ...session, hasSchedules: targets.has(session.id) },
|
|
1939
|
-
policy.workspaceServerIds,
|
|
1940
|
-
policy.workspaceDefaultServerIds,
|
|
1941
|
-
),
|
|
2472
|
+
sessionResponse({ ...session, hasSchedules: targets.has(session.id) }, policy),
|
|
1942
2473
|
),
|
|
1943
2474
|
children: decorateNodes(mapLineageNodes(lineage.children, policy)),
|
|
1944
2475
|
});
|
|
1945
2476
|
});
|
|
1946
2477
|
|
|
2478
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-accounts", async (c) => {
|
|
2479
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2480
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
2481
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
2482
|
+
// authorizeSessionHttp has already enforced private-session and agent scope.
|
|
2483
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
2484
|
+
const projection = await getSubscriptionCoreSessionCodexAccounts(db, {
|
|
2485
|
+
accountId: grant.accountId,
|
|
2486
|
+
workspaceId,
|
|
2487
|
+
sessionId: c.req.param("sessionId"),
|
|
2488
|
+
// A turn on the viewer's own personal connection shows it to them.
|
|
2489
|
+
viewerSubjectId: grant.subjectId,
|
|
2490
|
+
});
|
|
2491
|
+
if (!projection) throw new HTTPException(404, { message: "session not found" });
|
|
2492
|
+
const activeAccountId = projection.rotation?.activeCredentialId ?? null;
|
|
2493
|
+
return c.json({
|
|
2494
|
+
accounts: projection.accounts.map((account) => codexAccountJson(account)),
|
|
2495
|
+
activeAccountId,
|
|
2496
|
+
settings: {
|
|
2497
|
+
rotationEnabled: projection.rotation?.rotationEnabled ?? false,
|
|
2498
|
+
rotationStrategy: "sharded",
|
|
2499
|
+
activeCredentialId: activeAccountId,
|
|
2500
|
+
},
|
|
2501
|
+
currentSelection: projection.currentSelection,
|
|
2502
|
+
currentAccount: projection.currentAccount
|
|
2503
|
+
? codexAccountJson(projection.currentAccount)
|
|
2504
|
+
: null,
|
|
2505
|
+
pinnedAccountId: projection.pinnedAccountId,
|
|
2506
|
+
lastAccountId: projection.lastAccountId,
|
|
2507
|
+
});
|
|
2508
|
+
});
|
|
2509
|
+
|
|
1947
2510
|
// Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
|
|
1948
|
-
// "auto" clears the pin (
|
|
2511
|
+
// "auto" clears only the Codex pin (shared-core automatic selection); a
|
|
1949
2512
|
// uuid pins the session to that specific account. Overrides a capacity-blocked
|
|
1950
2513
|
// turn; a running attempt keeps its account. 404 when the session or the target
|
|
1951
2514
|
// account id isn't in the workspace.
|
|
@@ -1964,7 +2527,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1964
2527
|
try {
|
|
1965
2528
|
await requireSessionAuthorization(deps, grant, {
|
|
1966
2529
|
sessionId,
|
|
1967
|
-
operation: "session.
|
|
2530
|
+
operation: "session.model_account.write",
|
|
1968
2531
|
surface: "http",
|
|
1969
2532
|
});
|
|
1970
2533
|
} catch (error) {
|
|
@@ -1972,42 +2535,23 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1972
2535
|
}
|
|
1973
2536
|
}
|
|
1974
2537
|
const pinned = target === "auto" ? null : target;
|
|
1975
|
-
|
|
2538
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
2539
|
+
|
|
2540
|
+
const core = await pinSubscriptionCoreSessionCodexAccount(db, {
|
|
2541
|
+
accountId: grant.accountId,
|
|
1976
2542
|
workspaceId,
|
|
1977
2543
|
sessionId,
|
|
1978
|
-
|
|
2544
|
+
connectionId: pinned,
|
|
1979
2545
|
subjectId: grant.subjectId,
|
|
1980
2546
|
});
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
throw new HTTPException(404, {
|
|
1984
|
-
message: "session or codex account not found",
|
|
1985
|
-
});
|
|
2547
|
+
if (!core.result.changed) {
|
|
2548
|
+
throw new HTTPException(404, { message: "session or codex account not found" });
|
|
1986
2549
|
}
|
|
1987
|
-
await
|
|
1988
|
-
|
|
1989
|
-
workflowClient.signalCodexCapacity
|
|
1990
|
-
? workflowClient.signalCodexCapacity({
|
|
1991
|
-
accountId: wake.accountId,
|
|
1992
|
-
workspaceId: wake.workspaceId,
|
|
1993
|
-
sessionId: wake.sessionId,
|
|
1994
|
-
workflowId: wake.workflowId,
|
|
1995
|
-
wakeRevision: wake.wakeRevision,
|
|
1996
|
-
workflowWakeRevision: wake.workflowWakeRevision,
|
|
1997
|
-
})
|
|
1998
|
-
: workflowClient.wakeSessionWorkflow({
|
|
1999
|
-
accountId: wake.accountId,
|
|
2000
|
-
workspaceId: wake.workspaceId,
|
|
2001
|
-
sessionId: wake.sessionId,
|
|
2002
|
-
workflowId: wake.workflowId,
|
|
2003
|
-
wakeRevision: wake.workflowWakeRevision,
|
|
2004
|
-
}),
|
|
2005
|
-
),
|
|
2006
|
-
);
|
|
2007
|
-
await publishDurableSessionEvents(bus, workspaceId, sessionId, mutation.result.events);
|
|
2550
|
+
await deliverSubscriptionCoreCodexWake(db, core.wake);
|
|
2551
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, core.result.events);
|
|
2008
2552
|
return c.json({
|
|
2009
2553
|
pinned: target === "auto" ? "auto" : target,
|
|
2010
|
-
appliedTo:
|
|
2554
|
+
appliedTo: core.result.appliedTo,
|
|
2011
2555
|
});
|
|
2012
2556
|
});
|
|
2013
2557
|
|
|
@@ -2024,7 +2568,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2024
2568
|
operation: "session.control",
|
|
2025
2569
|
surface: "http",
|
|
2026
2570
|
});
|
|
2027
|
-
const payload =
|
|
2571
|
+
const payload = await parseRequestJson(c, UpdateSessionChannelRequest);
|
|
2028
2572
|
try {
|
|
2029
2573
|
const updated = await setSessionChannel(db, {
|
|
2030
2574
|
workspaceId,
|
|
@@ -2053,89 +2597,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2053
2597
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2054
2598
|
});
|
|
2055
2599
|
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
// cold rotation before the new environment can be materialized.
|
|
2600
|
+
// Variable Sets were replaced by vaults. The route answers 410 so old
|
|
2601
|
+
// clients learn where secrets moved instead of silently doing nothing.
|
|
2059
2602
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/variable-sets", async (c) => {
|
|
2060
2603
|
const workspaceId = c.req.param("workspaceId");
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
try {
|
|
2064
|
-
await requireSessionAuthorization(deps, grant, {
|
|
2065
|
-
sessionId,
|
|
2066
|
-
operation: "session.variable_sets.write",
|
|
2067
|
-
surface: "http",
|
|
2068
|
-
});
|
|
2069
|
-
} catch (error) {
|
|
2070
|
-
throw sessionAuthorizationHttpError(error);
|
|
2071
|
-
}
|
|
2072
|
-
// Detach still requires attach authority: it changes which protected
|
|
2073
|
-
// resources the session may materialize. Non-empty selections additionally
|
|
2074
|
-
// require use authority through validateVariableSetAttachment.
|
|
2075
|
-
requirePermission(grant, "variable-sets:attach");
|
|
2076
|
-
const parsed = UpdateSessionVariableSetsRequest.safeParse(await c.req.json().catch(() => null));
|
|
2077
|
-
if (!parsed.success) {
|
|
2078
|
-
throw new ApiHttpError(422, {
|
|
2079
|
-
code: "validation_failed",
|
|
2080
|
-
message: "Invalid session Variable Set request.",
|
|
2081
|
-
retryable: false,
|
|
2082
|
-
details: { fields: zodErrorFields(parsed.error) },
|
|
2083
|
-
});
|
|
2084
|
-
}
|
|
2085
|
-
const variableSets: VariableSet[] = [];
|
|
2086
|
-
for (const variableSetId of parsed.data.variableSetIds) {
|
|
2087
|
-
variableSets.push(
|
|
2088
|
-
await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
|
|
2089
|
-
);
|
|
2090
|
-
}
|
|
2091
|
-
const result = await updateSessionVariableSets(db, {
|
|
2092
|
-
accountId: grant.accountId,
|
|
2093
|
-
workspaceId,
|
|
2094
|
-
sessionId,
|
|
2095
|
-
subjectId: grant.subjectId,
|
|
2096
|
-
variableSets: variableSets.map((variableSet) => ({
|
|
2097
|
-
id: variableSet.id,
|
|
2098
|
-
name: variableSet.name,
|
|
2099
|
-
scope: variableSet.scope,
|
|
2100
|
-
})),
|
|
2101
|
-
});
|
|
2102
|
-
if (result.status === "not_found") {
|
|
2103
|
-
throw new HTTPException(404, { message: "session not found" });
|
|
2104
|
-
}
|
|
2105
|
-
if (result.status === "invalid_variable_sets") {
|
|
2106
|
-
throw new ApiHttpError(422, {
|
|
2107
|
-
code: "validation_failed",
|
|
2108
|
-
message: "One or more selected Variable Sets are no longer available.",
|
|
2109
|
-
retryable: false,
|
|
2110
|
-
outcomeUnknown: false,
|
|
2111
|
-
details: { variableSetIds: result.variableSetIds },
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
if (result.status === "blocked") {
|
|
2115
|
-
const messages = {
|
|
2116
|
-
turn_in_flight:
|
|
2117
|
-
"Variable Sets can be changed only when the session has no accepted, queued, claimed, or pending work.",
|
|
2118
|
-
shared_sandbox_group:
|
|
2119
|
-
"Variable Sets cannot be changed while this session shares a sandbox; fork it into a separate session first.",
|
|
2120
|
-
live_sandbox_holders:
|
|
2121
|
-
"Close active terminal, desktop, and sandbox operations before changing Variable Sets.",
|
|
2122
|
-
} as const;
|
|
2123
|
-
throw new HTTPException(409, { message: messages[result.reason] });
|
|
2124
|
-
}
|
|
2125
|
-
if (result.status === "updated") {
|
|
2126
|
-
await publishDurableSessionEvents(bus, workspaceId, sessionId, [result.event]);
|
|
2127
|
-
}
|
|
2128
|
-
const session = await getSessionForSubject(
|
|
2129
|
-
db,
|
|
2130
|
-
workspaceId,
|
|
2131
|
-
sessionId,
|
|
2132
|
-
grant.subjectId,
|
|
2133
|
-
relatedSessionAccessFor(c),
|
|
2134
|
-
);
|
|
2135
|
-
if (!session) {
|
|
2136
|
-
throw new HTTPException(404, { message: "session not found" });
|
|
2137
|
-
}
|
|
2138
|
-
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2604
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2605
|
+
return variableSetsRemovedResponse(c, 410);
|
|
2139
2606
|
});
|
|
2140
2607
|
|
|
2141
2608
|
// Manual rename. A user-set title is permanent: the db write is
|
|
@@ -2146,7 +2613,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2146
2613
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2147
2614
|
const sessionId = c.req.param("sessionId");
|
|
2148
2615
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2149
|
-
const payload =
|
|
2616
|
+
const payload = await parseRequestJson(c, UpdateSessionRequest);
|
|
2150
2617
|
const titleUpdate = await updateSessionTitle(deps, grant, sessionId, payload.title, "user");
|
|
2151
2618
|
// A session-returning member route must preserve the caller's private pin
|
|
2152
2619
|
// projection. Returning the generic mapSession() default here would reset a
|
|
@@ -2164,6 +2631,89 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2164
2631
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2165
2632
|
});
|
|
2166
2633
|
|
|
2634
|
+
// Workspace-wide keep-live exemption from the idle-session archive. Distinct
|
|
2635
|
+
// from the personal rail archive above; archived sessions are read-only.
|
|
2636
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/retention", async (c) => {
|
|
2637
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2638
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2639
|
+
const sessionId = c.req.param("sessionId");
|
|
2640
|
+
const payload = await parseRequestJson(c, UpdateSessionRetentionRequest);
|
|
2641
|
+
const { result, relatedSessionAccess } = await setSessionRetention(
|
|
2642
|
+
deps,
|
|
2643
|
+
grant,
|
|
2644
|
+
sessionId,
|
|
2645
|
+
payload.keepLive,
|
|
2646
|
+
);
|
|
2647
|
+
if (result.status === "not_found") {
|
|
2648
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
2649
|
+
}
|
|
2650
|
+
if (result.status === "archived") {
|
|
2651
|
+
return c.json(
|
|
2652
|
+
{
|
|
2653
|
+
code: "SESSION_ARCHIVED_READ_ONLY",
|
|
2654
|
+
message:
|
|
2655
|
+
"This session is already archived, so its keep-live setting can no longer change.",
|
|
2656
|
+
},
|
|
2657
|
+
409,
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
const session = await getSessionForSubject(
|
|
2661
|
+
db,
|
|
2662
|
+
workspaceId,
|
|
2663
|
+
sessionId,
|
|
2664
|
+
grant.subjectId,
|
|
2665
|
+
relatedSessionAccess,
|
|
2666
|
+
);
|
|
2667
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
2668
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
// Per-task usage limit. Reads follow session read access; a child
|
|
2672
|
+
// returns its root's shared state. Writes are compare-and-set on the ROOT
|
|
2673
|
+
// by a person in the app or the host's server key only, never an agent
|
|
2674
|
+
// (including one acting for a person), and never wake or resume work.
|
|
2675
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/usage-limit", async (c) => {
|
|
2676
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2677
|
+
const sessionId = c.req.param("sessionId");
|
|
2678
|
+
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
2679
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
2680
|
+
}
|
|
2681
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
2682
|
+
c,
|
|
2683
|
+
deps,
|
|
2684
|
+
workspaceId,
|
|
2685
|
+
"sessions:read",
|
|
2686
|
+
);
|
|
2687
|
+
return c.json(await getSessionUsageLimitForAccess(deps, authorization, sessionId));
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2690
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/usage-limit", async (c) => {
|
|
2691
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2692
|
+
const sessionId = c.req.param("sessionId");
|
|
2693
|
+
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
2694
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
2695
|
+
}
|
|
2696
|
+
// Checked first: an agent acting for a person (connected agent, admin
|
|
2697
|
+
// access, personal API key) is refused whatever access it holds.
|
|
2698
|
+
if (isAgentActingAsPerson(c)) {
|
|
2699
|
+
throw new SessionUsageLimitHttpError(
|
|
2700
|
+
403,
|
|
2701
|
+
"session_usage_limit_forbidden",
|
|
2702
|
+
"Only a person or the application's server can change a task usage limit.",
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
2706
|
+
c,
|
|
2707
|
+
deps,
|
|
2708
|
+
workspaceId,
|
|
2709
|
+
"sessions:control",
|
|
2710
|
+
);
|
|
2711
|
+
const payload = await parseRequestJson(c, SetSessionUsageLimitRequest);
|
|
2712
|
+
return c.json(
|
|
2713
|
+
await setSessionUsageLimitForAccess(deps, authorization, sessionId, payload, c.req.raw),
|
|
2714
|
+
);
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2167
2717
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
2168
2718
|
const workspaceId = c.req.param("workspaceId");
|
|
2169
2719
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
@@ -2179,30 +2729,55 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2179
2729
|
case "not_found":
|
|
2180
2730
|
throw new HTTPException(404, { message: "session not found" });
|
|
2181
2731
|
case "not_root":
|
|
2182
|
-
throw new
|
|
2183
|
-
|
|
2732
|
+
throw new ApiHttpError(409, {
|
|
2733
|
+
code: "conflict",
|
|
2734
|
+
message: "Delete the chat this one belongs to; sub-chats are removed with it.",
|
|
2735
|
+
retryable: false,
|
|
2736
|
+
outcomeUnknown: false,
|
|
2737
|
+
details: { code: "session_delete_not_root" },
|
|
2184
2738
|
});
|
|
2185
2739
|
case "active_sessions":
|
|
2186
|
-
throw new
|
|
2187
|
-
|
|
2740
|
+
throw new ApiHttpError(409, {
|
|
2741
|
+
code: "conflict",
|
|
2742
|
+
message:
|
|
2743
|
+
"This chat is still running. Stop it, then delete it once its current turn has finished.",
|
|
2744
|
+
retryable: true,
|
|
2745
|
+
outcomeUnknown: false,
|
|
2746
|
+
details: { code: "session_delete_active_sessions" },
|
|
2188
2747
|
});
|
|
2189
2748
|
case "active_video_generations":
|
|
2190
|
-
throw new
|
|
2191
|
-
|
|
2749
|
+
throw new ApiHttpError(409, {
|
|
2750
|
+
code: "conflict",
|
|
2751
|
+
message: "Wait for this chat's video generations to finish before deleting it.",
|
|
2752
|
+
retryable: false,
|
|
2753
|
+
outcomeUnknown: false,
|
|
2754
|
+
details: { code: "session_delete_active_video_generations" },
|
|
2192
2755
|
});
|
|
2193
2756
|
case "active_background_commands":
|
|
2194
|
-
throw new
|
|
2195
|
-
|
|
2757
|
+
throw new ApiHttpError(409, {
|
|
2758
|
+
code: "conflict",
|
|
2759
|
+
message: "Stop this chat's background commands before deleting it.",
|
|
2760
|
+
retryable: false,
|
|
2761
|
+
outcomeUnknown: false,
|
|
2762
|
+
details: { code: "session_delete_active_background_commands" },
|
|
2196
2763
|
});
|
|
2197
2764
|
case "live_sandboxes":
|
|
2198
|
-
throw new
|
|
2765
|
+
throw new ApiHttpError(409, {
|
|
2766
|
+
code: "conflict",
|
|
2199
2767
|
message:
|
|
2200
|
-
"
|
|
2768
|
+
"This chat's computer is still shutting down. Try deleting it again in a moment.",
|
|
2769
|
+
retryable: true,
|
|
2770
|
+
outcomeUnknown: false,
|
|
2771
|
+
details: { code: "session_delete_live_sandboxes" },
|
|
2201
2772
|
});
|
|
2202
2773
|
case "externally_referenced":
|
|
2203
|
-
throw new
|
|
2774
|
+
throw new ApiHttpError(409, {
|
|
2775
|
+
code: "conflict",
|
|
2204
2776
|
message:
|
|
2205
|
-
"
|
|
2777
|
+
"This chat has saved workspace outputs or forks that depend on it. Archive it instead.",
|
|
2778
|
+
retryable: false,
|
|
2779
|
+
outcomeUnknown: false,
|
|
2780
|
+
details: { code: "session_delete_externally_referenced" },
|
|
2206
2781
|
});
|
|
2207
2782
|
}
|
|
2208
2783
|
});
|
|
@@ -2239,7 +2814,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2239
2814
|
const workspaceId = c.req.param("workspaceId");
|
|
2240
2815
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2241
2816
|
const sessionId = c.req.param("sessionId");
|
|
2242
|
-
const payload =
|
|
2817
|
+
const payload = parseRequestBody(
|
|
2818
|
+
UpdateSessionToolPolicyRequest,
|
|
2819
|
+
await c.req.json().catch(() => null),
|
|
2820
|
+
);
|
|
2243
2821
|
try {
|
|
2244
2822
|
const session = await updateSessionToolPolicy(deps, grant, sessionId, payload);
|
|
2245
2823
|
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
@@ -2258,6 +2836,63 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2258
2836
|
}
|
|
2259
2837
|
});
|
|
2260
2838
|
|
|
2839
|
+
// Replace the session's agent configuration (capabilities, identity,
|
|
2840
|
+
// instructions alias, renderer). Shares the tool-policy version CAS and
|
|
2841
|
+
// applies from the next attempt.
|
|
2842
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/agent", async (c) => {
|
|
2843
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2844
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2845
|
+
const sessionId = c.req.param("sessionId");
|
|
2846
|
+
const payload = parseRequestBody(
|
|
2847
|
+
UpdateSessionAgentRequest,
|
|
2848
|
+
await c.req.json().catch(() => null),
|
|
2849
|
+
);
|
|
2850
|
+
try {
|
|
2851
|
+
const session = await updateSessionAgent(deps, grant, sessionId, payload);
|
|
2852
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
if (error instanceof SessionToolPolicyVersionConflictError) {
|
|
2855
|
+
return c.json(
|
|
2856
|
+
{
|
|
2857
|
+
code: error.code,
|
|
2858
|
+
message: error.message,
|
|
2859
|
+
currentVersion: error.currentVersion,
|
|
2860
|
+
},
|
|
2861
|
+
409,
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
throw error;
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
// Replace the Skills the session carries itself. Shares the tool-policy
|
|
2869
|
+
// version CAS and applies from the next attempt.
|
|
2870
|
+
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/skills", async (c) => {
|
|
2871
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2872
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2873
|
+
const sessionId = c.req.param("sessionId");
|
|
2874
|
+
const payload = parseRequestBody(
|
|
2875
|
+
UpdateSessionSkillsRequest,
|
|
2876
|
+
await c.req.json().catch(() => null),
|
|
2877
|
+
);
|
|
2878
|
+
try {
|
|
2879
|
+
const session = await updateSessionSkills(deps, grant, sessionId, payload);
|
|
2880
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
if (error instanceof SessionToolPolicyVersionConflictError) {
|
|
2883
|
+
return c.json(
|
|
2884
|
+
{
|
|
2885
|
+
code: error.code,
|
|
2886
|
+
message: error.message,
|
|
2887
|
+
currentVersion: error.currentVersion,
|
|
2888
|
+
},
|
|
2889
|
+
409,
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
throw error;
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
|
|
2261
2896
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
2262
2897
|
const workspaceId = c.req.param("workspaceId");
|
|
2263
2898
|
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -2265,6 +2900,12 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2265
2900
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2266
2901
|
const goal = await getSessionGoalWithContinuation(db, workspaceId, sessionId);
|
|
2267
2902
|
if (!goal) {
|
|
2903
|
+
// `?absent=null` opts in to a successful `null` for a goal-less session,
|
|
2904
|
+
// so a browser polling a fresh chat logs no failed request. Without it
|
|
2905
|
+
// the original 404 contract is unchanged for existing clients.
|
|
2906
|
+
if (c.req.query("absent") === "null") {
|
|
2907
|
+
return c.json(null);
|
|
2908
|
+
}
|
|
2268
2909
|
throw new HTTPException(404, { message: "session goal not found" });
|
|
2269
2910
|
}
|
|
2270
2911
|
return c.json(goal);
|
|
@@ -2313,7 +2954,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2313
2954
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2314
2955
|
const sessionId = c.req.param("sessionId");
|
|
2315
2956
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2316
|
-
const payload =
|
|
2957
|
+
const payload = await parseRequestJson(c, ApplySessionGoalRevisionRequest);
|
|
2317
2958
|
const revision = await getSessionGoalRevision(
|
|
2318
2959
|
db,
|
|
2319
2960
|
workspaceId,
|
|
@@ -2377,7 +3018,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2377
3018
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2378
3019
|
const sessionId = c.req.param("sessionId");
|
|
2379
3020
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2380
|
-
const payload =
|
|
3021
|
+
const payload = await parseRequestJson(c, RejectSessionGoalRevisionRequest);
|
|
2381
3022
|
try {
|
|
2382
3023
|
const result = await rejectSessionGoalRevisionWithEvent(db, {
|
|
2383
3024
|
accountId: grant.accountId,
|
|
@@ -2405,7 +3046,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2405
3046
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2406
3047
|
const sessionId = c.req.param("sessionId");
|
|
2407
3048
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2408
|
-
const payload =
|
|
3049
|
+
const payload = await parseRequestJson(c, RollbackSessionGoalRevisionRequest);
|
|
2409
3050
|
const revision = await getSessionGoalRevision(
|
|
2410
3051
|
db,
|
|
2411
3052
|
workspaceId,
|
|
@@ -2457,7 +3098,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2457
3098
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2458
3099
|
const sessionId = c.req.param("sessionId");
|
|
2459
3100
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2460
|
-
const payload =
|
|
3101
|
+
const payload = await parseRequestJson(c, UpdateSessionGoalRequest);
|
|
2461
3102
|
const existing = await getSessionGoal(db, workspaceId, sessionId);
|
|
2462
3103
|
if (!existing) {
|
|
2463
3104
|
throw new HTTPException(404, { message: "session goal not found" });
|
|
@@ -2542,6 +3183,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2542
3183
|
sessionId,
|
|
2543
3184
|
{
|
|
2544
3185
|
status: "active",
|
|
3186
|
+
beforeResume: (tx, session, causalTurn) =>
|
|
3187
|
+
assertGoalResumeAllowed({ ...deps, db: tx }, session, causalTurn).catch(
|
|
3188
|
+
(error: unknown) => {
|
|
3189
|
+
if (error instanceof GoalResumeBlockedError) {
|
|
3190
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
3191
|
+
}
|
|
3192
|
+
throw error;
|
|
3193
|
+
},
|
|
3194
|
+
),
|
|
2545
3195
|
event: { type: "goal.resumed", actor: "api" },
|
|
2546
3196
|
},
|
|
2547
3197
|
);
|
|
@@ -2569,10 +3219,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2569
3219
|
|
|
2570
3220
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
2571
3221
|
const workspaceId = c.req.param("workspaceId");
|
|
2572
|
-
await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
3222
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2573
3223
|
const sessionId = c.req.param("sessionId");
|
|
2574
3224
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2575
|
-
const { event } = await clearSessionGoal(db, workspaceId, sessionId
|
|
3225
|
+
const { event } = await clearSessionGoal(db, workspaceId, sessionId, {
|
|
3226
|
+
actor: grant.principalKind === "agent_attempt" ? "agent" : "api",
|
|
3227
|
+
}).catch((error: unknown) => {
|
|
3228
|
+
if (error instanceof SessionControlConflictError) {
|
|
3229
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
3230
|
+
}
|
|
3231
|
+
throw error;
|
|
3232
|
+
});
|
|
2576
3233
|
if (event) {
|
|
2577
3234
|
try {
|
|
2578
3235
|
await bus.publish(workspaceId, sessionId, [event]);
|
|
@@ -2635,7 +3292,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2635
3292
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2636
3293
|
const sessionId = c.req.param("sessionId");
|
|
2637
3294
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2638
|
-
CompactSessionContextRequest
|
|
3295
|
+
parseRequestBody(CompactSessionContextRequest, (await c.req.json().catch(() => ({}))) ?? {});
|
|
2639
3296
|
// /compact sets one durable request. The worker clears it only in the same
|
|
2640
3297
|
// fenced transaction that installs replacement history, so failed or stale
|
|
2641
3298
|
// attempts cannot lose the request.
|
|
@@ -2822,9 +3479,93 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2822
3479
|
direction === "before" ? "X-OpenGeni-Next-Before" : "X-OpenGeni-Next-After",
|
|
2823
3480
|
String(page.nextSequence),
|
|
2824
3481
|
);
|
|
2825
|
-
}
|
|
2826
|
-
return c.json(page.events);
|
|
2827
|
-
});
|
|
3482
|
+
}
|
|
3483
|
+
return c.json(page.events);
|
|
3484
|
+
});
|
|
3485
|
+
|
|
3486
|
+
// Selected search hit only: never use the audit event projection here, even
|
|
3487
|
+
// in summary mode (it can contain modelContext or truncate visible text).
|
|
3488
|
+
app.get(
|
|
3489
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/events/:eventId/message-preview",
|
|
3490
|
+
async (c) => {
|
|
3491
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3492
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
3493
|
+
const sessionId = c.req.param("sessionId");
|
|
3494
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
3495
|
+
const eventId = c.req.param("eventId");
|
|
3496
|
+
const rawSequence = c.req.query("sequence");
|
|
3497
|
+
const sequence = Number(rawSequence);
|
|
3498
|
+
if (
|
|
3499
|
+
!z.string().uuid().safeParse(eventId).success ||
|
|
3500
|
+
rawSequence === undefined ||
|
|
3501
|
+
!/^\d+$/.test(rawSequence) ||
|
|
3502
|
+
!Number.isSafeInteger(sequence) ||
|
|
3503
|
+
sequence < 1 ||
|
|
3504
|
+
sequence > 2_147_483_647
|
|
3505
|
+
)
|
|
3506
|
+
throw new HTTPException(400, { message: "Invalid message preview reference" });
|
|
3507
|
+
|
|
3508
|
+
const notFound = () => new HTTPException(404, { message: "message not found" });
|
|
3509
|
+
const maxUnits = 12_000;
|
|
3510
|
+
const eventTable = sessionPreviewSchema.sessionEvents;
|
|
3511
|
+
// The slice helper supports structured result views too. Check only the
|
|
3512
|
+
// JSON scalar *kind* before calling it; never fetch a structured text value.
|
|
3513
|
+
const candidates = await withWorkspaceRls(db, workspaceId, (tx) =>
|
|
3514
|
+
tx
|
|
3515
|
+
.select({ kind: sql<string | null>`jsonb_typeof(${eventTable.payload}->'text')` })
|
|
3516
|
+
.from(eventTable)
|
|
3517
|
+
.where(
|
|
3518
|
+
and(
|
|
3519
|
+
eq(eventTable.workspaceId, workspaceId),
|
|
3520
|
+
eq(eventTable.sessionId, sessionId),
|
|
3521
|
+
eq(eventTable.id, eventId),
|
|
3522
|
+
eq(eventTable.sequence, sequence),
|
|
3523
|
+
sql`${eventTable.type} in ('user.message', 'agent.message.completed')`,
|
|
3524
|
+
),
|
|
3525
|
+
)
|
|
3526
|
+
.limit(1),
|
|
3527
|
+
);
|
|
3528
|
+
if (candidates[0]?.kind !== "string") throw notFound();
|
|
3529
|
+
const read = (offset: number) =>
|
|
3530
|
+
listSessionEventSlices(db, workspaceId, sessionId, {
|
|
3531
|
+
sourceSequence: sequence,
|
|
3532
|
+
sourceOffset: offset,
|
|
3533
|
+
after: sequence - 1,
|
|
3534
|
+
before: sequence + 1,
|
|
3535
|
+
includeTypes: ["user.message", "agent.message.completed"],
|
|
3536
|
+
view: "conversation",
|
|
3537
|
+
});
|
|
3538
|
+
const first = await read(0);
|
|
3539
|
+
const event = first.events[0];
|
|
3540
|
+
const slice = first.slices?.[sequence];
|
|
3541
|
+
// Includes the slice reader's duplicate, late and unclaimed-prompt gates.
|
|
3542
|
+
if (!event || event.id !== eventId || event.sequence !== sequence || !slice || slice.omitted)
|
|
3543
|
+
throw notFound();
|
|
3544
|
+
if (slice.total > maxUnits) return c.json({ status: "unavailable" as const });
|
|
3545
|
+
|
|
3546
|
+
let text = slice.text;
|
|
3547
|
+
let offset = slice.unit === "utf16" ? slice.text.length : Array.from(slice.text).length;
|
|
3548
|
+
if (text.length > maxUnits) return c.json({ status: "unavailable" as const });
|
|
3549
|
+
while (offset < slice.total) {
|
|
3550
|
+
if (offset === 0) throw notFound();
|
|
3551
|
+
const next = await read(offset);
|
|
3552
|
+
const part = next.slices?.[sequence];
|
|
3553
|
+
if (
|
|
3554
|
+
next.events[0]?.id !== eventId ||
|
|
3555
|
+
!part ||
|
|
3556
|
+
part.omitted ||
|
|
3557
|
+
part.unit !== slice.unit ||
|
|
3558
|
+
part.total !== slice.total ||
|
|
3559
|
+
part.offset !== offset
|
|
3560
|
+
)
|
|
3561
|
+
throw notFound();
|
|
3562
|
+
text += part.text;
|
|
3563
|
+
offset += slice.unit === "utf16" ? part.text.length : Array.from(part.text).length;
|
|
3564
|
+
if (text.length > maxUnits) return c.json({ status: "unavailable" as const });
|
|
3565
|
+
}
|
|
3566
|
+
return c.json({ status: "available" as const, text });
|
|
3567
|
+
},
|
|
3568
|
+
);
|
|
2828
3569
|
|
|
2829
3570
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events/stream", async (c) => {
|
|
2830
3571
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -2919,7 +3660,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2919
3660
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2920
3661
|
const sessionId = c.req.param("sessionId");
|
|
2921
3662
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2922
|
-
const payload =
|
|
3663
|
+
const payload = await parseRequestJson(c, MoveSessionQueueItemRequest);
|
|
2923
3664
|
try {
|
|
2924
3665
|
const response = await moveHumanQueuePrompt(
|
|
2925
3666
|
deps,
|
|
@@ -2946,7 +3687,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2946
3687
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2947
3688
|
const sessionId = c.req.param("sessionId");
|
|
2948
3689
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2949
|
-
const payload =
|
|
3690
|
+
const payload = await parseRequestJson(c, EditSessionQueueItemRequest);
|
|
2950
3691
|
try {
|
|
2951
3692
|
const response = await editHumanQueuePrompt(
|
|
2952
3693
|
deps,
|
|
@@ -2973,7 +3714,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2973
3714
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2974
3715
|
const sessionId = c.req.param("sessionId");
|
|
2975
3716
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2976
|
-
const payload =
|
|
3717
|
+
const payload = await parseRequestJson(c, SteerSessionQueueItemRequest);
|
|
2977
3718
|
try {
|
|
2978
3719
|
const response = await steerHumanQueuePrompt(
|
|
2979
3720
|
deps,
|
|
@@ -3000,7 +3741,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3000
3741
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
3001
3742
|
const sessionId = c.req.param("sessionId");
|
|
3002
3743
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
3003
|
-
const payload =
|
|
3744
|
+
const payload = await parseRequestJson(c, DeleteSessionQueueItemRequest);
|
|
3004
3745
|
try {
|
|
3005
3746
|
const response = await deleteHumanQueuePrompt(
|
|
3006
3747
|
deps,
|
|
@@ -3040,7 +3781,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3040
3781
|
const workspaceId = c.req.param("workspaceId");
|
|
3041
3782
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
3042
3783
|
const sessionId = c.req.param("sessionId");
|
|
3043
|
-
const payload =
|
|
3784
|
+
const payload = await parseRequestJson(c, SaveComposerDraftRequest);
|
|
3044
3785
|
try {
|
|
3045
3786
|
return c.json(
|
|
3046
3787
|
await saveHumanComposerDraft(
|
|
@@ -3059,6 +3800,113 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3059
3800
|
}
|
|
3060
3801
|
});
|
|
3061
3802
|
|
|
3803
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
|
|
3804
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3805
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
3806
|
+
try {
|
|
3807
|
+
return c.json(
|
|
3808
|
+
await getManagedHumanSandboxRecovery(
|
|
3809
|
+
deps,
|
|
3810
|
+
authorization,
|
|
3811
|
+
workspaceId,
|
|
3812
|
+
c.req.param("sessionId"),
|
|
3813
|
+
),
|
|
3814
|
+
);
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
throw sessionTenancyHttpError(error);
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3819
|
+
|
|
3820
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
|
|
3821
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3822
|
+
const sessionId = c.req.param("sessionId");
|
|
3823
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
3824
|
+
const parsed = SandboxRecoveryRequest.safeParse(await c.req.json().catch(() => null));
|
|
3825
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid checkpoint consent" });
|
|
3826
|
+
let consentCommitted = false;
|
|
3827
|
+
try {
|
|
3828
|
+
const receipt = await consentManagedHumanSandboxRecovery(
|
|
3829
|
+
deps,
|
|
3830
|
+
authorization,
|
|
3831
|
+
workspaceId,
|
|
3832
|
+
sessionId,
|
|
3833
|
+
parsed.data,
|
|
3834
|
+
);
|
|
3835
|
+
consentCommitted = true;
|
|
3836
|
+
const projection = await getManagedHumanSandboxRecovery(
|
|
3837
|
+
deps,
|
|
3838
|
+
authorization,
|
|
3839
|
+
workspaceId,
|
|
3840
|
+
sessionId,
|
|
3841
|
+
);
|
|
3842
|
+
if (
|
|
3843
|
+
projection.operationId === receipt.operationId &&
|
|
3844
|
+
projection.status === "consent_accepted"
|
|
3845
|
+
) {
|
|
3846
|
+
// Establish/verify only. No failed-turn Retry, Send, tool callback or
|
|
3847
|
+
// command replay. The ordinary cold->warming election owns creation.
|
|
3848
|
+
const session = await getSession(db, workspaceId, sessionId);
|
|
3849
|
+
if (session) {
|
|
3850
|
+
try {
|
|
3851
|
+
await withChannelARead(
|
|
3852
|
+
channelAServices,
|
|
3853
|
+
{
|
|
3854
|
+
accountId: authorization.grant.accountId,
|
|
3855
|
+
workspaceId,
|
|
3856
|
+
session,
|
|
3857
|
+
subjectId: authorization.grant.subjectId,
|
|
3858
|
+
grant: authorization.grant,
|
|
3859
|
+
},
|
|
3860
|
+
async () => undefined,
|
|
3861
|
+
);
|
|
3862
|
+
} catch {
|
|
3863
|
+
// Consent committed. Never turn a provider failure into a false
|
|
3864
|
+
// rejection of consent or leak native provider bindings/messages.
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
return c.json({
|
|
3869
|
+
...receipt,
|
|
3870
|
+
recovery: await getManagedHumanSandboxRecovery(deps, authorization, workspaceId, sessionId),
|
|
3871
|
+
});
|
|
3872
|
+
} catch (error) {
|
|
3873
|
+
if (consentCommitted) {
|
|
3874
|
+
// A later authorization/read failure cannot revoke the committed
|
|
3875
|
+
// receipt. Return no session state, checkpoint or provider details.
|
|
3876
|
+
return c.json(
|
|
3877
|
+
{
|
|
3878
|
+
code: "upstream_unavailable",
|
|
3879
|
+
message:
|
|
3880
|
+
"Recovery status could not be confirmed. Check status without resubmitting consent.",
|
|
3881
|
+
outcomeUnknown: true,
|
|
3882
|
+
retryable: false,
|
|
3883
|
+
},
|
|
3884
|
+
503,
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
if (error instanceof SandboxRecoveryConflictError)
|
|
3888
|
+
return c.json({ code: error.code, message: error.message }, 409);
|
|
3889
|
+
if (error instanceof SessionCommandIdempotencyError) return commandConflictResponse(c, error);
|
|
3890
|
+
throw sessionTenancyHttpError(error);
|
|
3891
|
+
}
|
|
3892
|
+
});
|
|
3893
|
+
|
|
3894
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/retry", async (c) => {
|
|
3895
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3896
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
3897
|
+
const parsed = SessionRetryRequest.safeParse(await c.req.json().catch(() => null));
|
|
3898
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid session retry request" });
|
|
3899
|
+
try {
|
|
3900
|
+
return c.json(
|
|
3901
|
+
await retryFailedSession(deps, grant, workspaceId, c.req.param("sessionId"), parsed.data),
|
|
3902
|
+
);
|
|
3903
|
+
} catch (error) {
|
|
3904
|
+
if (error instanceof SessionRetryConflictError)
|
|
3905
|
+
return c.json({ code: error.code, message: error.message }, 409);
|
|
3906
|
+
return commandConflictResponse(c, error);
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3909
|
+
|
|
3062
3910
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/control", async (c) => {
|
|
3063
3911
|
const workspaceId = c.req.param("workspaceId");
|
|
3064
3912
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
@@ -3112,34 +3960,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3112
3960
|
const payload = parseSteerSessionAdmission(await c.req.json().catch(() => null));
|
|
3113
3961
|
let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
|
|
3114
3962
|
try {
|
|
3115
|
-
result = await
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
:
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
? {
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
});
|
|
3963
|
+
result = await withSiteCommandOrigin(c, grant, () =>
|
|
3964
|
+
acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
3965
|
+
text: payload.text,
|
|
3966
|
+
annotations: payload.annotations,
|
|
3967
|
+
modelContext: payload.modelContext ?? null,
|
|
3968
|
+
resources: payload.resources,
|
|
3969
|
+
model: payload.model ?? null,
|
|
3970
|
+
reasoningEffort: payload.reasoningEffort ?? null,
|
|
3971
|
+
latencyMode: payload.latencyMode ?? null,
|
|
3972
|
+
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
3973
|
+
connectionAccounts: payload.connectionAccounts,
|
|
3974
|
+
...(payload.personalResourceAttachment
|
|
3975
|
+
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
3976
|
+
: {}),
|
|
3977
|
+
authorization,
|
|
3978
|
+
delivery: "steer",
|
|
3979
|
+
origin: "human",
|
|
3980
|
+
...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
|
|
3981
|
+
...(payload.expectedDraftRevision !== undefined
|
|
3982
|
+
? { expectedDraftRevision: payload.expectedDraftRevision }
|
|
3983
|
+
: {}),
|
|
3984
|
+
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
3985
|
+
}),
|
|
3986
|
+
);
|
|
3140
3987
|
} catch (error) {
|
|
3141
3988
|
return commandConflictResponse(c, error);
|
|
3142
3989
|
}
|
|
3990
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3143
3991
|
return c.json(result, 202);
|
|
3144
3992
|
});
|
|
3145
3993
|
|
|
@@ -3154,15 +4002,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3154
4002
|
const grant = authorization.grant;
|
|
3155
4003
|
const sessionId = c.req.param("sessionId");
|
|
3156
4004
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
3157
|
-
const payload =
|
|
4005
|
+
const payload = parseRequestBody(
|
|
4006
|
+
SubmitComposerDraftRequest,
|
|
4007
|
+
await c.req.json().catch(() => null),
|
|
4008
|
+
);
|
|
3158
4009
|
let result: Awaited<ReturnType<typeof submitComposerDraftForRequest>>;
|
|
3159
4010
|
try {
|
|
3160
|
-
result = await
|
|
3161
|
-
|
|
3162
|
-
|
|
4011
|
+
result = await withSiteCommandOrigin(c, grant, () =>
|
|
4012
|
+
submitComposerDraftForRequest(deps, grant, workspaceId, sessionId, payload, {
|
|
4013
|
+
authorization,
|
|
4014
|
+
}),
|
|
4015
|
+
);
|
|
3163
4016
|
} catch (error) {
|
|
3164
4017
|
return commandConflictResponse(c, error);
|
|
3165
4018
|
}
|
|
4019
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3166
4020
|
return c.json(result, 202);
|
|
3167
4021
|
});
|
|
3168
4022
|
|
|
@@ -3197,47 +4051,73 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3197
4051
|
if (event.type === "user.message") {
|
|
3198
4052
|
let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
|
|
3199
4053
|
try {
|
|
3200
|
-
result = await
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
? {
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
});
|
|
4054
|
+
result = await withSiteCommandOrigin(c, grant, () =>
|
|
4055
|
+
acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
4056
|
+
text: event.payload.text,
|
|
4057
|
+
annotations: event.payload.annotations,
|
|
4058
|
+
modelContext: event.payload.modelContext ?? null,
|
|
4059
|
+
resources: event.payload.resources ?? [],
|
|
4060
|
+
model: event.payload.model ?? null,
|
|
4061
|
+
reasoningEffort: event.payload.reasoningEffort ?? null,
|
|
4062
|
+
latencyMode: event.payload.latencyMode ?? null,
|
|
4063
|
+
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
4064
|
+
connectionAccounts: event.payload.connectionAccounts,
|
|
4065
|
+
...(event.payload.personalResourceAttachment
|
|
4066
|
+
? { personalResourceAttachment: event.payload.personalResourceAttachment }
|
|
4067
|
+
: {}),
|
|
4068
|
+
authorization,
|
|
4069
|
+
...(event.payload.controlEtag !== undefined
|
|
4070
|
+
? { controlEtag: event.payload.controlEtag }
|
|
4071
|
+
: {}),
|
|
4072
|
+
...(event.payload.expectedDraftRevision !== undefined
|
|
4073
|
+
? { expectedDraftRevision: event.payload.expectedDraftRevision }
|
|
4074
|
+
: {}),
|
|
4075
|
+
...(event.clientEventId ? { clientEventId: event.clientEventId } : {}),
|
|
4076
|
+
}),
|
|
4077
|
+
);
|
|
3225
4078
|
} catch (error) {
|
|
3226
4079
|
return commandConflictResponse(c, error);
|
|
3227
4080
|
}
|
|
4081
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3228
4082
|
return c.json(result.accepted, 202);
|
|
3229
4083
|
}
|
|
3230
4084
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
4085
|
+
// Responses use the same header-only validation, permission and encryption
|
|
4086
|
+
// boundary as Send/Steer. Persistence belongs to the response transaction,
|
|
4087
|
+
// never a separate rotation that could commit without accepting the reply.
|
|
4088
|
+
const updates = event.payload.mcpCredentialUpdates ?? [];
|
|
4089
|
+
let mcpCredentialUpdates = [] as ReturnType<typeof validateSessionMcpCredentialUpdates>;
|
|
4090
|
+
if (updates.length) {
|
|
4091
|
+
const session = await getSession(db, workspaceId, sessionId);
|
|
4092
|
+
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
4093
|
+
mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
4094
|
+
settings,
|
|
4095
|
+
grant,
|
|
4096
|
+
session,
|
|
4097
|
+
updates,
|
|
3240
4098
|
});
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
if (event.type === "user.approvalDecision") {
|
|
4102
|
+
const { mcpCredentialUpdates: _writeOnlyCredentials, ...payload } = event.payload;
|
|
4103
|
+
let accepted;
|
|
4104
|
+
try {
|
|
4105
|
+
accepted = await acceptSessionApprovalDecision(db, {
|
|
4106
|
+
accountId: grant.accountId,
|
|
4107
|
+
workspaceId,
|
|
4108
|
+
sessionId,
|
|
4109
|
+
subjectId: grant.subjectId,
|
|
4110
|
+
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
4111
|
+
payload,
|
|
4112
|
+
mcpCredentialUpdates,
|
|
4113
|
+
clientEventId: event.clientEventId ?? null,
|
|
4114
|
+
});
|
|
4115
|
+
} catch (error) {
|
|
4116
|
+
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
4117
|
+
throw new HTTPException(422, { message: error.message });
|
|
4118
|
+
}
|
|
4119
|
+
throw error;
|
|
4120
|
+
}
|
|
3241
4121
|
if (accepted.action === "conflict") {
|
|
3242
4122
|
throw new HTTPException(409, {
|
|
3243
4123
|
message: `session is ${accepted.sessionStatus}; no unhandled approval is pending`,
|
|
@@ -3257,21 +4137,45 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3257
4137
|
}
|
|
3258
4138
|
|
|
3259
4139
|
if (event.type === "user.humanInputResponse") {
|
|
4140
|
+
// Secret values are write-only: split them off before anything else sees
|
|
4141
|
+
// the response, generate/encrypt them, and let the accept transaction
|
|
4142
|
+
// write them to vaults. The stored response and event get references.
|
|
4143
|
+
const { response, secretAnswers: rawSecretAnswers } = splitSecretAnswers(
|
|
4144
|
+
event.payload.response,
|
|
4145
|
+
event.payload.secretAnswers,
|
|
4146
|
+
);
|
|
3260
4147
|
let accepted;
|
|
3261
4148
|
try {
|
|
4149
|
+
const secretAnswers = prepareSecretAnswers(settings, rawSecretAnswers, "human");
|
|
3262
4150
|
accepted = await acceptSessionHumanInputResponse(db, {
|
|
3263
4151
|
accountId: grant.accountId,
|
|
3264
4152
|
workspaceId,
|
|
3265
4153
|
sessionId,
|
|
3266
4154
|
requestId: event.payload.requestId,
|
|
3267
|
-
response
|
|
4155
|
+
response,
|
|
4156
|
+
...(secretAnswers.length
|
|
4157
|
+
? {
|
|
4158
|
+
secretAnswers,
|
|
4159
|
+
secretWriteContext: humanSecretWriteContext({
|
|
4160
|
+
accountId: grant.accountId,
|
|
4161
|
+
workspaceId,
|
|
4162
|
+
subjectId: grant.subjectId,
|
|
4163
|
+
}),
|
|
4164
|
+
}
|
|
4165
|
+
: {}),
|
|
3268
4166
|
respondedBy: grant.subjectId,
|
|
3269
4167
|
canonicalHumanSession:
|
|
3270
|
-
authorization.canonicalManagedHumanSession ||
|
|
4168
|
+
authorization.canonicalManagedHumanSession ||
|
|
4169
|
+
authorization.canonicalLocalHumanSession ||
|
|
4170
|
+
isVerifiedDelegatedHumanAuthorization(authorization),
|
|
3271
4171
|
respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
|
|
3272
4172
|
clientEventId: event.clientEventId ?? null,
|
|
4173
|
+
mcpCredentialUpdates,
|
|
3273
4174
|
});
|
|
3274
4175
|
} catch (error) {
|
|
4176
|
+
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
4177
|
+
throw new HTTPException(422, { message: error.message });
|
|
4178
|
+
}
|
|
3275
4179
|
if (error instanceof SkillHumanResponseError) {
|
|
3276
4180
|
throw new HTTPException(
|
|
3277
4181
|
error.code === "conflict" ? 409 : error.code === "forbidden" ? 403 : 422,
|
|
@@ -3292,6 +4196,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3292
4196
|
},
|
|
3293
4197
|
);
|
|
3294
4198
|
}
|
|
4199
|
+
const vaultError = isVaultError(error) ? error : vaultErrorFromDatabase(error);
|
|
4200
|
+
if (vaultError) {
|
|
4201
|
+
const body: VaultApiError = {
|
|
4202
|
+
error: vaultError.code,
|
|
4203
|
+
message: vaultError.message,
|
|
4204
|
+
...(vaultError.details ? { details: vaultError.details } : {}),
|
|
4205
|
+
};
|
|
4206
|
+
return c.json(body, VAULT_ERROR_HTTP_STATUS[vaultError.code] as ContentfulStatusCode);
|
|
4207
|
+
}
|
|
3295
4208
|
throw error;
|
|
3296
4209
|
}
|
|
3297
4210
|
if (accepted.action === "not_found") {
|
|
@@ -3382,13 +4295,18 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3382
4295
|
// Resolve the shared-exposure disclosure for a session's group: `shared` when
|
|
3383
4296
|
// the group has >1 session (addendum E.1), and the OTHER sessions' ids ONLY
|
|
3384
4297
|
// (never their conversation/metadata; the query selects only id — stress g).
|
|
4298
|
+
// `shared` counts every member, including Only-me sessions the caller cannot
|
|
4299
|
+
// read, because sharing is a property of the box; the ids stay actor-scoped.
|
|
3385
4300
|
async function resolveSharedExposure(
|
|
3386
4301
|
workspaceId: string,
|
|
3387
4302
|
session: { id: string; sandboxGroupId: string },
|
|
3388
4303
|
): Promise<{ shared: boolean; sharedSessionIds: string[] }> {
|
|
3389
|
-
const ids = await
|
|
4304
|
+
const [ids, memberCount] = await Promise.all([
|
|
4305
|
+
listSessionIdsInGroup(db, workspaceId, session.sandboxGroupId),
|
|
4306
|
+
countAllSessionsInGroup(db, workspaceId, session.sandboxGroupId),
|
|
4307
|
+
]);
|
|
3390
4308
|
const others = ids.filter((id) => id !== session.id);
|
|
3391
|
-
return { shared: others.length > 0, sharedSessionIds: others };
|
|
4309
|
+
return { shared: memberCount > 1 || others.length > 0, sharedSessionIds: others };
|
|
3392
4310
|
}
|
|
3393
4311
|
|
|
3394
4312
|
// GET .../stream-capabilities — the capability-negotiation read. Returns the
|
|
@@ -3755,6 +4673,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3755
4673
|
workspaceId,
|
|
3756
4674
|
session,
|
|
3757
4675
|
viewerSubjectId: grant.subjectId,
|
|
4676
|
+
grant,
|
|
3758
4677
|
waitSignal: c.req.raw.signal,
|
|
3759
4678
|
...(parsed.data.viewerId ? { viewerId: parsed.data.viewerId } : {}),
|
|
3760
4679
|
});
|
|
@@ -4008,6 +4927,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
4008
4927
|
return c.json(out);
|
|
4009
4928
|
});
|
|
4010
4929
|
|
|
4930
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/read-workspace", async (c) => {
|
|
4931
|
+
const ctx = await channelAPreamble(c, "files:read", "fs.read");
|
|
4932
|
+
const req = await parseChannelABody(c, FsReadRequest);
|
|
4933
|
+
const out = await withChannelARead(channelAServices, ctx, ({ service }) =>
|
|
4934
|
+
service.fsRead({ ...req, workspaceOnly: true }),
|
|
4935
|
+
);
|
|
4936
|
+
return c.json(out);
|
|
4937
|
+
});
|
|
4938
|
+
|
|
4011
4939
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/publish", async (c) => {
|
|
4012
4940
|
const workspaceId = c.req.param("workspaceId");
|
|
4013
4941
|
const authorization = await requireAccessGrantAuthorization(
|
|
@@ -4517,11 +5445,19 @@ function codexRealtimeHttpFailure(error: CodexRealtimeBrokerError): {
|
|
|
4517
5445
|
}
|
|
4518
5446
|
}
|
|
4519
5447
|
|
|
4520
|
-
|
|
5448
|
+
/** Conflicts a person can hit by starting voice; the browser shows these verbatim. */
|
|
5449
|
+
const SESSION_REALTIME_USER_MESSAGES: Partial<Record<string, string>> = {
|
|
5450
|
+
REALTIME_ACTIVE:
|
|
5451
|
+
"Voice is already on for this session in another tab or window. End it there, or try again in a minute.",
|
|
5452
|
+
CONTROL_NOT_ACTIVE: "Resume this session to start voice.",
|
|
5453
|
+
SESSION_CANCELLED: "This session was cancelled, so voice can't start.",
|
|
5454
|
+
};
|
|
5455
|
+
|
|
5456
|
+
export function sessionRealtimeHttpError(error: unknown): HTTPException {
|
|
4521
5457
|
if (error instanceof HTTPException) return error;
|
|
4522
5458
|
if (error instanceof SessionRealtimeConflictError) {
|
|
4523
5459
|
return new HTTPException(error.code === "REALTIME_NOT_FOUND" ? 404 : 409, {
|
|
4524
|
-
message: error.message,
|
|
5460
|
+
message: SESSION_REALTIME_USER_MESSAGES[error.code] ?? error.message,
|
|
4525
5461
|
cause: error,
|
|
4526
5462
|
});
|
|
4527
5463
|
}
|
|
@@ -4553,11 +5489,28 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4553
5489
|
if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
|
|
4554
5490
|
if (suffix === "/attention" && verb === "PUT") return "session.attention.write";
|
|
4555
5491
|
if (suffix === "/archive" && verb === "PUT") return "session.archive.write";
|
|
5492
|
+
if (suffix === "/retention" && verb === "PUT") return "session.retention.write";
|
|
5493
|
+
// Task usage limit: read with the session; change as a control write
|
|
5494
|
+
// (the route additionally admits only a person or the host server key).
|
|
5495
|
+
if (suffix === "/usage-limit") {
|
|
5496
|
+
if (verb === "GET") return "session.read";
|
|
5497
|
+
if (verb === "PUT") return "session.control";
|
|
5498
|
+
return null;
|
|
5499
|
+
}
|
|
4556
5500
|
if (suffix === "/visibility" && verb === "PUT") return "session.visibility.write";
|
|
5501
|
+
// Owner-only preview of the same change: identical authority to making it.
|
|
5502
|
+
if (suffix === "/visibility/preview" && verb === "GET") return "session.visibility.write";
|
|
4557
5503
|
if (suffix === "/forks" && verb === "POST") return "session.fork.create";
|
|
4558
5504
|
if (suffix === "/channel" && verb === "PUT") return "session.channel.write";
|
|
4559
5505
|
if (suffix === "/variable-sets" && verb === "PUT") return "session.variable_sets.write";
|
|
4560
5506
|
if (suffix === "/tool-policy" && verb === "PUT") return "session.tool_policy.write";
|
|
5507
|
+
if (suffix === "/agent" && verb === "PUT") return "session.tool_policy.write";
|
|
5508
|
+
if (suffix === "/skills" && verb === "PUT") return "session.tool_policy.write";
|
|
5509
|
+
if (suffix === "/admin-access") {
|
|
5510
|
+
if (verb === "GET") return "session.read";
|
|
5511
|
+
if (verb === "PUT" || verb === "DELETE") return "session.tool_policy.write";
|
|
5512
|
+
return null;
|
|
5513
|
+
}
|
|
4561
5514
|
if (suffix === "/mcp-credentials/rotate" && verb === "POST")
|
|
4562
5515
|
return "session.mcp.credentials.rotate";
|
|
4563
5516
|
if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
|
|
@@ -4566,11 +5519,21 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4566
5519
|
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
4567
5520
|
if (suffix === "/background-commands" && verb === "GET") return "session.read";
|
|
4568
5521
|
if (suffix === "/model-context" && verb === "GET") return "session.read";
|
|
5522
|
+
if (/^\/tool-reviews\/[^/]+(?:\/details)?$/.test(suffix) && verb === "GET") return "session.read";
|
|
5523
|
+
if (suffix === "/codex-accounts" && verb === "GET") return "session.read";
|
|
4569
5524
|
if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
|
|
4570
5525
|
return "session.control";
|
|
4571
5526
|
}
|
|
4572
5527
|
if (suffix === "/codex-account" && verb === "POST") {
|
|
4573
|
-
return "session.
|
|
5528
|
+
return "session.model_account.write";
|
|
5529
|
+
}
|
|
5530
|
+
// The provider-neutral account choice (any provider) and the older
|
|
5531
|
+
// per-provider pin share one internal operation. Core adapts its name at the
|
|
5532
|
+
// host boundary according to the authorization port's explicit capability.
|
|
5533
|
+
if (suffix === "/model-account") {
|
|
5534
|
+
if (verb === "GET") return "session.read";
|
|
5535
|
+
if (verb === "PUT") return "session.model_account.write";
|
|
5536
|
+
return null;
|
|
4574
5537
|
}
|
|
4575
5538
|
if (suffix === "/realtime/webrtc" && verb === "POST") {
|
|
4576
5539
|
return "session.realtime.start";
|
|
@@ -4615,6 +5578,8 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4615
5578
|
return verb === "POST" ? "session.context.write" : null;
|
|
4616
5579
|
}
|
|
4617
5580
|
if (suffix === "/events/stream" && verb === "GET") return "session.stream.read";
|
|
5581
|
+
if (/^\/events\/[^/]+\/message-preview$/.test(suffix) && verb === "GET")
|
|
5582
|
+
return "session.events.read";
|
|
4618
5583
|
if (suffix === "/events") {
|
|
4619
5584
|
if (verb === "GET") return "session.events.read";
|
|
4620
5585
|
if (verb === "POST") return "session.append";
|
|
@@ -4632,6 +5597,8 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4632
5597
|
// acceptSessionUserMessageWithOutcome after the body is parsed.
|
|
4633
5598
|
if (suffix === "/composer-draft/submit" && verb === "POST") return "session.append";
|
|
4634
5599
|
if (suffix === "/control" && verb === "POST") return "session.control";
|
|
5600
|
+
if (suffix === "/retry" && verb === "POST") return "session.control";
|
|
5601
|
+
if (suffix === "/sandbox-recovery" && ["GET", "POST"].includes(verb)) return "session.control";
|
|
4635
5602
|
if (suffix === "/steer" && verb === "POST") return "session.steer";
|
|
4636
5603
|
if (suffix === "/human-input-requests" && verb === "GET") {
|
|
4637
5604
|
return "session.human_input.read";
|
|
@@ -4647,10 +5614,16 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4647
5614
|
if (suffix.startsWith("/viewers/") && ["POST", "DELETE"].includes(verb)) {
|
|
4648
5615
|
return "session.viewer.control";
|
|
4649
5616
|
}
|
|
4650
|
-
if (
|
|
5617
|
+
if (
|
|
5618
|
+
suffix === "/fs/list" ||
|
|
5619
|
+
suffix === "/fs/list-batch" ||
|
|
5620
|
+
suffix === "/fs/read" ||
|
|
5621
|
+
suffix === "/fs/read-workspace"
|
|
5622
|
+
) {
|
|
4651
5623
|
return verb === "POST" ? "session.files.read" : null;
|
|
4652
5624
|
}
|
|
4653
5625
|
if (suffix === "/artifacts/publish" && verb === "POST") return "session.files.write";
|
|
5626
|
+
if (suffix.startsWith("/artifact-associations/") && verb === "GET") return "session.read";
|
|
4654
5627
|
if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
|
|
4655
5628
|
return verb === "POST" ? "session.files.write" : null;
|
|
4656
5629
|
}
|
|
@@ -4679,6 +5652,17 @@ function sessionAuthorizationHttpError(error: unknown): HTTPException {
|
|
|
4679
5652
|
throw error;
|
|
4680
5653
|
}
|
|
4681
5654
|
|
|
5655
|
+
const SESSION_FORK_POINT_REFUSAL_MESSAGES: Record<SessionForkPointRefusal, string> = {
|
|
5656
|
+
message_unavailable:
|
|
5657
|
+
"Only your messages and the agent's replies can be fork points. Choose one of those, or fork the whole session.",
|
|
5658
|
+
boundary_not_retained:
|
|
5659
|
+
"This message is from before the conversation was compacted, so it can't be a fork point. Choose a more recent message, or fork the whole session.",
|
|
5660
|
+
history_compacted:
|
|
5661
|
+
"The conversation up to this message was compacted, so it can't be copied as it was then. Choose a more recent message, or fork the whole session.",
|
|
5662
|
+
unfinished_tool_exchange:
|
|
5663
|
+
"The agent was still waiting for a tool result at this message. Choose a later message, or fork the whole session.",
|
|
5664
|
+
};
|
|
5665
|
+
|
|
4682
5666
|
export function sessionTenancyHttpError(error: unknown): Error {
|
|
4683
5667
|
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
4684
5668
|
return new ApiHttpError(403, {
|
|
@@ -4690,7 +5674,7 @@ export function sessionTenancyHttpError(error: unknown): Error {
|
|
|
4690
5674
|
if (error instanceof SessionTenancyNotActivatedError) {
|
|
4691
5675
|
return new ApiHttpError(409, {
|
|
4692
5676
|
code: "conflict",
|
|
4693
|
-
message: "
|
|
5677
|
+
message: "Only-me chats are not enabled for this organization.",
|
|
4694
5678
|
retryable: false,
|
|
4695
5679
|
details: { reason: "not_activated" },
|
|
4696
5680
|
});
|
|
@@ -4711,6 +5695,25 @@ export function sessionTenancyHttpError(error: unknown): Error {
|
|
|
4711
5695
|
},
|
|
4712
5696
|
});
|
|
4713
5697
|
}
|
|
5698
|
+
if (error instanceof SessionVisibilityWorkConflictError) {
|
|
5699
|
+
return new ApiHttpError(409, {
|
|
5700
|
+
code: "conflict",
|
|
5701
|
+
message:
|
|
5702
|
+
error.reason === "active_work"
|
|
5703
|
+
? "The chat has work in progress. Send stopActiveWork to stop it and make the change."
|
|
5704
|
+
: "The chat's work is still stopping. Retry with the same idempotency key.",
|
|
5705
|
+
retryable: error.reason === "stopping_active_work",
|
|
5706
|
+
details: {
|
|
5707
|
+
reason: error.reason,
|
|
5708
|
+
runningTurnCount: error.work.runningTurnCount,
|
|
5709
|
+
queuedTurnCount: error.work.queuedTurnCount,
|
|
5710
|
+
activeGoal: error.work.activeGoal,
|
|
5711
|
+
liveVoice: error.work.liveVoice,
|
|
5712
|
+
otherPeoplePendingUpdateCount: error.work.otherPeoplePendingUpdateCount,
|
|
5713
|
+
otherPeopleAccountChoiceCount: error.work.otherPeopleAccountChoiceCount,
|
|
5714
|
+
},
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
4714
5717
|
if (
|
|
4715
5718
|
error instanceof SessionTenancyAccessError ||
|
|
4716
5719
|
error instanceof SessionAuthorizationDeniedError
|
|
@@ -4722,6 +5725,14 @@ export function sessionTenancyHttpError(error: unknown): Error {
|
|
|
4722
5725
|
});
|
|
4723
5726
|
}
|
|
4724
5727
|
if (error instanceof SessionTenancyInvalidRequestError) {
|
|
5728
|
+
if (error.forkPointRefusal) {
|
|
5729
|
+
return new ApiHttpError(422, {
|
|
5730
|
+
code: "validation_failed",
|
|
5731
|
+
message: SESSION_FORK_POINT_REFUSAL_MESSAGES[error.forkPointRefusal],
|
|
5732
|
+
retryable: false,
|
|
5733
|
+
details: { reason: `fork_point_${error.forkPointRefusal}` },
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
4725
5736
|
return new ApiHttpError(422, {
|
|
4726
5737
|
code: "validation_failed",
|
|
4727
5738
|
message: "Invalid session tenancy request.",
|
|
@@ -4795,7 +5806,7 @@ function eventEnumList<T extends string>(
|
|
|
4795
5806
|
});
|
|
4796
5807
|
}
|
|
4797
5808
|
|
|
4798
|
-
function sessionListQuery(
|
|
5809
|
+
export function sessionListQuery(
|
|
4799
5810
|
query: Record<string, string>,
|
|
4800
5811
|
allowCursor = true,
|
|
4801
5812
|
): {
|
|
@@ -4805,7 +5816,13 @@ function sessionListQuery(
|
|
|
4805
5816
|
cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
|
|
4806
5817
|
search: string | undefined;
|
|
4807
5818
|
pinsOnly: boolean;
|
|
5819
|
+
includePinned: boolean;
|
|
5820
|
+
includeTotals: boolean;
|
|
5821
|
+
needsYouOnly: boolean;
|
|
5822
|
+
contentArchivedOnly: boolean;
|
|
4808
5823
|
archivedOnly: boolean;
|
|
5824
|
+
sortBy: "updatedAt" | "createdAt" | "name" | undefined;
|
|
5825
|
+
archiveStatus: "active" | "archived" | "all" | undefined;
|
|
4809
5826
|
channelId: string | null | undefined;
|
|
4810
5827
|
createdBy: { kind: "subject" | "service"; subjectId: string } | undefined;
|
|
4811
5828
|
updatedFrom: Date | undefined;
|
|
@@ -4848,10 +5865,42 @@ function sessionListQuery(
|
|
|
4848
5865
|
});
|
|
4849
5866
|
}
|
|
4850
5867
|
const pinsOnly = query.pinsOnly === "true";
|
|
5868
|
+
if (query.includePinned !== undefined && !["true", "false"].includes(query.includePinned)) {
|
|
5869
|
+
throw new HTTPException(400, { message: 'includePinned must be "true" or "false"' });
|
|
5870
|
+
}
|
|
5871
|
+
const includePinned = query.includePinned !== "false";
|
|
5872
|
+
for (const key of ["includeTotals", "needsYouOnly", "contentArchivedOnly"]) {
|
|
5873
|
+
if (query[key] !== undefined && !["true", "false"].includes(query[key]!))
|
|
5874
|
+
throw new HTTPException(400, { message: `${key} must be "true" or "false"` });
|
|
5875
|
+
}
|
|
5876
|
+
const includeTotals = query.includeTotals === "true";
|
|
5877
|
+
const needsYouOnly = query.needsYouOnly === "true";
|
|
5878
|
+
const contentArchivedOnly = query.contentArchivedOnly === "true";
|
|
5879
|
+
if (includeTotals && (!allowCursor || (parentSessionId !== "null" && !pinsOnly)))
|
|
5880
|
+
throw new HTTPException(400, { message: "includeTotals requires a root page" });
|
|
5881
|
+
if (needsYouOnly && !allowCursor)
|
|
5882
|
+
throw new HTTPException(400, { message: 'needsYouOnly requires view="page"' });
|
|
5883
|
+
if (pinsOnly && !includePinned) {
|
|
5884
|
+
throw new HTTPException(400, { message: "pinsOnly requires includePinned" });
|
|
5885
|
+
}
|
|
4851
5886
|
if (query.archivedOnly !== undefined && query.archivedOnly !== "true") {
|
|
4852
5887
|
throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
|
|
4853
5888
|
}
|
|
4854
5889
|
const archivedOnly = query.archivedOnly === "true";
|
|
5890
|
+
const sortBy = z.enum(["updatedAt", "createdAt", "name"]).optional().safeParse(query.sortBy);
|
|
5891
|
+
const archiveStatus = z
|
|
5892
|
+
.enum(["active", "archived", "all"])
|
|
5893
|
+
.optional()
|
|
5894
|
+
.safeParse(query.archiveStatus);
|
|
5895
|
+
if (!sortBy.success || !archiveStatus.success) {
|
|
5896
|
+
throw new HTTPException(400, { message: "Invalid session sortBy or archiveStatus" });
|
|
5897
|
+
}
|
|
5898
|
+
if (archivedOnly && archiveStatus.data !== undefined && archiveStatus.data !== "archived") {
|
|
5899
|
+
throw new HTTPException(400, { message: "archivedOnly conflicts with archiveStatus" });
|
|
5900
|
+
}
|
|
5901
|
+
if (pinsOnly && archiveStatus.data === "archived") {
|
|
5902
|
+
throw new HTTPException(400, { message: "pinsOnly cannot be combined with archived status" });
|
|
5903
|
+
}
|
|
4855
5904
|
const channelId = query.channelId;
|
|
4856
5905
|
if (
|
|
4857
5906
|
channelId !== undefined &&
|
|
@@ -4918,12 +5967,14 @@ function sessionListQuery(
|
|
|
4918
5967
|
const parsedEndUser = SessionScopeSubjectId.safeParse(query.scopeSubjectId);
|
|
4919
5968
|
if (!parsedEndUser.success) {
|
|
4920
5969
|
throw new HTTPException(400, {
|
|
4921
|
-
message: "scopeSubjectId must be a canonical
|
|
5970
|
+
message: "scopeSubjectId must be a canonical Opengeni user subject",
|
|
4922
5971
|
});
|
|
4923
5972
|
}
|
|
4924
5973
|
scopeSubjectId = parsedEndUser.data;
|
|
4925
5974
|
}
|
|
4926
5975
|
const hasPageFilters =
|
|
5976
|
+
needsYouOnly ||
|
|
5977
|
+
contentArchivedOnly ||
|
|
4927
5978
|
originSiteId !== undefined ||
|
|
4928
5979
|
channelId !== undefined ||
|
|
4929
5980
|
createdByKind !== undefined ||
|
|
@@ -4955,7 +6006,13 @@ function sessionListQuery(
|
|
|
4955
6006
|
cursor,
|
|
4956
6007
|
search: search || undefined,
|
|
4957
6008
|
pinsOnly,
|
|
6009
|
+
includePinned,
|
|
6010
|
+
includeTotals,
|
|
6011
|
+
needsYouOnly,
|
|
6012
|
+
contentArchivedOnly,
|
|
4958
6013
|
archivedOnly,
|
|
6014
|
+
sortBy: sortBy.data,
|
|
6015
|
+
archiveStatus: archiveStatus.data,
|
|
4959
6016
|
channelId: channelId === undefined ? undefined : channelId === "null" ? null : channelId,
|
|
4960
6017
|
createdBy:
|
|
4961
6018
|
createdByKind && createdBySubjectId
|
|
@@ -5310,8 +6367,182 @@ function optionalEventSequence(raw: string | undefined): number | undefined {
|
|
|
5310
6367
|
return Math.floor(sequence);
|
|
5311
6368
|
}
|
|
5312
6369
|
|
|
6370
|
+
/**
|
|
6371
|
+
* The minimal Site profile of a session a Site page starts: only the allowed
|
|
6372
|
+
* create fields, metadata without reserved `_opengeni` keys, no vault access,
|
|
6373
|
+
* and the workspace's default agent capabilities without workspace
|
|
6374
|
+
* administration or browser and computer control, so the page cannot reach
|
|
6375
|
+
* the viewer's Connected Machines, sandboxes or browsers. Workers it spawns
|
|
6376
|
+
* only narrow that. It runs without a sandbox; its first-party tools are the
|
|
6377
|
+
* ones the Site declares plus the Site's own data tools, and its MCP
|
|
6378
|
+
* connections only the ones the Site declares. Everything else (resources,
|
|
6379
|
+
* Skills, learning, goals) is the workspace default for a plain chat. Agents
|
|
6380
|
+
* started from a Site stay clamped
|
|
6381
|
+
* (`assertNotSiteClampedAgent`).
|
|
6382
|
+
*/
|
|
6383
|
+
async function siteFrameSessionProfile(
|
|
6384
|
+
deps: ApiRouteDeps,
|
|
6385
|
+
workspaceId: string,
|
|
6386
|
+
payload: unknown,
|
|
6387
|
+
frame: { accountId: string; siteId: string | null; versionId: string | null },
|
|
6388
|
+
): Promise<Record<string, unknown>> {
|
|
6389
|
+
const body = (payload ?? {}) as Record<string, unknown>;
|
|
6390
|
+
const profile: Record<string, unknown> = {};
|
|
6391
|
+
for (const field of SITE_FRAME_SESSION_CREATE_FIELDS)
|
|
6392
|
+
if (body[field] !== undefined && body[field] !== null) profile[field] = body[field];
|
|
6393
|
+
const metadata = body.metadata;
|
|
6394
|
+
profile.metadata =
|
|
6395
|
+
metadata && typeof metadata === "object" && !Array.isArray(metadata)
|
|
6396
|
+
? Object.fromEntries(Object.entries(metadata).filter(([key]) => !key.startsWith("_opengeni")))
|
|
6397
|
+
: {};
|
|
6398
|
+
profile.vaultPolicy = { mode: "selected", vaultIds: [] };
|
|
6399
|
+
// Tools are the overlap of what the Site declares and what the person may
|
|
6400
|
+
// use: only the workspace MCP connections the Site version requests (the
|
|
6401
|
+
// person's own access to each still applies), never the workspace
|
|
6402
|
+
// default set. A frame with no published Site declares none.
|
|
6403
|
+
profile.tools = await siteFrameDeclaredTools(deps, workspaceId, frame);
|
|
6404
|
+
const unavailable = agentConfigDeploymentPolicy(deps.settings).unavailable;
|
|
6405
|
+
const capabilities = siteFrameCapabilities(
|
|
6406
|
+
resolveWorkspaceAgentDefaults((await requireWorkspace(deps.db, workspaceId)).settings)
|
|
6407
|
+
?.capabilities ?? "all",
|
|
6408
|
+
unavailable,
|
|
6409
|
+
);
|
|
6410
|
+
profile.agent = { capabilities };
|
|
6411
|
+
// First-party tools are the ones the Site declares plus the Site's own data
|
|
6412
|
+
// tools, never the workspace default set; a Site page's agent runs without
|
|
6413
|
+
// a sandbox.
|
|
6414
|
+
profile.firstPartyMcpTools = (await siteFrameFirstPartyTools(deps, workspaceId, frame)).filter(
|
|
6415
|
+
(tool) => {
|
|
6416
|
+
const owner = FIRST_PARTY_MCP_TOOL_CAPABILITIES[tool];
|
|
6417
|
+
if (owner === "runtime") return true;
|
|
6418
|
+
if (owner === "sandbox" || unavailable[owner] !== undefined) return false;
|
|
6419
|
+
const value = (capabilities as Record<string, unknown>)[owner];
|
|
6420
|
+
return value === undefined ? capabilities.from === "all" : value !== false;
|
|
6421
|
+
},
|
|
6422
|
+
);
|
|
6423
|
+
profile.sandboxBackend = "none";
|
|
6424
|
+
return profile;
|
|
6425
|
+
}
|
|
6426
|
+
|
|
6427
|
+
/**
|
|
6428
|
+
* The first-party tools of a Site page's session: the session runtime's own,
|
|
6429
|
+
* the first-party tools the Site version declares, and, for a published Site,
|
|
6430
|
+
* its own data tools (`site_data_query`, `site_data_write`, and the Site SQL
|
|
6431
|
+
* tools when Site SQL is on), all within the deployment's tool allowlist.
|
|
6432
|
+
*/
|
|
6433
|
+
async function siteFrameFirstPartyTools(
|
|
6434
|
+
deps: ApiRouteDeps,
|
|
6435
|
+
workspaceId: string,
|
|
6436
|
+
frame: { accountId: string; siteId: string | null; versionId: string | null },
|
|
6437
|
+
): Promise<FirstPartyMcpToolName[]> {
|
|
6438
|
+
const allowed = new Set<string>(resolveFirstPartyMcpToolPolicy(deps.settings).allowed);
|
|
6439
|
+
const tools = new Set<FirstPartyMcpToolName>(["set_session_title", "wait_for_input"]);
|
|
6440
|
+
if (frame.siteId && frame.versionId) {
|
|
6441
|
+
const scope = { accountId: frame.accountId, workspaceId };
|
|
6442
|
+
const version = await getPublishedSiteVersion(
|
|
6443
|
+
deps.db,
|
|
6444
|
+
scope,
|
|
6445
|
+
frame.siteId,
|
|
6446
|
+
frame.versionId,
|
|
6447
|
+
).catch(() => null);
|
|
6448
|
+
for (const tool of version?.requestedTools ?? []) {
|
|
6449
|
+
const parsed = FirstPartyMcpToolName.safeParse(tool.toolName);
|
|
6450
|
+
if (tool.serverId === "opengeni" && parsed.success) tools.add(parsed.data);
|
|
6451
|
+
}
|
|
6452
|
+
if (version)
|
|
6453
|
+
for (const tool of siteAgentDataToolNames({
|
|
6454
|
+
viewerKind: "member",
|
|
6455
|
+
siteSql: await siteAgentSiteSqlOn(deps, scope, frame.siteId),
|
|
6456
|
+
allowed: [...allowed],
|
|
6457
|
+
}))
|
|
6458
|
+
tools.add(tool);
|
|
6459
|
+
}
|
|
6460
|
+
return [...tools].filter((tool) => allowed.has(tool)).sort();
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
/** The workspace MCP connections (not the first-party server) a Site version declares. */
|
|
6464
|
+
async function siteFrameDeclaredTools(
|
|
6465
|
+
deps: ApiRouteDeps,
|
|
6466
|
+
workspaceId: string,
|
|
6467
|
+
frame: { accountId: string; siteId: string | null; versionId: string | null },
|
|
6468
|
+
): Promise<ToolRef[]> {
|
|
6469
|
+
if (!frame.siteId || !frame.versionId) return [];
|
|
6470
|
+
const version = await getPublishedSiteVersion(
|
|
6471
|
+
deps.db,
|
|
6472
|
+
{ accountId: frame.accountId, workspaceId },
|
|
6473
|
+
frame.siteId,
|
|
6474
|
+
frame.versionId,
|
|
6475
|
+
).catch(() => null);
|
|
6476
|
+
const declared = new Map<string, Set<string>>();
|
|
6477
|
+
for (const { serverId, toolName } of version?.requestedTools ?? []) {
|
|
6478
|
+
if (serverId === "opengeni" || !/^[A-Za-z0-9_-]{1,200}$/u.test(serverId)) continue;
|
|
6479
|
+
const names = declared.get(serverId) ?? new Set<string>();
|
|
6480
|
+
names.add(toolName);
|
|
6481
|
+
declared.set(serverId, names);
|
|
6482
|
+
}
|
|
6483
|
+
return [...declared].map(([id, names]) => ({
|
|
6484
|
+
kind: "mcp",
|
|
6485
|
+
id,
|
|
6486
|
+
optional: true,
|
|
6487
|
+
allowedTools: [...names].sort(),
|
|
6488
|
+
}));
|
|
6489
|
+
}
|
|
6490
|
+
|
|
6491
|
+
/**
|
|
6492
|
+
* A Site page's session capabilities: the workspace default with workspace
|
|
6493
|
+
* administration (sandboxes, Connected Machines, `run_on`) and browser and
|
|
6494
|
+
* computer control off. Capabilities this server does not offer are left to
|
|
6495
|
+
* the default, so the create never fails on them.
|
|
6496
|
+
*/
|
|
6497
|
+
export function siteFrameCapabilities(
|
|
6498
|
+
workspaceDefault: AgentCapabilities,
|
|
6499
|
+
unavailable: Readonly<Partial<Record<string, string>>>,
|
|
6500
|
+
): Exclude<AgentCapabilities, string> {
|
|
6501
|
+
if (typeof workspaceDefault === "string")
|
|
6502
|
+
return { from: workspaceDefault, workspaceAdmin: false, browser: false };
|
|
6503
|
+
const capabilities: Record<string, unknown> = {};
|
|
6504
|
+
for (const [name, value] of Object.entries(workspaceDefault))
|
|
6505
|
+
if (name === "from" || value === false || unavailable[name] === undefined)
|
|
6506
|
+
capabilities[name] = value;
|
|
6507
|
+
return {
|
|
6508
|
+
...(capabilities as Exclude<AgentCapabilities, string>),
|
|
6509
|
+
workspaceAdmin: false,
|
|
6510
|
+
browser: false,
|
|
6511
|
+
};
|
|
6512
|
+
}
|
|
6513
|
+
|
|
5313
6514
|
/** Stable, value-free JSON errors for only the create-session boundary. */
|
|
5314
6515
|
export function sessionCreateErrorResponse(c: Context, error: unknown): Response {
|
|
6516
|
+
if (error instanceof SessionUsageLimitHttpError) {
|
|
6517
|
+
// a create-time usage limit the caller may not set (agents and
|
|
6518
|
+
// children) or that this server does not accept yet (producer flag off).
|
|
6519
|
+
return c.json(
|
|
6520
|
+
{
|
|
6521
|
+
code: error.status === 403 ? "SESSION_CREATE_FORBIDDEN" : "SESSION_CREATE_REJECTED",
|
|
6522
|
+
message: error.message,
|
|
6523
|
+
details: { code: error.code },
|
|
6524
|
+
},
|
|
6525
|
+
error.status as 403 | 404 | 409 | 422 | 503,
|
|
6526
|
+
);
|
|
6527
|
+
}
|
|
6528
|
+
if (error instanceof UnsupportedLatencyModeError) {
|
|
6529
|
+
return c.json(
|
|
6530
|
+
{
|
|
6531
|
+
code: "SESSION_CREATE_REJECTED",
|
|
6532
|
+
message: error.message,
|
|
6533
|
+
details: {
|
|
6534
|
+
code: error.code,
|
|
6535
|
+
modelId: error.modelId,
|
|
6536
|
+
latencyMode: error.latencyMode,
|
|
6537
|
+
allowedLatencyModes: [...error.allowedLatencyModes],
|
|
6538
|
+
},
|
|
6539
|
+
},
|
|
6540
|
+
422,
|
|
6541
|
+
);
|
|
6542
|
+
}
|
|
6543
|
+
if (error instanceof SessionCreateConnectionSelectionUnavailableError) {
|
|
6544
|
+
return c.json({ code: error.code, message: error.message, retryable: false }, 409);
|
|
6545
|
+
}
|
|
5315
6546
|
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
5316
6547
|
return c.json(
|
|
5317
6548
|
{
|
|
@@ -5374,6 +6605,24 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
5374
6605
|
409,
|
|
5375
6606
|
);
|
|
5376
6607
|
}
|
|
6608
|
+
if (
|
|
6609
|
+
error instanceof HTTPException &&
|
|
6610
|
+
error.status === 422 &&
|
|
6611
|
+
error.cause instanceof AgentConfigError
|
|
6612
|
+
) {
|
|
6613
|
+
// Typed agent-configuration failure: the specific code is details.code.
|
|
6614
|
+
return c.json(
|
|
6615
|
+
{
|
|
6616
|
+
code: "SESSION_CREATE_REJECTED",
|
|
6617
|
+
message: error.message,
|
|
6618
|
+
details: {
|
|
6619
|
+
code: error.cause.code,
|
|
6620
|
+
...(error.cause.capability ? { capability: error.cause.capability } : {}),
|
|
6621
|
+
},
|
|
6622
|
+
},
|
|
6623
|
+
422,
|
|
6624
|
+
);
|
|
6625
|
+
}
|
|
5377
6626
|
if (error instanceof HTTPException && error.status === 422) {
|
|
5378
6627
|
return c.json(
|
|
5379
6628
|
{
|
|
@@ -5425,6 +6674,52 @@ function zodErrorFields(error: ZodError): string {
|
|
|
5425
6674
|
return `${shown.join(", ")}${remainder > 0 ? `, and ${remainder} more` : ""}`;
|
|
5426
6675
|
}
|
|
5427
6676
|
|
|
6677
|
+
/** Statuses in which a chat still has work that would keep running after it is archived. */
|
|
6678
|
+
const ARCHIVE_LIVE_WORK_STATUSES: ReadonlySet<string> = new Set([
|
|
6679
|
+
"queued",
|
|
6680
|
+
"running",
|
|
6681
|
+
"requires_action",
|
|
6682
|
+
"recovering",
|
|
6683
|
+
"waiting_capacity",
|
|
6684
|
+
]);
|
|
6685
|
+
|
|
6686
|
+
/**
|
|
6687
|
+
* Archiving a chat you started means you are done with it, so work it still
|
|
6688
|
+
* has (a running or queued turn, a pending wait, an active goal) must not keep
|
|
6689
|
+
* running out of sight. It is paused exactly like the Pause control: reversible
|
|
6690
|
+
* with Resume, and a paused chat dispatches no goal continuations either. A
|
|
6691
|
+
* chat someone else started is never paused: archiving only hides it for you.
|
|
6692
|
+
* Settled chats are left alone so a later message to them still just runs.
|
|
6693
|
+
*/
|
|
6694
|
+
async function pauseOwnArchivedWork(
|
|
6695
|
+
deps: ApiRouteDeps,
|
|
6696
|
+
grant: AccessGrant,
|
|
6697
|
+
workspaceId: string,
|
|
6698
|
+
session: Session,
|
|
6699
|
+
): Promise<Session> {
|
|
6700
|
+
if (session.createdBy.kind !== "subject" || session.createdBy.subjectId !== grant.subjectId)
|
|
6701
|
+
return session;
|
|
6702
|
+
if (!hasPermission(grant.permissions, "sessions:control")) return session;
|
|
6703
|
+
if (session.effectiveControl.directState === "paused") return session;
|
|
6704
|
+
const liveWork =
|
|
6705
|
+
ARCHIVE_LIVE_WORK_STATUSES.has(session.status) ||
|
|
6706
|
+
session.activeTurnId !== null ||
|
|
6707
|
+
Boolean(session.inputWait) ||
|
|
6708
|
+
(await getSessionGoal(deps.db, workspaceId, session.id))?.status === "active";
|
|
6709
|
+
if (!liveWork) return session;
|
|
6710
|
+
const paused = await controlHumanSessionWorkstream(
|
|
6711
|
+
deps,
|
|
6712
|
+
{ accountId: grant.accountId, workspaceId, sessionId: session.id, subjectId: grant.subjectId },
|
|
6713
|
+
{
|
|
6714
|
+
action: "pause",
|
|
6715
|
+
reason: "Archived",
|
|
6716
|
+
// One pause per archive: a retried archive replays it instead of pausing twice.
|
|
6717
|
+
clientEventId: `archive-pause:${session.id}:${session.archiveVersion}`,
|
|
6718
|
+
},
|
|
6719
|
+
);
|
|
6720
|
+
return { ...session, effectiveControl: paused.effectiveControl };
|
|
6721
|
+
}
|
|
6722
|
+
|
|
5428
6723
|
function commandConflictResponse(c: Context, error: unknown): Response {
|
|
5429
6724
|
if (error instanceof QueueCommandConflictError) {
|
|
5430
6725
|
return c.json({ code: error.code, message: error.message, current: error.current }, 409);
|
|
@@ -5435,24 +6730,56 @@ function commandConflictResponse(c: Context, error: unknown): Response {
|
|
|
5435
6730
|
if (error instanceof SessionCommandIdempotencyError) {
|
|
5436
6731
|
return c.json({ code: error.code, message: error.message }, 409);
|
|
5437
6732
|
}
|
|
6733
|
+
if (error instanceof PersonalSessionInitiatorRequiredError) {
|
|
6734
|
+
return c.json({ code: error.code, message: error.message }, 403);
|
|
6735
|
+
}
|
|
5438
6736
|
throw error;
|
|
5439
6737
|
}
|
|
5440
6738
|
|
|
5441
6739
|
type EffectivePolicyContext = {
|
|
5442
6740
|
workspaceServerIds: string[];
|
|
5443
6741
|
workspaceDefaultServerIds: string[];
|
|
6742
|
+
effectiveToolsContext: Awaited<ReturnType<typeof workspaceSessionEffectiveToolsContext>>;
|
|
6743
|
+
/** The Codex session pointers by cutover disposition (see codex-session-pointers). */
|
|
6744
|
+
codexPointers: CodexSessionPointerProjection;
|
|
5444
6745
|
};
|
|
5445
6746
|
|
|
6747
|
+
/**
|
|
6748
|
+
* Everything a Session response projection needs, loaded once for all the
|
|
6749
|
+
* sessions it returns. Every route that returns a Session projects it through
|
|
6750
|
+
* `sessionResponse` with this context.
|
|
6751
|
+
*/
|
|
5446
6752
|
async function loadEffectivePolicyContext(
|
|
5447
6753
|
deps: ApiRouteDeps,
|
|
5448
6754
|
workspaceId: string,
|
|
5449
6755
|
subjectId: string,
|
|
6756
|
+
sessions: readonly Session[],
|
|
5450
6757
|
): Promise<EffectivePolicyContext> {
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
6758
|
+
// Both projections read the same workspace row; hydrate it once.
|
|
6759
|
+
const workspaceRead = requireWorkspace(deps.db, workspaceId);
|
|
6760
|
+
void workspaceRead.catch(() => undefined);
|
|
6761
|
+
const [policy, effectiveToolsContext, codexPointers] = await Promise.all([
|
|
6762
|
+
workspaceSessionToolPolicyContext(
|
|
6763
|
+
deps.db,
|
|
6764
|
+
workspaceId,
|
|
6765
|
+
deps.settings,
|
|
6766
|
+
subjectId,
|
|
6767
|
+
workspaceRead,
|
|
6768
|
+
),
|
|
6769
|
+
workspaceSessionEffectiveToolsContext(deps, workspaceId, subjectId, sessions, workspaceRead),
|
|
6770
|
+
loadCodexSessionPointerProjection(deps.db, workspaceId, sessions),
|
|
5454
6771
|
]);
|
|
5455
|
-
return {
|
|
6772
|
+
return { ...policy, effectiveToolsContext, codexPointers };
|
|
6773
|
+
}
|
|
6774
|
+
|
|
6775
|
+
/** The response projection of one Session: Codex pointers and effective tool policy. */
|
|
6776
|
+
function sessionResponse(session: Session, policy: EffectivePolicyContext): Session {
|
|
6777
|
+
return sessionWithEffectiveToolPolicy(
|
|
6778
|
+
policy.codexPointers(session),
|
|
6779
|
+
policy.workspaceServerIds,
|
|
6780
|
+
policy.workspaceDefaultServerIds,
|
|
6781
|
+
policy.effectiveToolsContext,
|
|
6782
|
+
);
|
|
5456
6783
|
}
|
|
5457
6784
|
|
|
5458
6785
|
async function withEffectivePolicy(
|
|
@@ -5461,22 +6788,14 @@ async function withEffectivePolicy(
|
|
|
5461
6788
|
subjectId: string,
|
|
5462
6789
|
session: Session,
|
|
5463
6790
|
): Promise<Session> {
|
|
5464
|
-
const policy = await loadEffectivePolicyContext(deps, workspaceId, subjectId);
|
|
5465
|
-
return
|
|
5466
|
-
session,
|
|
5467
|
-
policy.workspaceServerIds,
|
|
5468
|
-
policy.workspaceDefaultServerIds,
|
|
5469
|
-
);
|
|
6791
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId, subjectId, [session]);
|
|
6792
|
+
return sessionResponse(session, policy);
|
|
5470
6793
|
}
|
|
5471
6794
|
|
|
5472
6795
|
function mapLineageNodes(nodes: LineageNode[], policy: EffectivePolicyContext): LineageNode[] {
|
|
5473
6796
|
return nodes.map((node) => ({
|
|
5474
6797
|
...node,
|
|
5475
|
-
session:
|
|
5476
|
-
node.session as Session,
|
|
5477
|
-
policy.workspaceServerIds,
|
|
5478
|
-
policy.workspaceDefaultServerIds,
|
|
5479
|
-
),
|
|
6798
|
+
session: sessionResponse(node.session as Session, policy),
|
|
5480
6799
|
children: mapLineageNodes(node.children, policy),
|
|
5481
6800
|
}));
|
|
5482
6801
|
}
|