@opengeni/core 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/README.md +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { sandboxImageAllowlist, type Settings } from "@opengeni/config";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
resolveWorkspaceDefaultSandboxImage,
|
|
4
|
+
type AccessGrant,
|
|
5
5
|
type RigVersion,
|
|
6
6
|
type Session,
|
|
7
7
|
type SandboxBackend,
|
|
@@ -9,11 +9,17 @@ import {
|
|
|
9
9
|
} from "@opengeni/contracts";
|
|
10
10
|
import {
|
|
11
11
|
getRigVersion,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
getScheduledScopedRigVersionMetadata,
|
|
13
|
+
getWorkspace,
|
|
14
|
+
nestedPostgresSqlState,
|
|
14
15
|
type Database,
|
|
15
16
|
} from "@opengeni/db";
|
|
16
17
|
import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
|
|
18
|
+
import { HTTPException } from "hono/http-exception";
|
|
19
|
+
import {
|
|
20
|
+
grantHasAgentAttemptAuthority,
|
|
21
|
+
requireLiveAgentAttemptAuthorization,
|
|
22
|
+
} from "../session-authorization";
|
|
17
23
|
import {
|
|
18
24
|
rigProviderImageContentHash,
|
|
19
25
|
rigProviderImageMatchesDefinition,
|
|
@@ -41,6 +47,29 @@ export function managedSessionGroupBackend(
|
|
|
41
47
|
return backend === "none" || backend === "selfhosted" ? null : backend;
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
const SESSION_GROUP_MACHINE_NAMES: Record<ManagedSessionGroupBackend, string> = {
|
|
51
|
+
local: "this computer",
|
|
52
|
+
docker: "Docker",
|
|
53
|
+
modal: "Modal",
|
|
54
|
+
daytona: "Daytona",
|
|
55
|
+
runloop: "Runloop",
|
|
56
|
+
e2b: "E2B",
|
|
57
|
+
blaxel: "Blaxel",
|
|
58
|
+
cloudflare: "Cloudflare",
|
|
59
|
+
vercel: "Vercel",
|
|
60
|
+
opensandbox: "OpenSandbox",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Display name and kind for the session's own managed box. The kind is the
|
|
64
|
+
* real provider. Callers must not collapse local, Docker, or other providers
|
|
65
|
+
* into Modal. */
|
|
66
|
+
export function sessionGroupMachinePresentation(backend: ManagedSessionGroupBackend): {
|
|
67
|
+
name: string;
|
|
68
|
+
kind: ManagedSessionGroupBackend;
|
|
69
|
+
} {
|
|
70
|
+
return { name: SESSION_GROUP_MACHINE_NAMES[backend], kind: backend };
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
/** A machine-home row carries the machine's host OS; its managed group uses the
|
|
45
74
|
* platform's canonical managed-sandbox OS instead. Ordinary sessions preserve
|
|
46
75
|
* their explicitly selected OS. */
|
|
@@ -51,74 +80,25 @@ export function managedSessionGroupOs(
|
|
|
51
80
|
return sessionBackend === "selfhosted" ? "linux" : sessionOs;
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return input.manifestSnapshot === null && input.manifestDigest === null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function resolveWorkspaceLegacyRuntimePacks(
|
|
66
|
-
db: Database,
|
|
67
|
-
workspaceId: string,
|
|
68
|
-
): Promise<CapabilityPack[]> {
|
|
69
|
-
const installations = await listPackInstallations(db, workspaceId);
|
|
70
|
-
const packs: CapabilityPack[] = [];
|
|
71
|
-
for (const installation of installations) {
|
|
72
|
-
if (installation.status !== "active" || !packInstallationUsesLegacyRuntime(installation)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const registration = await getWorkspacePack(db, workspaceId, installation.packId);
|
|
76
|
-
if (!registration)
|
|
77
|
-
throw new Error(`Enabled Pack ${installation.packId} has no registered manifest`);
|
|
78
|
-
const parsed = StoredCapabilityPack.safeParse(registration.pack);
|
|
79
|
-
if (!parsed.success)
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Enabled Pack ${installation.packId} requires repair: ${parsed.error.message}`,
|
|
82
|
-
);
|
|
83
|
-
packs.push(parsed.data);
|
|
84
|
-
}
|
|
85
|
-
return packs;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function legacySandboxRuntimeFromPacks(packs: readonly CapabilityPack[]): {
|
|
89
|
-
sandboxImage: string | null;
|
|
90
|
-
sandboxProviderImages: CapabilityPack["sandboxProviderImages"] | null;
|
|
91
|
-
} {
|
|
92
|
-
const imagePacks = packs.filter(
|
|
93
|
-
(pack) => typeof pack.sandboxImage === "string" && pack.sandboxImage.trim().length > 0,
|
|
94
|
-
);
|
|
95
|
-
if (imagePacks.length > 1) {
|
|
96
|
-
const ids = imagePacks
|
|
97
|
-
.map((pack) => pack.id)
|
|
98
|
-
.sort()
|
|
99
|
-
.join(", ");
|
|
100
|
-
throw new Error(
|
|
101
|
-
`Multiple enabled packs declare a sandbox image (${ids}). Only one enabled pack per workspace may declare sandboxImage; disable the others and retry.`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
sandboxImage: imagePacks[0]?.sandboxImage?.trim() ?? null,
|
|
106
|
-
sandboxProviderImages: imagePacks[0]?.sandboxProviderImages ?? null,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function settingsWithPackSandboxImage(
|
|
83
|
+
/**
|
|
84
|
+
* Apply a workspace's selected sandbox image. The deployment keeps authority:
|
|
85
|
+
* only an image that is on its allowlist right now is used, so removing an
|
|
86
|
+
* entry returns every workspace that chose it to the deployment image. The
|
|
87
|
+
* lease treats the image as shared state, so a change rotates a live box
|
|
88
|
+
* through the normal capture-and-restore path instead of reusing it.
|
|
89
|
+
*/
|
|
90
|
+
export function settingsWithWorkspaceSandboxImage(
|
|
111
91
|
settings: Settings,
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
workspaceSettings: unknown,
|
|
93
|
+
backend: SandboxBackend,
|
|
114
94
|
): Settings {
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
modalImageRef:
|
|
120
|
-
|
|
121
|
-
|
|
95
|
+
const image = resolveWorkspaceDefaultSandboxImage(workspaceSettings);
|
|
96
|
+
if (!image || !sandboxImageAllowlist(settings).includes(image)) return settings;
|
|
97
|
+
if (backend === "docker") return { ...settings, dockerImage: image };
|
|
98
|
+
if (backend === "modal") {
|
|
99
|
+
return { ...settings, modalImageRef: image, modalImageId: undefined };
|
|
100
|
+
}
|
|
101
|
+
return settings;
|
|
122
102
|
}
|
|
123
103
|
|
|
124
104
|
export function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings {
|
|
@@ -272,27 +252,121 @@ export type SessionSandboxRuntime = {
|
|
|
272
252
|
rigVersion: RigVersion | null;
|
|
273
253
|
};
|
|
274
254
|
|
|
255
|
+
/** The subject a direct attach resolves under when no human subject drives it
|
|
256
|
+
* (the same sentinel the attach Variable Set defaults use). It holds no
|
|
257
|
+
* organization membership, so it can resolve organization and workspace
|
|
258
|
+
* Sandbox Environments but never a personal one. */
|
|
259
|
+
const SESSION_ATTACH_SUBJECT = "session-attach";
|
|
260
|
+
|
|
261
|
+
/** Who a direct attach resolves a session's Sandbox Environment as: an explicit
|
|
262
|
+
* subject (null for a service attach), or the authenticated route grant. */
|
|
263
|
+
export type SessionAttachRigAuthority = { subjectId: string | null } | { grant: AccessGrant };
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The subject whose scoped visibility applies. An agent-attempt grant carries a
|
|
267
|
+
* technical worker identity with no organization membership, so it resolves as
|
|
268
|
+
* its live attempt's frozen initiating human, as the MCP Sandbox Environment and
|
|
269
|
+
* fleet tools do; an attempt without one resolves as the service sentinel.
|
|
270
|
+
*/
|
|
271
|
+
async function sessionAttachRigSubjectId(
|
|
272
|
+
db: Database,
|
|
273
|
+
authority: SessionAttachRigAuthority,
|
|
274
|
+
): Promise<string | null> {
|
|
275
|
+
if (!("grant" in authority)) return authority.subjectId;
|
|
276
|
+
const { grant } = authority;
|
|
277
|
+
if (!grantHasAgentAttemptAuthority(grant)) return grant.subjectId;
|
|
278
|
+
const callerSessionId = grant.metadata?.["sessionId"];
|
|
279
|
+
if (typeof callerSessionId !== "string") return null;
|
|
280
|
+
const actor = await requireLiveAgentAttemptAuthorization(db, grant, callerSessionId);
|
|
281
|
+
return actor.initiatingHumanSubjectId;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The scoped seam's refusal for a subject that is an organization member but
|
|
285
|
+
* has no current access to the session's workspace. Other 42501 failures
|
|
286
|
+
* (grants, capability rows) remain errors. */
|
|
287
|
+
function isWorkspaceAccessRefusal(error: unknown): boolean {
|
|
288
|
+
if (nestedPostgresSqlState(error) !== "42501") return false;
|
|
289
|
+
let current: unknown = error;
|
|
290
|
+
for (let depth = 0; depth < 8 && current instanceof Error; depth += 1) {
|
|
291
|
+
if (current.message.includes("lacks current workspace access")) return true;
|
|
292
|
+
current = current.cause;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The exact frozen Sandbox Environment version a session is bound to, resolved
|
|
299
|
+
* for a direct attach (terminal, Files, desktop viewer, Browser, Computer).
|
|
300
|
+
*
|
|
301
|
+
* A version homed in the session's own workspace resolves physically under the
|
|
302
|
+
* session access the route already enforced, exactly as before. An
|
|
303
|
+
* organization Sandbox Environment, or a personal one used outside its home
|
|
304
|
+
* workspace, keeps its versions in that home workspace, so those resolve
|
|
305
|
+
* through the same scoped authority the attach Variable Set defaults and
|
|
306
|
+
* scheduled tasks use: same organization, active, and visible to the attaching
|
|
307
|
+
* subject in the session's workspace.
|
|
308
|
+
*/
|
|
309
|
+
async function resolveSessionRigVersion(
|
|
310
|
+
db: Database,
|
|
311
|
+
session: Pick<Session, "accountId" | "workspaceId"> & { rigId: string; rigVersionId: string },
|
|
312
|
+
authority: SessionAttachRigAuthority,
|
|
313
|
+
): Promise<RigVersion | null> {
|
|
314
|
+
const local = await getRigVersion(db, session.workspaceId, session.rigId, session.rigVersionId);
|
|
315
|
+
if (local) return local;
|
|
316
|
+
const subjectId = await sessionAttachRigSubjectId(db, authority);
|
|
317
|
+
let scoped: Awaited<ReturnType<typeof getScheduledScopedRigVersionMetadata>>;
|
|
318
|
+
try {
|
|
319
|
+
scoped = await getScheduledScopedRigVersionMetadata(
|
|
320
|
+
db,
|
|
321
|
+
{
|
|
322
|
+
accountId: session.accountId,
|
|
323
|
+
workspaceId: session.workspaceId,
|
|
324
|
+
subjectId: subjectId ?? SESSION_ATTACH_SUBJECT,
|
|
325
|
+
},
|
|
326
|
+
session.rigId,
|
|
327
|
+
session.rigVersionId,
|
|
328
|
+
);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (!isWorkspaceAccessRefusal(error)) throw error;
|
|
331
|
+
console.warn("[session-attach] sandbox environment refused without workspace access", {
|
|
332
|
+
workspaceId: session.workspaceId,
|
|
333
|
+
rigId: session.rigId,
|
|
334
|
+
rigVersionId: session.rigVersionId,
|
|
335
|
+
});
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return scoped?.version.rigId === session.rigId ? scoped.version : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
275
341
|
export async function resolveSessionSandboxRuntime(
|
|
276
342
|
db: Database,
|
|
277
343
|
settings: Settings,
|
|
278
|
-
session: Pick<Session, "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">,
|
|
344
|
+
session: Pick<Session, "accountId" | "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">,
|
|
345
|
+
/** Who drives the attach; pass the route grant whenever one exists. */
|
|
346
|
+
authority: SessionAttachRigAuthority,
|
|
279
347
|
): Promise<SessionSandboxRuntime> {
|
|
280
|
-
const
|
|
281
|
-
resolveWorkspaceLegacyRuntimePacks(db, session.workspaceId),
|
|
348
|
+
const rigVersion =
|
|
282
349
|
session.rigId && session.rigVersionId
|
|
283
|
-
?
|
|
284
|
-
|
|
285
|
-
|
|
350
|
+
? await resolveSessionRigVersion(
|
|
351
|
+
db,
|
|
352
|
+
{ ...session, rigId: session.rigId, rigVersionId: session.rigVersionId },
|
|
353
|
+
authority,
|
|
354
|
+
)
|
|
355
|
+
: null;
|
|
286
356
|
if (session.rigVersionId && !rigVersion) {
|
|
287
|
-
throw new
|
|
357
|
+
throw new HTTPException(403, {
|
|
358
|
+
message: "This session's Sandbox Environment version is not available.",
|
|
359
|
+
});
|
|
288
360
|
}
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
361
|
+
// Setup and checks layer on the deployment image, or on the workspace's
|
|
362
|
+
// allowlisted selection of one.
|
|
363
|
+
const logicalSettings = sandboxImageAllowlist(settings).length
|
|
364
|
+
? settingsWithWorkspaceSandboxImage(
|
|
365
|
+
settings,
|
|
366
|
+
(await getWorkspace(db, session.workspaceId))?.settings,
|
|
367
|
+
session.sandboxBackend,
|
|
368
|
+
)
|
|
369
|
+
: settings;
|
|
296
370
|
return {
|
|
297
371
|
settings: {
|
|
298
372
|
...logicalSettings,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
canonicalSessionAuthorizationOperation,
|
|
2
3
|
SessionAuthorizationActor,
|
|
3
4
|
SessionAuthorizationDecision,
|
|
4
5
|
SessionAuthorizationListScope,
|
|
@@ -10,8 +11,11 @@ import {
|
|
|
10
11
|
type SessionAuthorizationTarget,
|
|
11
12
|
type SessionScopeSubjectId,
|
|
12
13
|
} from "@opengeni/contracts";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
15
|
import {
|
|
16
|
+
currentSessionRlsActorIdentityKey,
|
|
14
17
|
getSessionAuthorityProjection,
|
|
18
|
+
getSessionAuthorizationTargetProjection,
|
|
15
19
|
getSession,
|
|
16
20
|
getSessionTurnForAttempt,
|
|
17
21
|
getSlackInteractionSessionAccessForSession,
|
|
@@ -20,9 +24,86 @@ import {
|
|
|
20
24
|
type SessionRlsActorContext,
|
|
21
25
|
} from "@opengeni/db";
|
|
22
26
|
import type { AppDependencies } from "./dependencies";
|
|
27
|
+
import {
|
|
28
|
+
currentSiteSessionOrigin,
|
|
29
|
+
siteOriginsMatch,
|
|
30
|
+
siteSessionOriginFromMetadata,
|
|
31
|
+
} from "./site-session-origin";
|
|
32
|
+
import { siteAgentMayReachSession } from "./site-agent-clamp";
|
|
23
33
|
|
|
24
34
|
export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
|
|
25
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Identical caller-session and attempt reads shared by the authorization
|
|
38
|
+
* checks of one first-party MCP request. Phases bound reuse to checks that run
|
|
39
|
+
* back to back: the route's own checks, then only checks a tool handler starts
|
|
40
|
+
* synchronously on entry. Anything later (a re-check after a wait or a write)
|
|
41
|
+
* reads fresh. Results are keyed by database handle and session RLS actor, so
|
|
42
|
+
* a read is only reused under the exact visibility it was made with.
|
|
43
|
+
*/
|
|
44
|
+
type SessionAuthorizationReads = {
|
|
45
|
+
phase: "route" | "handoff" | "dispatch" | "closed";
|
|
46
|
+
readonly results: WeakMap<Database, Map<string, unknown>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sessionAuthorizationReads = new AsyncLocalStorage<SessionAuthorizationReads>();
|
|
50
|
+
|
|
51
|
+
/** Scope read reuse to one request; it never outlives `fn`. */
|
|
52
|
+
export async function withSessionAuthorizationReadReuse<T>(
|
|
53
|
+
fn: (reuse: { handOffToToolDispatch(): void }) => Promise<T>,
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
const reads: SessionAuthorizationReads = { phase: "route", results: new WeakMap() };
|
|
56
|
+
const handOffToToolDispatch = () => {
|
|
57
|
+
if (reads.phase === "route") reads.phase = "handoff";
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
return await sessionAuthorizationReads.run(reads, () => fn({ handOffToToolDispatch }));
|
|
61
|
+
} finally {
|
|
62
|
+
reads.phase = "closed";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Invoke a tool handler so that authorization checks it starts before its
|
|
68
|
+
* first await reuse the route's reads. Later checks, and every other dispatch
|
|
69
|
+
* in the same request, read fresh.
|
|
70
|
+
*/
|
|
71
|
+
export function dispatchWithSessionAuthorizationReadReuse<T>(handler: () => T): T {
|
|
72
|
+
const reads = sessionAuthorizationReads.getStore();
|
|
73
|
+
if (reads?.phase !== "handoff") return handler();
|
|
74
|
+
reads.phase = "dispatch";
|
|
75
|
+
try {
|
|
76
|
+
return handler();
|
|
77
|
+
} finally {
|
|
78
|
+
reads.phase = "closed";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Call before the first await, so the phase is the one the check started in. */
|
|
83
|
+
function claimSessionAuthorizationReads(): SessionAuthorizationReads | null {
|
|
84
|
+
const reads = sessionAuthorizationReads.getStore();
|
|
85
|
+
return reads?.phase === "route" || reads?.phase === "dispatch" ? reads : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function reusableAuthorizationRead<T>(
|
|
89
|
+
reads: SessionAuthorizationReads | null,
|
|
90
|
+
db: Database,
|
|
91
|
+
key: readonly string[],
|
|
92
|
+
read: () => Promise<T>,
|
|
93
|
+
): Promise<T> {
|
|
94
|
+
if (!reads) return await read();
|
|
95
|
+
const scopedKey = JSON.stringify([...key, currentSessionRlsActorIdentityKey()]);
|
|
96
|
+
let results = reads.results.get(db);
|
|
97
|
+
if (results?.has(scopedKey)) return results.get(scopedKey) as T;
|
|
98
|
+
const value = await read();
|
|
99
|
+
if (!results) {
|
|
100
|
+
results = new Map();
|
|
101
|
+
reads.results.set(db, results);
|
|
102
|
+
}
|
|
103
|
+
results.set(scopedKey, value);
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
26
107
|
/** Maximum time an omitted host hint leaves a live session stream unchecked. */
|
|
27
108
|
export const SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS = 15_000;
|
|
28
109
|
|
|
@@ -120,7 +201,6 @@ export function agentAccessListScopeForViewer(
|
|
|
120
201
|
|
|
121
202
|
type ResolvedSessionAuthorizationTarget = {
|
|
122
203
|
target: SessionAuthorizationTarget;
|
|
123
|
-
parentSessionId: string | null;
|
|
124
204
|
};
|
|
125
205
|
|
|
126
206
|
/**
|
|
@@ -178,7 +258,8 @@ export async function requireLiveAgentAttemptAuthorization(
|
|
|
178
258
|
grant: AccessGrant,
|
|
179
259
|
callerSessionId: string,
|
|
180
260
|
): Promise<Extract<SessionAuthorizationActor, { kind: "agent_attempt" }>> {
|
|
181
|
-
const
|
|
261
|
+
const reads = claimSessionAuthorizationReads();
|
|
262
|
+
const { actor } = await resolveSessionAuthorizationActor(db, grant, reads);
|
|
182
263
|
if (actor.kind !== "agent_attempt" || actor.callerSessionId !== callerSessionId) {
|
|
183
264
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
184
265
|
}
|
|
@@ -205,16 +286,42 @@ export async function requireSessionAuthorization(
|
|
|
205
286
|
surface: SessionAuthorizationSurface;
|
|
206
287
|
},
|
|
207
288
|
): Promise<ResolvedSessionAuthorization | null> {
|
|
289
|
+
const reads = claimSessionAuthorizationReads();
|
|
208
290
|
const port = deps.sessionAuthorization;
|
|
209
291
|
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
292
|
+
// Callers can supply a transaction handle. Both reads open nested RLS
|
|
293
|
+
// savepoints, which must not race on the same PostgreSQL connection.
|
|
294
|
+
const slackAccess = await getSlackInteractionSessionAccessForSession(deps.db, {
|
|
295
|
+
accountId: grant.accountId,
|
|
296
|
+
workspaceId: grant.workspaceId,
|
|
297
|
+
sessionId: input.sessionId,
|
|
298
|
+
});
|
|
299
|
+
const authority = await getSessionAuthorityProjection(
|
|
300
|
+
deps.db,
|
|
301
|
+
grant.workspaceId,
|
|
302
|
+
input.sessionId,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// The projection is read under the caller's own RLS context, so a missing
|
|
306
|
+
// session and one the caller may not see (another member's private session)
|
|
307
|
+
// are the same answer. Every request-facing surface refuses both here,
|
|
308
|
+
// before a route-specific read can observe the difference or fail on the
|
|
309
|
+
// absent row. In-process `core` callers keep the standalone null result and
|
|
310
|
+
// their own not-found handling.
|
|
311
|
+
if (!authority && input.surface !== "core") {
|
|
312
|
+
throw new SessionAuthorizationDeniedError("not_found");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// A request from a Site page (an HTTP frame request, or a tool the page
|
|
316
|
+
// called) may act on a session only when that same Site, or for a frame
|
|
317
|
+
// without a published Site the same mounted frame, started it. Anything
|
|
318
|
+
// else is indistinguishable from a missing session.
|
|
319
|
+
const siteOrigin = currentSiteSessionOrigin();
|
|
320
|
+
if (siteOrigin && authority && !input.operation.endsWith(".read")) {
|
|
321
|
+
const session = await getSession(deps.db, grant.workspaceId, input.sessionId);
|
|
322
|
+
if (!siteOriginsMatch(siteOrigin, siteSessionOriginFromMetadata(session?.metadata)))
|
|
323
|
+
throw new SessionAuthorizationDeniedError("not_found");
|
|
324
|
+
}
|
|
218
325
|
|
|
219
326
|
// Preserve the standalone workspace-shared path. Private sessions continue
|
|
220
327
|
// through the durable actor and ownership checks even without a host port.
|
|
@@ -228,10 +335,13 @@ export async function requireSessionAuthorization(
|
|
|
228
335
|
}
|
|
229
336
|
if (!authority) throw new SessionAuthorizationDeniedError("not_found");
|
|
230
337
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
338
|
+
const resolvedActor = await resolveSessionAuthorizationActor(deps.db, grant, reads);
|
|
339
|
+
const resolvedTarget = await resolveSessionAuthorizationTarget(
|
|
340
|
+
deps.db,
|
|
341
|
+
grant,
|
|
342
|
+
input.sessionId,
|
|
343
|
+
reads,
|
|
344
|
+
);
|
|
235
345
|
const actor = resolvedActor.actor;
|
|
236
346
|
const target = resolvedTarget.target;
|
|
237
347
|
const agentRelatedSessionAccess =
|
|
@@ -247,6 +357,23 @@ export async function requireSessionAuthorization(
|
|
|
247
357
|
if (actor.kind === "agent_attempt" && input.operation === "session.approval.write") {
|
|
248
358
|
throw new SessionAuthorizationDeniedError("forbidden");
|
|
249
359
|
}
|
|
360
|
+
// An agent started from a Site page acts with that page's reach: it may
|
|
361
|
+
// read, message, steer or control only sessions that act for the same Site
|
|
362
|
+
// (its own lineage, or another session that Site started), never a session
|
|
363
|
+
// a person started elsewhere. Reads count too: a transcript is data the
|
|
364
|
+
// Site page must not reach. Indistinguishable from a missing session.
|
|
365
|
+
if (
|
|
366
|
+
actor.kind === "agent_attempt" &&
|
|
367
|
+
target.sessionId !== actor.callerSessionId &&
|
|
368
|
+
!(await siteAgentMayReachSession(
|
|
369
|
+
deps.db,
|
|
370
|
+
{ accountId: grant.accountId, workspaceId: grant.workspaceId },
|
|
371
|
+
{ sessionId: actor.callerSessionId, turnId: actor.turnId },
|
|
372
|
+
target.sessionId,
|
|
373
|
+
))
|
|
374
|
+
) {
|
|
375
|
+
throw new SessionAuthorizationDeniedError("not_found");
|
|
376
|
+
}
|
|
250
377
|
|
|
251
378
|
if (authority.visibility === "user_private") {
|
|
252
379
|
const allowed =
|
|
@@ -290,12 +417,18 @@ export async function requireSessionAuthorization(
|
|
|
290
417
|
|
|
291
418
|
let rawDecision: unknown;
|
|
292
419
|
try {
|
|
420
|
+
// Adapt once, before the host decision. Never retry a denial under an alias.
|
|
421
|
+
const operation = canonicalSessionAuthorizationOperation(input.operation);
|
|
293
422
|
rawDecision = await port.authorizeSession({
|
|
294
423
|
accountId: grant.accountId,
|
|
295
424
|
workspaceId: grant.workspaceId,
|
|
296
425
|
actor,
|
|
297
426
|
target,
|
|
298
|
-
operation:
|
|
427
|
+
operation:
|
|
428
|
+
operation === "session.model_account.write" &&
|
|
429
|
+
port.capabilities?.modelAccountOperation !== true
|
|
430
|
+
? "session.codex_account.write"
|
|
431
|
+
: operation,
|
|
299
432
|
surface: input.surface,
|
|
300
433
|
});
|
|
301
434
|
} catch (error) {
|
|
@@ -325,10 +458,11 @@ export async function requireSessionAuthorizationListScope(
|
|
|
325
458
|
grant: AccessGrant,
|
|
326
459
|
surface: SessionAuthorizationSurface,
|
|
327
460
|
): Promise<SessionAuthorizationListScope | null> {
|
|
461
|
+
const reads = claimSessionAuthorizationReads();
|
|
328
462
|
const port = deps.sessionAuthorization;
|
|
329
463
|
const isAgentAttempt = grantHasAgentAttemptAuthority(grant);
|
|
330
464
|
if (!port && !isAgentAttempt) return null;
|
|
331
|
-
const { actor, callerAccess } = await resolveSessionAuthorizationActor(deps.db, grant);
|
|
465
|
+
const { actor, callerAccess } = await resolveSessionAuthorizationActor(deps.db, grant, reads);
|
|
332
466
|
// Standalone agents retain compact workspace discovery only while the signed
|
|
333
467
|
// caller attempt is still the exact live attempt, and only within their own
|
|
334
468
|
// declared agent-access reach (migration 0427). The viewer is derived from
|
|
@@ -357,7 +491,7 @@ export async function requireSessionAuthorizationListScope(
|
|
|
357
491
|
if (!parsed.success) {
|
|
358
492
|
throw new SessionAuthorizationUnavailableError({ cause: parsed.error });
|
|
359
493
|
}
|
|
360
|
-
// A host never supplies the viewer:
|
|
494
|
+
// A host never supplies the viewer: Opengeni resolved it above from durable
|
|
361
495
|
// state, so a host-returned value is dropped before the intersection.
|
|
362
496
|
const hostScope: SessionAuthorizationListScope =
|
|
363
497
|
parsed.data.kind === "all"
|
|
@@ -374,19 +508,25 @@ async function resolveSessionAuthorizationTarget(
|
|
|
374
508
|
db: Database,
|
|
375
509
|
grant: AccessGrant,
|
|
376
510
|
sessionId: string,
|
|
511
|
+
reads: SessionAuthorizationReads | null,
|
|
377
512
|
): Promise<ResolvedSessionAuthorizationTarget> {
|
|
378
|
-
const session = await
|
|
513
|
+
const session = await reusableAuthorizationRead(
|
|
514
|
+
reads,
|
|
515
|
+
db,
|
|
516
|
+
["target-identity", grant.workspaceId, sessionId],
|
|
517
|
+
() => getSessionAuthorizationTargetProjection(db, grant.workspaceId, sessionId),
|
|
518
|
+
);
|
|
379
519
|
if (!session || session.accountId !== grant.accountId) {
|
|
380
520
|
throw new SessionAuthorizationDeniedError("not_found");
|
|
381
521
|
}
|
|
382
522
|
return {
|
|
383
523
|
target: { sessionId: session.id, rootSessionId: session.rootSessionId },
|
|
384
|
-
parentSessionId: session.parentSessionId,
|
|
385
524
|
};
|
|
386
525
|
}
|
|
387
526
|
async function resolveSessionAuthorizationActor(
|
|
388
527
|
db: Database,
|
|
389
528
|
grant: AccessGrant,
|
|
529
|
+
reads: SessionAuthorizationReads | null,
|
|
390
530
|
): Promise<ResolvedSessionAuthorizationActor> {
|
|
391
531
|
const callerSessionId = grant.metadata?.["sessionId"];
|
|
392
532
|
const turnId = grant.metadata?.["turnId"];
|
|
@@ -414,10 +554,19 @@ async function resolveSessionAuthorizationActor(
|
|
|
414
554
|
) {
|
|
415
555
|
throw new SessionAuthorizationDeniedError("caller_stale");
|
|
416
556
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
557
|
+
// These scoped reads can also open nested savepoints on a transaction handle.
|
|
558
|
+
const callerSession = await reusableAuthorizationRead(
|
|
559
|
+
reads,
|
|
560
|
+
db,
|
|
561
|
+
["session", grant.workspaceId, callerSessionId],
|
|
562
|
+
() => getSession(db, grant.workspaceId, callerSessionId),
|
|
563
|
+
);
|
|
564
|
+
const turn = await reusableAuthorizationRead(
|
|
565
|
+
reads,
|
|
566
|
+
db,
|
|
567
|
+
["turn", grant.workspaceId, callerSessionId, attemptId],
|
|
568
|
+
() => getSessionTurnForAttempt(db, grant.workspaceId, callerSessionId, attemptId),
|
|
569
|
+
);
|
|
421
570
|
if (
|
|
422
571
|
!callerSession ||
|
|
423
572
|
callerSession.accountId !== grant.accountId ||
|