@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,31 +1,44 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ForkSessionResponse,
|
|
3
|
+
SessionVisibilityChangePreview,
|
|
3
4
|
SessionTenancyCreateCapabilities,
|
|
4
5
|
UpdateSessionVisibilityResponse,
|
|
5
6
|
sessionVisibilityFromPublic,
|
|
6
7
|
sessionVisibilityToPublic,
|
|
7
8
|
type ForkSessionRequest,
|
|
8
9
|
type SessionAuthorizationSurface,
|
|
10
|
+
type SessionVisibility,
|
|
9
11
|
type UpdateSessionVisibilityRequest,
|
|
10
12
|
type VariableSet,
|
|
11
13
|
} from "@opengeni/contracts";
|
|
12
14
|
import {
|
|
15
|
+
assertSessionIsNotImported,
|
|
13
16
|
forkSessionContent,
|
|
17
|
+
getSessionForSubject,
|
|
14
18
|
getRig,
|
|
15
19
|
getPrivateSessionCreatePolicy,
|
|
16
20
|
getSessionEventForSubject,
|
|
21
|
+
inspectSessionVisibilityChange,
|
|
22
|
+
EMPTY_SESSION_VISIBILITY_WORK,
|
|
17
23
|
isRetryableDatabaseTransportFailure,
|
|
18
24
|
nestedPostgresSqlState,
|
|
19
25
|
replayAppliedSessionFork,
|
|
20
|
-
|
|
26
|
+
sessionHasLiveAttempts,
|
|
21
27
|
SessionTenancyAccessError,
|
|
22
28
|
SessionTenancyInvalidRequestError,
|
|
23
|
-
|
|
29
|
+
SessionVisibilityWorkConflictError,
|
|
30
|
+
stopSessionWorkForVisibilityChange,
|
|
24
31
|
transitionSessionVisibility,
|
|
25
32
|
type Database,
|
|
26
33
|
type ForkSessionContentResult,
|
|
34
|
+
type TransitionSessionVisibilityResult,
|
|
27
35
|
} from "@opengeni/db";
|
|
28
36
|
import { publishDurableSessionEvents, type EventBus } from "@opengeni/events";
|
|
37
|
+
import {
|
|
38
|
+
privacyDeploymentLimits,
|
|
39
|
+
requireDeploymentAllowsOnlyMe,
|
|
40
|
+
resolveOrganizationPrivacy,
|
|
41
|
+
} from "./organization-privacy";
|
|
29
42
|
import {
|
|
30
43
|
requirePermission,
|
|
31
44
|
hasVerifiedOwningUserAuthorization,
|
|
@@ -33,7 +46,7 @@ import {
|
|
|
33
46
|
} from "../access";
|
|
34
47
|
import { requireSessionAuthorization } from "../session-authorization";
|
|
35
48
|
import type { AppDependencies } from "../dependencies";
|
|
36
|
-
import {
|
|
49
|
+
import { variableSetsRemovedError } from "../domain/encryption-key";
|
|
37
50
|
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
38
51
|
|
|
39
52
|
type SessionTenancyDependencies = Pick<
|
|
@@ -41,6 +54,18 @@ type SessionTenancyDependencies = Pick<
|
|
|
41
54
|
"db" | "bus" | "sessionAuthorization" | "settings"
|
|
42
55
|
>;
|
|
43
56
|
|
|
57
|
+
type SessionVisibilityDependencies = SessionTenancyDependencies & {
|
|
58
|
+
workflowClient: Pick<AppDependencies["workflowClient"], "wakeSessionWorkflow">;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How long one "make private anyway" request waits for a worker to settle the
|
|
63
|
+
* response it is executing. Past it the request answers `stopping_active_work`
|
|
64
|
+
* and the same idempotency key finishes the change on retry.
|
|
65
|
+
*/
|
|
66
|
+
const STOP_WORK_SETTLE_TIMEOUT_MS = 15_000;
|
|
67
|
+
const STOP_WORK_POLL_INTERVAL_MS = 400;
|
|
68
|
+
|
|
44
69
|
export class SessionTenancyManagedHumanRequiredError extends Error {
|
|
45
70
|
readonly name = "SessionTenancyManagedHumanRequiredError";
|
|
46
71
|
constructor() {
|
|
@@ -82,7 +107,7 @@ export function requireVerifiedOwningUser(
|
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
export async function getManagedHumanSessionCreateCapabilities(
|
|
85
|
-
deps: Pick<SessionTenancyDependencies, "db">,
|
|
110
|
+
deps: Pick<SessionTenancyDependencies, "db" | "settings">,
|
|
86
111
|
authorization: AccessGrantAuthorization,
|
|
87
112
|
workspaceId: string,
|
|
88
113
|
): Promise<SessionTenancyCreateCapabilities> {
|
|
@@ -99,16 +124,22 @@ export async function getManagedHumanSessionCreateCapabilities(
|
|
|
99
124
|
}
|
|
100
125
|
throw error;
|
|
101
126
|
}
|
|
102
|
-
// Mirror the database create fence exactly:
|
|
103
|
-
//
|
|
104
|
-
//
|
|
127
|
+
// Mirror the database create fence exactly: every organization is
|
|
128
|
+
// session-tenancy activated (migration 0611), so a member's own Personal
|
|
129
|
+
// workspace is always private-capable, while a shared organization workspace
|
|
130
|
+
// additionally needs the owner/admin Only-me product setting.
|
|
105
131
|
let activated = false;
|
|
132
|
+
let sharedWorkspace = false;
|
|
106
133
|
try {
|
|
107
134
|
const policy = await getPrivateSessionCreatePolicy(deps.db, {
|
|
108
135
|
workspaceId,
|
|
109
136
|
actorSubjectId: authorization.grant.subjectId,
|
|
110
137
|
});
|
|
111
|
-
|
|
138
|
+
sharedWorkspace = !policy.personalWorkspace;
|
|
139
|
+
// A deployment that turns Only me off narrows every shared workspace.
|
|
140
|
+
activated = policy.personalWorkspace
|
|
141
|
+
? policy.platformAvailable
|
|
142
|
+
: policy.organizationEnabled && privacyDeploymentLimits(deps.settings).onlyMeAllowed;
|
|
112
143
|
} catch (error) {
|
|
113
144
|
// A canonical managed human without an active organization membership or
|
|
114
145
|
// stated workspace authority (legacy managed access) is simply not
|
|
@@ -121,15 +152,22 @@ export async function getManagedHumanSessionCreateCapabilities(
|
|
|
121
152
|
throw error;
|
|
122
153
|
}
|
|
123
154
|
}
|
|
155
|
+
// The organization's default audience applies to shared workspaces where
|
|
156
|
+
// Only me is available; a Personal workspace has nobody else to share with.
|
|
157
|
+
const defaultVisibility =
|
|
158
|
+
activated && sharedWorkspace
|
|
159
|
+
? (await resolveOrganizationPrivacy(deps, authorization.grant.accountId)).defaultVisibility
|
|
160
|
+
: "workspace";
|
|
124
161
|
return SessionTenancyCreateCapabilities.parse({
|
|
125
162
|
activated,
|
|
126
163
|
canCreatePrivate: activated,
|
|
127
164
|
reason: activated ? "available" : "not_activated",
|
|
165
|
+
defaultVisibility,
|
|
128
166
|
});
|
|
129
167
|
}
|
|
130
168
|
|
|
131
169
|
export async function requireManagedHumanPrivateSessionCreate(
|
|
132
|
-
deps: Pick<SessionTenancyDependencies, "db">,
|
|
170
|
+
deps: Pick<SessionTenancyDependencies, "db" | "settings">,
|
|
133
171
|
authorization: AccessGrantAuthorization,
|
|
134
172
|
workspaceId: string,
|
|
135
173
|
): Promise<void> {
|
|
@@ -138,6 +176,10 @@ export async function requireManagedHumanPrivateSessionCreate(
|
|
|
138
176
|
// fence, after keyed replay resolution), so a committed keyed success still
|
|
139
177
|
// replays after the setting is disabled while a fresh key fails closed.
|
|
140
178
|
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:create"]);
|
|
179
|
+
await requireDeploymentAllowsOnlyMe(deps, {
|
|
180
|
+
workspaceId,
|
|
181
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
182
|
+
});
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
async function requireSessionTenancyMutationGate(
|
|
@@ -151,9 +193,6 @@ async function requireSessionTenancyMutationGate(
|
|
|
151
193
|
// missing, shared, or another owner's private session.
|
|
152
194
|
requireVerifiedOwningUser(authorization, workspaceId);
|
|
153
195
|
for (const permission of permissions) requirePermission(authorization.grant, permission);
|
|
154
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
155
|
-
throw new SessionTenancyNotActivatedError();
|
|
156
|
-
}
|
|
157
196
|
}
|
|
158
197
|
|
|
159
198
|
async function publishExactCommittedEvent(
|
|
@@ -244,7 +283,7 @@ async function projectAndPublishSessionFork(
|
|
|
244
283
|
}
|
|
245
284
|
|
|
246
285
|
export async function updateManagedHumanSessionVisibility(
|
|
247
|
-
deps:
|
|
286
|
+
deps: SessionVisibilityDependencies,
|
|
248
287
|
authorization: AccessGrantAuthorization,
|
|
249
288
|
workspaceId: string,
|
|
250
289
|
sessionId: string,
|
|
@@ -258,19 +297,57 @@ export async function updateManagedHumanSessionVisibility(
|
|
|
258
297
|
operation: "session.visibility.write",
|
|
259
298
|
surface: authorizationSurface,
|
|
260
299
|
});
|
|
300
|
+
if (request.visibility === "private") {
|
|
301
|
+
await requireDeploymentAllowsOnlyMe(deps, {
|
|
302
|
+
workspaceId,
|
|
303
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
261
306
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
307
|
+
const actorSubjectId = authorization.grant.subjectId;
|
|
308
|
+
const transition = async () =>
|
|
309
|
+
await runTenancyMutation(
|
|
310
|
+
async () =>
|
|
311
|
+
await transitionSessionVisibility(deps.db, {
|
|
312
|
+
workspaceId,
|
|
313
|
+
sessionId,
|
|
314
|
+
actorSubjectId,
|
|
315
|
+
targetVisibility: sessionVisibilityFromPublic(request.visibility),
|
|
316
|
+
expectedAuthorityEpoch: request.expectedAuthorityEpoch,
|
|
317
|
+
operationKey: request.idempotencyKey,
|
|
318
|
+
...(request.stopActiveWork === true ? { stopActiveWork: true } : {}),
|
|
319
|
+
...(beforeCommit ? { beforeCommit } : {}),
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
let result: TransitionSessionVisibilityResult;
|
|
323
|
+
try {
|
|
324
|
+
result = await transition();
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (
|
|
327
|
+
!(error instanceof SessionVisibilityWorkConflictError) ||
|
|
328
|
+
request.stopActiveWork !== true ||
|
|
329
|
+
error.liveAttemptCount === 0
|
|
330
|
+
) {
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
// A worker is executing a response. Stopping it is asynchronous: durably
|
|
334
|
+
// interrupt it (and cancel everything queued or parked) first, let the
|
|
335
|
+
// worker cancel the turn, then make the same transition.
|
|
336
|
+
await stopLiveSessionWorkForVisibilityChange(deps, {
|
|
337
|
+
workspaceId,
|
|
338
|
+
sessionId,
|
|
339
|
+
actorSubjectId,
|
|
340
|
+
expectedAuthorityEpoch: request.expectedAuthorityEpoch,
|
|
341
|
+
operationKey: `${request.idempotencyKey}:stop-work`,
|
|
342
|
+
});
|
|
343
|
+
result = await transitionAfterLiveWorkSettles(deps, {
|
|
344
|
+
workspaceId,
|
|
345
|
+
sessionId,
|
|
346
|
+
actorSubjectId,
|
|
347
|
+
transition,
|
|
348
|
+
conflict: error,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
274
351
|
const response = UpdateSessionVisibilityResponse.parse({
|
|
275
352
|
operationId: result.operationId,
|
|
276
353
|
eventId: result.eventId,
|
|
@@ -281,16 +358,160 @@ export async function updateManagedHumanSessionVisibility(
|
|
|
281
358
|
replay: result.replay,
|
|
282
359
|
revokedGrantCount: result.revokedGrantCount,
|
|
283
360
|
});
|
|
361
|
+
await publishCommittedEventIds(deps, {
|
|
362
|
+
workspaceId,
|
|
363
|
+
sessionId,
|
|
364
|
+
subjectId: actorSubjectId,
|
|
365
|
+
eventIds: result.settledEventIds,
|
|
366
|
+
});
|
|
284
367
|
await publishExactCommittedEvent(deps, {
|
|
285
368
|
workspaceId,
|
|
286
369
|
sessionId,
|
|
287
|
-
subjectId:
|
|
370
|
+
subjectId: actorSubjectId,
|
|
288
371
|
eventId: response.eventId,
|
|
289
372
|
eventSequence: response.eventSequence,
|
|
290
373
|
});
|
|
374
|
+
await wakeSessionWorkflowAfterVisibilityWork(deps, result.workflowWake);
|
|
291
375
|
return response;
|
|
292
376
|
}
|
|
293
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Owner-only preview for the audience dialog: what a change to `visibility`
|
|
380
|
+
* would stop, and what would still refuse it. Same gates as the change itself,
|
|
381
|
+
* so a non-owner learns nothing a refused change would not tell them.
|
|
382
|
+
*/
|
|
383
|
+
export async function previewManagedHumanSessionVisibilityChange(
|
|
384
|
+
deps: SessionTenancyDependencies,
|
|
385
|
+
authorization: AccessGrantAuthorization,
|
|
386
|
+
workspaceId: string,
|
|
387
|
+
sessionId: string,
|
|
388
|
+
targetVisibility: SessionVisibility,
|
|
389
|
+
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
390
|
+
): Promise<SessionVisibilityChangePreview> {
|
|
391
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:control"]);
|
|
392
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
393
|
+
sessionId,
|
|
394
|
+
operation: "session.visibility.write",
|
|
395
|
+
surface: authorizationSurface,
|
|
396
|
+
});
|
|
397
|
+
const preview = await inspectSessionVisibilityChange(deps.db, {
|
|
398
|
+
workspaceId,
|
|
399
|
+
sessionId,
|
|
400
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
401
|
+
});
|
|
402
|
+
const current = sessionVisibilityToPublic(preview.visibility);
|
|
403
|
+
// Only a change to Only me stops work or drops other people's access;
|
|
404
|
+
// sharing a private chat with the workspace never does either.
|
|
405
|
+
const changesToPrivate = targetVisibility === "private" && current !== "private";
|
|
406
|
+
return SessionVisibilityChangePreview.parse({
|
|
407
|
+
visibility: current,
|
|
408
|
+
targetVisibility,
|
|
409
|
+
authorityEpoch: preview.authorityEpoch,
|
|
410
|
+
work: changesToPrivate ? preview.work : EMPTY_SESSION_VISIBILITY_WORK,
|
|
411
|
+
// Kept for older clients; nothing refuses the change any more.
|
|
412
|
+
blocker: null,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function stopLiveSessionWorkForVisibilityChange(
|
|
417
|
+
deps: SessionVisibilityDependencies,
|
|
418
|
+
input: Parameters<typeof stopSessionWorkForVisibilityChange>[1],
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const stopped = await runTenancyMutation(
|
|
421
|
+
async () => await stopSessionWorkForVisibilityChange(deps.db, input),
|
|
422
|
+
);
|
|
423
|
+
await publishCommittedEventIds(deps, {
|
|
424
|
+
workspaceId: input.workspaceId,
|
|
425
|
+
sessionId: input.sessionId,
|
|
426
|
+
subjectId: input.actorSubjectId,
|
|
427
|
+
eventIds: stopped.eventIds,
|
|
428
|
+
});
|
|
429
|
+
await wakeSessionWorkflowAfterVisibilityWork(deps, stopped.workflowWake);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function transitionAfterLiveWorkSettles(
|
|
433
|
+
deps: SessionVisibilityDependencies,
|
|
434
|
+
input: {
|
|
435
|
+
workspaceId: string;
|
|
436
|
+
sessionId: string;
|
|
437
|
+
actorSubjectId: string;
|
|
438
|
+
transition: () => Promise<TransitionSessionVisibilityResult>;
|
|
439
|
+
conflict: SessionVisibilityWorkConflictError;
|
|
440
|
+
},
|
|
441
|
+
): Promise<TransitionSessionVisibilityResult> {
|
|
442
|
+
const deadline = Date.now() + STOP_WORK_SETTLE_TIMEOUT_MS;
|
|
443
|
+
for (;;) {
|
|
444
|
+
const live = await sessionHasLiveAttempts(deps.db, input);
|
|
445
|
+
if (!live) {
|
|
446
|
+
try {
|
|
447
|
+
return await input.transition();
|
|
448
|
+
} catch (error) {
|
|
449
|
+
// A worker claimed a new attempt between the read and the transition.
|
|
450
|
+
if (!(error instanceof SessionVisibilityWorkConflictError) || error.liveAttemptCount === 0)
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (Date.now() >= deadline) {
|
|
455
|
+
throw new SessionVisibilityWorkConflictError(
|
|
456
|
+
"stopping_active_work",
|
|
457
|
+
input.conflict.work,
|
|
458
|
+
input.conflict.liveAttemptCount,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
await new Promise((resolve) => setTimeout(resolve, STOP_WORK_POLL_INTERVAL_MS));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function publishCommittedEventIds(
|
|
466
|
+
deps: { db: Database; bus: EventBus },
|
|
467
|
+
input: { workspaceId: string; sessionId: string; subjectId: string; eventIds: string[] },
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
for (const eventId of input.eventIds) {
|
|
470
|
+
try {
|
|
471
|
+
const event = await getSessionEventForSubject(
|
|
472
|
+
deps.db,
|
|
473
|
+
input.workspaceId,
|
|
474
|
+
input.subjectId,
|
|
475
|
+
eventId,
|
|
476
|
+
);
|
|
477
|
+
if (!event || event.sessionId !== input.sessionId) continue;
|
|
478
|
+
await publishDurableSessionEvents(deps.bus, input.workspaceId, input.sessionId, [event]);
|
|
479
|
+
} catch {
|
|
480
|
+
// Committed; live fanout is repairable from the durable stream.
|
|
481
|
+
console.warn("[session-tenancy] settled event live publication was deferred to replay", {
|
|
482
|
+
errorClass: "SessionTenancyEventPublishOperationError",
|
|
483
|
+
errorCode: "session_tenancy_event_publish_deferred",
|
|
484
|
+
origin: "core",
|
|
485
|
+
sessionId: input.sessionId,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function wakeSessionWorkflowAfterVisibilityWork(
|
|
492
|
+
deps: SessionVisibilityDependencies,
|
|
493
|
+
wake: TransitionSessionVisibilityResult["workflowWake"],
|
|
494
|
+
): Promise<void> {
|
|
495
|
+
if (!wake) return;
|
|
496
|
+
try {
|
|
497
|
+
await deps.workflowClient.wakeSessionWorkflow({
|
|
498
|
+
accountId: wake.accountId,
|
|
499
|
+
workspaceId: wake.workspaceId,
|
|
500
|
+
sessionId: wake.sessionId,
|
|
501
|
+
workflowId: wake.temporalWorkflowId,
|
|
502
|
+
wakeRevision: wake.wakeRevision,
|
|
503
|
+
interruptionRequested: true,
|
|
504
|
+
});
|
|
505
|
+
} catch {
|
|
506
|
+
// The wake revision committed with the change; the durable outbox retries.
|
|
507
|
+
console.warn("[session-tenancy] immediate workflow wake failed; durable outbox will retry", {
|
|
508
|
+
errorClass: "WorkflowWakeOperationError",
|
|
509
|
+
errorCode: "session_tenancy_wake_failed",
|
|
510
|
+
origin: "core",
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
294
515
|
export async function forkManagedHumanSession(
|
|
295
516
|
deps: SessionTenancyDependencies,
|
|
296
517
|
authorization: AccessGrantAuthorization,
|
|
@@ -303,6 +524,12 @@ export async function forkManagedHumanSession(
|
|
|
303
524
|
"sessions:read",
|
|
304
525
|
"sessions:create",
|
|
305
526
|
]);
|
|
527
|
+
if (request.visibility === "private") {
|
|
528
|
+
await requireDeploymentAllowsOnlyMe(deps, {
|
|
529
|
+
workspaceId,
|
|
530
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
306
533
|
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
307
534
|
const forkInput = {
|
|
308
535
|
sourceWorkspaceId: workspaceId,
|
|
@@ -335,17 +562,9 @@ export async function forkManagedHumanSession(
|
|
|
335
562
|
const resolveRuntimeConfiguration = async () => {
|
|
336
563
|
if (runtimeConfigurationPromise) return await runtimeConfigurationPromise;
|
|
337
564
|
runtimeConfigurationPromise = (async () => {
|
|
565
|
+
// Variable Sets were replaced by vaults (RM-1).
|
|
566
|
+
if (request.variableSetIds?.length) throw variableSetsRemovedError("variableSetIds");
|
|
338
567
|
const variableSets: VariableSet[] = [];
|
|
339
|
-
for (const variableSetId of request.variableSetIds ?? []) {
|
|
340
|
-
variableSets.push(
|
|
341
|
-
await validateVariableSetAttachment(
|
|
342
|
-
{ settings: deps.settings, db: deps.db },
|
|
343
|
-
authorization.grant,
|
|
344
|
-
workspaceId,
|
|
345
|
-
variableSetId,
|
|
346
|
-
),
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
568
|
let rigVersionId: string | null = null;
|
|
350
569
|
if (request.rigId) {
|
|
351
570
|
requirePermission(authorization.grant, "rigs:use");
|
|
@@ -353,14 +572,6 @@ export async function forkManagedHumanSession(
|
|
|
353
572
|
if (!rig?.activeVersion) {
|
|
354
573
|
throw new SessionTenancyInvalidRequestError();
|
|
355
574
|
}
|
|
356
|
-
for (const defaultVariableSetId of new Set(rig.activeVersion.defaultVariableSetIds)) {
|
|
357
|
-
await validateVariableSetAttachment(
|
|
358
|
-
{ settings: deps.settings, db: deps.db },
|
|
359
|
-
authorization.grant,
|
|
360
|
-
workspaceId,
|
|
361
|
-
defaultVariableSetId,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
575
|
rigVersionId = rig.activeVersion.id;
|
|
365
576
|
}
|
|
366
577
|
return {
|
|
@@ -422,6 +633,13 @@ export async function forkManagedHumanSession(
|
|
|
422
633
|
|
|
423
634
|
// Validate all requested runtime resources before committing the independent
|
|
424
635
|
// fork so ordinary authorization failures cannot leave an unusable copy.
|
|
636
|
+
const source = await getSessionForSubject(
|
|
637
|
+
deps.db,
|
|
638
|
+
workspaceId,
|
|
639
|
+
sourceSessionId,
|
|
640
|
+
authorization.grant.subjectId,
|
|
641
|
+
);
|
|
642
|
+
if (source) assertSessionIsNotImported(source);
|
|
425
643
|
const runtimeConfiguration = runtimeSetupRequested ? await resolveRuntimeConfiguration() : null;
|
|
426
644
|
const result = await runTenancyMutation(
|
|
427
645
|
async () =>
|
|
@@ -6,11 +6,8 @@ import type {
|
|
|
6
6
|
} from "@opengeni/contracts";
|
|
7
7
|
import {
|
|
8
8
|
issueSelfUserResourceGrant,
|
|
9
|
-
issueSelfLocalConnectionUseGrant,
|
|
10
9
|
listSelfUserResourceAuthorities,
|
|
11
10
|
revokeSelfUserResourceGrant,
|
|
12
|
-
sessionTenancyProductActivated,
|
|
13
|
-
SessionTenancyNotActivatedError,
|
|
14
11
|
} from "@opengeni/db";
|
|
15
12
|
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
16
13
|
import type { AppDependencies } from "../dependencies";
|
|
@@ -47,9 +44,6 @@ async function requireOwnerProductGate(
|
|
|
47
44
|
// Target-free gates must run before an optional target-session lookup or host
|
|
48
45
|
// callback so rejected principals cannot use grant management as an oracle.
|
|
49
46
|
requireOwnerAuthority(authorization, workspaceId, permissions);
|
|
50
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
51
|
-
throw new SessionTenancyNotActivatedError();
|
|
52
|
-
}
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
function requireOwnerAuthority(
|
|
@@ -76,14 +70,6 @@ export async function listManagedHumanUserResourceAuthorities(
|
|
|
76
70
|
input: { resourceKind: UserResourceKind; cursor?: string | undefined; limit: number },
|
|
77
71
|
) {
|
|
78
72
|
requireOwnerAuthority(authorization, workspaceId, LIST_PERMISSIONS[input.resourceKind]);
|
|
79
|
-
// Before activation there cannot be usable personal-resource authority in
|
|
80
|
-
// this organization. Discovery therefore has an exact empty answer; only
|
|
81
|
-
// issue/revoke and runtime use remain behind the forward-only product gate.
|
|
82
|
-
// This keeps ordinary workspace-owned Rig/Variable Set selection independent
|
|
83
|
-
// from an optional personal-resource product that is not enabled.
|
|
84
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
85
|
-
return { authorities: [], nextCursor: null };
|
|
86
|
-
}
|
|
87
73
|
return await listSelfUserResourceAuthorities(deps.db, {
|
|
88
74
|
accountId: authorization.grant.accountId,
|
|
89
75
|
workspaceId,
|
|
@@ -103,21 +89,7 @@ export async function issueManagedHumanUserResourceGrant(
|
|
|
103
89
|
const modePermissions: Permission[] =
|
|
104
90
|
request.mode === "session" ? ["sessions:control"] : ["sessions:create"];
|
|
105
91
|
if (authorization.canonicalLocalHumanSession) {
|
|
106
|
-
|
|
107
|
-
...ISSUE_PERMISSIONS[request.resourceKind],
|
|
108
|
-
"sessions:create",
|
|
109
|
-
]);
|
|
110
|
-
if (request.resourceKind !== "connection" || request.mode !== "always") {
|
|
111
|
-
throw new SessionTenancyManagedHumanRequiredError();
|
|
112
|
-
}
|
|
113
|
-
return await issueSelfLocalConnectionUseGrant(deps.db, {
|
|
114
|
-
accountId: authorization.grant.accountId,
|
|
115
|
-
workspaceId,
|
|
116
|
-
subjectId: authorization.grant.subjectId,
|
|
117
|
-
authorityId,
|
|
118
|
-
context: request.context,
|
|
119
|
-
workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
|
|
120
|
-
});
|
|
92
|
+
throw new SessionTenancyManagedHumanRequiredError();
|
|
121
93
|
}
|
|
122
94
|
await requireOwnerProductGate(deps, authorization, workspaceId, [
|
|
123
95
|
...ISSUE_PERMISSIONS[request.resourceKind],
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeConfiguredModelId,
|
|
3
|
+
configuredStaticUsageLimits,
|
|
4
|
+
type Settings,
|
|
5
|
+
} from "@opengeni/config";
|
|
6
|
+
import type { EntitlementsPort } from "@opengeni/contracts";
|
|
7
|
+
import { coreRoutedSubscriptionFunding, modelFundingForAdmission } from "./limits";
|
|
8
|
+
import {
|
|
9
|
+
checkWorkspaceAllowance,
|
|
10
|
+
getSpendableCreditBalance,
|
|
11
|
+
isCodexBilledTurn,
|
|
12
|
+
sumUsageQuantity,
|
|
13
|
+
type Database,
|
|
14
|
+
} from "@opengeni/db";
|
|
15
|
+
|
|
16
|
+
export type AgentRunAdmissionDenial =
|
|
17
|
+
| "insufficient_credits"
|
|
18
|
+
| "allowance_exhausted"
|
|
19
|
+
| "monthly_model_cost_limit"
|
|
20
|
+
| "monthly_agent_run_limit";
|
|
21
|
+
|
|
22
|
+
/** Read-only admission shared by service-authored runs and goal Resume. */
|
|
23
|
+
export async function agentRunAdmissionDenial(
|
|
24
|
+
services: { db: Database; settings: Settings; entitlements?: EntitlementsPort | null },
|
|
25
|
+
input: {
|
|
26
|
+
accountId: string;
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
model: string;
|
|
29
|
+
requestedAgentRuns: number;
|
|
30
|
+
/** The accepted work's causal human, not the scheduler/service caller. */
|
|
31
|
+
initiatingHumanSubjectId?: string | null;
|
|
32
|
+
acceptedTurn?: { sessionId: string; turnId: string };
|
|
33
|
+
},
|
|
34
|
+
): Promise<AgentRunAdmissionDenial | null> {
|
|
35
|
+
const codexBilled = await isCodexBilledTurn({
|
|
36
|
+
db: services.db,
|
|
37
|
+
settings: services.settings,
|
|
38
|
+
workspaceId: input.workspaceId,
|
|
39
|
+
model: input.model,
|
|
40
|
+
accountId: input.accountId,
|
|
41
|
+
subjectId: input.initiatingHumanSubjectId ?? null,
|
|
42
|
+
...(input.acceptedTurn ? { acceptedTurn: input.acceptedTurn } : {}),
|
|
43
|
+
});
|
|
44
|
+
const externallyBilled = modelFundingForAdmission(
|
|
45
|
+
services.settings,
|
|
46
|
+
input.model,
|
|
47
|
+
codexBilled,
|
|
48
|
+
await coreRoutedSubscriptionFunding(services.db, services.settings, {
|
|
49
|
+
accountId: input.accountId,
|
|
50
|
+
workspaceId: input.workspaceId,
|
|
51
|
+
model: input.model,
|
|
52
|
+
subjectId: input.initiatingHumanSubjectId ?? null,
|
|
53
|
+
...(input.acceptedTurn ? { acceptedTurn: input.acceptedTurn } : {}),
|
|
54
|
+
}),
|
|
55
|
+
).fundedWithoutCredits;
|
|
56
|
+
if (
|
|
57
|
+
!externallyBilled &&
|
|
58
|
+
(services.settings.billingMode === "stripe" || services.settings.usageLimitsMode === "managed")
|
|
59
|
+
) {
|
|
60
|
+
if (services.entitlements) {
|
|
61
|
+
const decision = await services.entitlements.admitRun({
|
|
62
|
+
accountId: input.accountId,
|
|
63
|
+
workspaceId: input.workspaceId,
|
|
64
|
+
action: "agent_run:create",
|
|
65
|
+
quantity: input.requestedAgentRuns,
|
|
66
|
+
});
|
|
67
|
+
if (!decision.allowed) return "insufficient_credits";
|
|
68
|
+
} else {
|
|
69
|
+
const balance = await getSpendableCreditBalance(
|
|
70
|
+
services.db,
|
|
71
|
+
input.accountId,
|
|
72
|
+
canonicalizeConfiguredModelId(services.settings, input.model),
|
|
73
|
+
);
|
|
74
|
+
if (balance.balanceMicros <= 0) return "insufficient_credits";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const refusal = await checkWorkspaceAllowance(services.db, {
|
|
78
|
+
accountId: input.accountId,
|
|
79
|
+
workspaceId: input.workspaceId,
|
|
80
|
+
subjectId: input.initiatingHumanSubjectId ?? null,
|
|
81
|
+
...(externallyBilled ? { fundedWithoutCredits: true } : {}),
|
|
82
|
+
});
|
|
83
|
+
if (refusal) return refusal.code;
|
|
84
|
+
if (
|
|
85
|
+
services.settings.usageLimitsMode !== "static" &&
|
|
86
|
+
services.settings.usageLimitsMode !== "managed"
|
|
87
|
+
) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const limits = configuredStaticUsageLimits(services.settings);
|
|
91
|
+
if (!externallyBilled && limits.maxMonthlyCostMicrosPerAccount) {
|
|
92
|
+
const used = await sumUsageQuantity(services.db, {
|
|
93
|
+
accountId: input.accountId,
|
|
94
|
+
eventType: "model.cost",
|
|
95
|
+
since: startOfUtcMonth(),
|
|
96
|
+
});
|
|
97
|
+
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
98
|
+
return "monthly_model_cost_limit";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (limits.maxMonthlyAgentRunsPerWorkspace) {
|
|
102
|
+
const used = await sumUsageQuantity(services.db, {
|
|
103
|
+
workspaceId: input.workspaceId,
|
|
104
|
+
eventType: "agent_run.created",
|
|
105
|
+
since: startOfUtcMonth(),
|
|
106
|
+
});
|
|
107
|
+
if (used + input.requestedAgentRuns > limits.maxMonthlyAgentRunsPerWorkspace) {
|
|
108
|
+
return "monthly_agent_run_limit";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function startOfUtcMonth(): Date {
|
|
115
|
+
const now = new Date();
|
|
116
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
117
|
+
}
|