@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,5 +1,6 @@
|
|
|
1
1
|
import type { EditableArtifactAuthorizationPort } from "../domain/editable-artifacts/ports";
|
|
2
2
|
import type { EditableArtifactService } from "../domain/editable-artifacts/service";
|
|
3
|
+
import type { Database } from "@opengeni/db";
|
|
3
4
|
import {
|
|
4
5
|
EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
|
|
5
6
|
EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
|
|
@@ -54,9 +55,13 @@ import {
|
|
|
54
55
|
type EditableArtifactLiveSnapshot,
|
|
55
56
|
type EditableArtifactLiveTicket,
|
|
56
57
|
type EditableArtifactLiveTicketRecord,
|
|
58
|
+
type EditableArtifactLiveSourceSessionAuthority,
|
|
57
59
|
} from "./types";
|
|
58
60
|
|
|
59
61
|
const textEncoder = new TextEncoder();
|
|
62
|
+
// Source-bound sockets must renew through the product proxy's live host check.
|
|
63
|
+
// Unbound native console sockets retain their existing artifact-only lifecycle.
|
|
64
|
+
const SOURCE_SESSION_LIVE_LEASE_MS = 15_000;
|
|
60
65
|
const strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
61
66
|
|
|
62
67
|
// Shared OGATX is bounded at 5 MiB and whole OGACO at 8 MiB. Transport
|
|
@@ -112,6 +117,13 @@ export type OpenEditableArtifactLiveInput = Readonly<{
|
|
|
112
117
|
resume: EditableArtifactLiveResume;
|
|
113
118
|
sink: EditableArtifactLiveSinkPort;
|
|
114
119
|
signal?: AbortSignal;
|
|
120
|
+
/** API-owned canonical source session authorization, including live revocation. */
|
|
121
|
+
authorizeSourceSession?: (
|
|
122
|
+
ticket: EditableArtifactLiveTicketRecord,
|
|
123
|
+
permission: "read" | "edit",
|
|
124
|
+
transaction?: Database,
|
|
125
|
+
signal?: AbortSignal,
|
|
126
|
+
) => Promise<boolean>;
|
|
115
127
|
}>;
|
|
116
128
|
|
|
117
129
|
export interface EditableArtifactLiveSession {
|
|
@@ -160,6 +172,7 @@ export class EditableArtifactLiveServer {
|
|
|
160
172
|
modality: EditableArtifactModality;
|
|
161
173
|
actor: EditableArtifactActor;
|
|
162
174
|
allowEdit: boolean;
|
|
175
|
+
sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
|
|
163
176
|
}>,
|
|
164
177
|
): Promise<EditableArtifactLiveTicket> {
|
|
165
178
|
return this.ticketAuthority.mint(input);
|
|
@@ -183,6 +196,9 @@ export class EditableArtifactLiveServer {
|
|
|
183
196
|
resume: input.resume,
|
|
184
197
|
sink: input.sink,
|
|
185
198
|
...(input.signal === undefined ? {} : { parentSignal: input.signal }),
|
|
199
|
+
...(input.authorizeSourceSession
|
|
200
|
+
? { authorizeSourceSession: input.authorizeSourceSession }
|
|
201
|
+
: {}),
|
|
186
202
|
});
|
|
187
203
|
await session.start();
|
|
188
204
|
return session;
|
|
@@ -197,6 +213,7 @@ type SessionConstructor = Readonly<{
|
|
|
197
213
|
resume: EditableArtifactLiveResume;
|
|
198
214
|
sink: EditableArtifactLiveSinkPort;
|
|
199
215
|
parentSignal?: AbortSignal;
|
|
216
|
+
authorizeSourceSession?: OpenEditableArtifactLiveInput["authorizeSourceSession"];
|
|
200
217
|
}>;
|
|
201
218
|
|
|
202
219
|
type InFlight = Readonly<{
|
|
@@ -214,6 +231,8 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
214
231
|
private readonly dependencies: EditableArtifactLiveServerDependencies;
|
|
215
232
|
private readonly options: NormalizedOptions;
|
|
216
233
|
private readonly ticket: EditableArtifactLiveTicketRecord;
|
|
234
|
+
private readonly authorizeSourceSession: OpenEditableArtifactLiveInput["authorizeSourceSession"];
|
|
235
|
+
private readonly sourceSessionLeaseExpiresAt: number | null;
|
|
217
236
|
private readonly resume: EditableArtifactLiveResume;
|
|
218
237
|
private readonly sink: EditableArtifactLiveSinkPort;
|
|
219
238
|
private readonly abort = new AbortController();
|
|
@@ -247,6 +266,10 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
247
266
|
this.dependencies = input.dependencies;
|
|
248
267
|
this.options = input.options;
|
|
249
268
|
this.ticket = input.ticket;
|
|
269
|
+
this.authorizeSourceSession = input.authorizeSourceSession;
|
|
270
|
+
this.sourceSessionLeaseExpiresAt = input.ticket.sourceSessionAuthority
|
|
271
|
+
? Date.parse(input.ticket.issuedAt) + SOURCE_SESSION_LIVE_LEASE_MS
|
|
272
|
+
: null;
|
|
250
273
|
this.artifactId = input.ticket.artifactId;
|
|
251
274
|
this.modality = input.ticket.modality;
|
|
252
275
|
this.streamEpoch = input.streamEpoch;
|
|
@@ -268,6 +291,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
268
291
|
async start(): Promise<void> {
|
|
269
292
|
try {
|
|
270
293
|
this.assertOpen();
|
|
294
|
+
// Deadline enforcement owns no work-queue slot and starts before any
|
|
295
|
+
// bootstrap/authorization await. A hung callback cannot retain a socket.
|
|
296
|
+
if (this.sourceSessionLeaseExpiresAt !== null) {
|
|
297
|
+
void this.runSourceSessionLease().catch((error: unknown) => {
|
|
298
|
+
if (!this.abort.signal.aborted) void this.fail(error);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
271
301
|
this.authorizationRevision = await this.requireRead();
|
|
272
302
|
this.assertOpen();
|
|
273
303
|
|
|
@@ -527,6 +557,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
527
557
|
}
|
|
528
558
|
|
|
529
559
|
private assertOpen(): void {
|
|
560
|
+
this.assertSourceSessionLease();
|
|
530
561
|
if (this.closedValue) {
|
|
531
562
|
throw new EditableArtifactLiveError("closed", "Live session is closed");
|
|
532
563
|
}
|
|
@@ -925,6 +956,25 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
925
956
|
scope: this.ticket.scope,
|
|
926
957
|
artifactId: this.artifactId,
|
|
927
958
|
actor: this.ticket.actor,
|
|
959
|
+
...(this.ticket.sourceSessionAuthority
|
|
960
|
+
? {
|
|
961
|
+
authorizeCommit: async (transaction) => {
|
|
962
|
+
this.assertOpen();
|
|
963
|
+
if (
|
|
964
|
+
!this.authorizeSourceSession ||
|
|
965
|
+
!(await this.withBoundedAuthorization((signal) =>
|
|
966
|
+
this.authorizeSourceSession!(this.ticket, "edit", transaction, signal),
|
|
967
|
+
))
|
|
968
|
+
) {
|
|
969
|
+
throw new EditableArtifactLiveError(
|
|
970
|
+
"permission_changed",
|
|
971
|
+
"Editable artifact source session permission changed",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
this.assertOpen();
|
|
975
|
+
},
|
|
976
|
+
}
|
|
977
|
+
: {}),
|
|
928
978
|
request: {
|
|
929
979
|
intentBytes: frame.intentBytes.slice(),
|
|
930
980
|
requestHash: frame.requestHash,
|
|
@@ -1035,11 +1085,22 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1035
1085
|
private async checkPermission(
|
|
1036
1086
|
permission: "read" | "edit",
|
|
1037
1087
|
): Promise<Awaited<ReturnType<EditableArtifactAuthorizationPort["authorize"]>>> {
|
|
1038
|
-
const decision = await this.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1088
|
+
const decision = await this.withBoundedAuthorization(async (signal) => {
|
|
1089
|
+
if (this.ticket.sourceSessionAuthority) {
|
|
1090
|
+
if (
|
|
1091
|
+
!this.authorizeSourceSession ||
|
|
1092
|
+
!(await this.authorizeSourceSession(this.ticket, permission, undefined, signal))
|
|
1093
|
+
) {
|
|
1094
|
+
return Object.freeze({ allowed: false, revision: this.authorizationRevision });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
signal.throwIfAborted();
|
|
1098
|
+
return await this.dependencies.authorization.authorize({
|
|
1099
|
+
scope: this.ticket.scope,
|
|
1100
|
+
artifactId: this.artifactId,
|
|
1101
|
+
actor: this.ticket.actor,
|
|
1102
|
+
permission,
|
|
1103
|
+
});
|
|
1043
1104
|
});
|
|
1044
1105
|
if (permission === "edit" && !this.ticket.allowEdit) {
|
|
1045
1106
|
return Object.freeze({ ...decision, allowed: false });
|
|
@@ -1047,6 +1108,57 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1047
1108
|
return decision;
|
|
1048
1109
|
}
|
|
1049
1110
|
|
|
1111
|
+
private async withBoundedAuthorization<T>(
|
|
1112
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
1113
|
+
): Promise<T> {
|
|
1114
|
+
this.assertSourceSessionLease();
|
|
1115
|
+
const timeout = new AbortController();
|
|
1116
|
+
const signal = AbortSignal.any([this.abort.signal, timeout.signal]);
|
|
1117
|
+
// Native artifact-only sockets retain their authorization timing. Only
|
|
1118
|
+
// source-bound sockets have a renewable product-session lease.
|
|
1119
|
+
if (this.sourceSessionLeaseExpiresAt !== null) {
|
|
1120
|
+
const budget = this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
|
|
1121
|
+
void this.dependencies.scheduler.sleep(budget, signal).then(
|
|
1122
|
+
() =>
|
|
1123
|
+
timeout.abort(
|
|
1124
|
+
new EditableArtifactLiveError("ticket_expired", "Live authorization lease expired", {
|
|
1125
|
+
retryable: true,
|
|
1126
|
+
}),
|
|
1127
|
+
),
|
|
1128
|
+
() => undefined,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await awaitLiveAuthorization(operation(signal), signal);
|
|
1133
|
+
this.assertOpen();
|
|
1134
|
+
return result;
|
|
1135
|
+
} finally {
|
|
1136
|
+
timeout.abort();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private async runSourceSessionLease(): Promise<void> {
|
|
1141
|
+
while (!this.abort.signal.aborted && this.sourceSessionLeaseExpiresAt !== null) {
|
|
1142
|
+
const remaining = this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
|
|
1143
|
+
if (remaining <= 0) {
|
|
1144
|
+
await this.close("ticket_expired");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
await this.dependencies.scheduler.sleep(remaining, this.abort.signal);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
private assertSourceSessionLease(): void {
|
|
1151
|
+
if (
|
|
1152
|
+
this.sourceSessionLeaseExpiresAt !== null &&
|
|
1153
|
+
this.dependencies.clock.now().getTime() >= this.sourceSessionLeaseExpiresAt
|
|
1154
|
+
) {
|
|
1155
|
+
void this.close("ticket_expired");
|
|
1156
|
+
throw new EditableArtifactLiveError("ticket_expired", "Source session lease expired", {
|
|
1157
|
+
retryable: true,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1050
1162
|
private async sendBarrier(): Promise<void> {
|
|
1051
1163
|
if (!this.sentStateHash)
|
|
1052
1164
|
throw new EditableArtifactLiveError("durable_gap", "Bootstrap has no state boundary");
|
|
@@ -1062,6 +1174,7 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1062
1174
|
|
|
1063
1175
|
private async send(frame: EditableArtifactLiveServerFrame): Promise<void> {
|
|
1064
1176
|
if (this.closedValue) return;
|
|
1177
|
+
this.assertSourceSessionLease();
|
|
1065
1178
|
const bytes = estimateFrameBytes(frame);
|
|
1066
1179
|
if (bytes > this.options.maxOutboundFrameBytes) {
|
|
1067
1180
|
throw new EditableArtifactLiveError(
|
|
@@ -1131,9 +1244,21 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1131
1244
|
),
|
|
1132
1245
|
);
|
|
1133
1246
|
while (!this.abort.signal.aborted) {
|
|
1134
|
-
|
|
1247
|
+
const leaseRemaining =
|
|
1248
|
+
this.sourceSessionLeaseExpiresAt === null
|
|
1249
|
+
? tick
|
|
1250
|
+
: this.sourceSessionLeaseExpiresAt - this.dependencies.clock.now().getTime();
|
|
1251
|
+
if (leaseRemaining <= 0) {
|
|
1252
|
+
await this.close("ticket_expired");
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
await this.dependencies.scheduler.sleep(Math.min(tick, leaseRemaining), this.abort.signal);
|
|
1135
1256
|
if (this.abort.signal.aborted || this.closedValue) return;
|
|
1136
1257
|
const now = this.dependencies.clock.now().getTime();
|
|
1258
|
+
if (this.sourceSessionLeaseExpiresAt !== null && now >= this.sourceSessionLeaseExpiresAt) {
|
|
1259
|
+
await this.close("ticket_expired");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1137
1262
|
if (this.oldestInFlightAt !== 0 && now - this.oldestInFlightAt >= this.options.ackTimeoutMs) {
|
|
1138
1263
|
await this.enqueue(() => this.requireResync("slow_client", this.targetHead));
|
|
1139
1264
|
return;
|
|
@@ -1177,6 +1302,29 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
|
|
|
1177
1302
|
}
|
|
1178
1303
|
}
|
|
1179
1304
|
|
|
1305
|
+
/** Settle callers on cancellation even when an embedding callback ignores it. */
|
|
1306
|
+
function awaitLiveAuthorization<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
1307
|
+
return new Promise<T>((resolve, reject) => {
|
|
1308
|
+
const abort = () => {
|
|
1309
|
+
signal.removeEventListener("abort", abort);
|
|
1310
|
+
reject(signal.reason);
|
|
1311
|
+
};
|
|
1312
|
+
operation.then(
|
|
1313
|
+
(value) => {
|
|
1314
|
+
signal.removeEventListener("abort", abort);
|
|
1315
|
+
if (signal.aborted) reject(signal.reason);
|
|
1316
|
+
else resolve(value);
|
|
1317
|
+
},
|
|
1318
|
+
(error: unknown) => {
|
|
1319
|
+
signal.removeEventListener("abort", abort);
|
|
1320
|
+
reject(error);
|
|
1321
|
+
},
|
|
1322
|
+
);
|
|
1323
|
+
if (signal.aborted) abort();
|
|
1324
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1180
1328
|
function decodeClientFrame(
|
|
1181
1329
|
bytes: Uint8Array,
|
|
1182
1330
|
artifactId: EditableArtifactId,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { AccessGrant } from "@opengeni/contracts";
|
|
2
|
+
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
1
5
|
import type { EditableArtifactAuthorizationPort } from "../domain/editable-artifacts/ports";
|
|
2
6
|
import {
|
|
3
7
|
assertIsoTimestamp,
|
|
@@ -19,11 +23,22 @@ import {
|
|
|
19
23
|
EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
|
|
20
24
|
type EditableArtifactLiveTicket,
|
|
21
25
|
type EditableArtifactLiveTicketRecord,
|
|
26
|
+
type EditableArtifactLiveSourceSessionAuthority,
|
|
22
27
|
} from "./types";
|
|
23
28
|
|
|
24
29
|
const DEFAULT_TTL_MS = 30_000;
|
|
25
30
|
const MAX_TTL_MS = 60_000;
|
|
26
31
|
const MAX_TOKEN_BYTES = 4_096;
|
|
32
|
+
const SOURCE_AUTHORITY_MARKER = ".ogs1.";
|
|
33
|
+
const COMPRESSED_SOURCE_AUTHORITY_MARKER = ".ogs2.";
|
|
34
|
+
const MAX_SOURCE_AUTHORITY_BYTES = 16 * 1024;
|
|
35
|
+
const SourceSessionAuthority = z
|
|
36
|
+
.object({
|
|
37
|
+
sessionId: z.string().uuid(),
|
|
38
|
+
grant: AccessGrant,
|
|
39
|
+
externalContinuation: ExternalActorContinuation.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
27
42
|
|
|
28
43
|
export type EditableArtifactLiveTicketAuthorityDependencies = Readonly<{
|
|
29
44
|
authorization: EditableArtifactAuthorizationPort;
|
|
@@ -52,6 +67,7 @@ export class EditableArtifactLiveTicketAuthority {
|
|
|
52
67
|
modality: EditableArtifactModality;
|
|
53
68
|
actor: EditableArtifactActor;
|
|
54
69
|
allowEdit: boolean;
|
|
70
|
+
sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
|
|
55
71
|
}>,
|
|
56
72
|
): Promise<EditableArtifactLiveTicket> {
|
|
57
73
|
const scope = editableArtifactScope(input.scope);
|
|
@@ -74,7 +90,28 @@ export class EditableArtifactLiveTicketAuthority {
|
|
|
74
90
|
"Editable artifact read permission denied",
|
|
75
91
|
);
|
|
76
92
|
}
|
|
77
|
-
const
|
|
93
|
+
const sourceSessionAuthority = input.sourceSessionAuthority
|
|
94
|
+
? SourceSessionAuthority.parse(input.sourceSessionAuthority)
|
|
95
|
+
: undefined;
|
|
96
|
+
if (sourceSessionAuthority) {
|
|
97
|
+
assertSourceSessionAuthority(sourceSessionAuthority, scope, actor);
|
|
98
|
+
}
|
|
99
|
+
const sourceBytes = sourceSessionAuthority
|
|
100
|
+
? Buffer.from(JSON.stringify(sourceSessionAuthority))
|
|
101
|
+
: null;
|
|
102
|
+
if (sourceBytes && sourceBytes.byteLength > MAX_SOURCE_AUTHORITY_BYTES) {
|
|
103
|
+
throw new EditableArtifactLiveError(
|
|
104
|
+
"invalid_ticket",
|
|
105
|
+
"Live ticket source authority is too large",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const nonce = this.dependencies.tokens.randomOpaqueToken();
|
|
109
|
+
// The execute-only PG store persists the digest of this COMPLETE token.
|
|
110
|
+
// Changing/removing the suffix cannot consume its record. Decode only after
|
|
111
|
+
// atomic consume authenticates it, keeping the existing fixed SQL DTO intact.
|
|
112
|
+
const token = sourceBytes
|
|
113
|
+
? `${nonce}${COMPRESSED_SOURCE_AUTHORITY_MARKER}${deflateRawSync(sourceBytes).toString("base64url")}`
|
|
114
|
+
: nonce;
|
|
78
115
|
assertToken(token);
|
|
79
116
|
const tokenDigest = await this.dependencies.tokens.digestOpaqueToken(token);
|
|
80
117
|
assertDigest(tokenDigest);
|
|
@@ -124,6 +161,10 @@ export class EditableArtifactLiveTicketAuthority {
|
|
|
124
161
|
);
|
|
125
162
|
}
|
|
126
163
|
validateTicketRecord(record);
|
|
164
|
+
const sourceSessionAuthority = decodeSourceSessionAuthority(input.token);
|
|
165
|
+
if (sourceSessionAuthority) {
|
|
166
|
+
assertSourceSessionAuthority(sourceSessionAuthority, record.scope, record.actor);
|
|
167
|
+
}
|
|
127
168
|
if (Date.parse(record.expiresAt) <= this.dependencies.clock.now().getTime()) {
|
|
128
169
|
throw new EditableArtifactLiveError("ticket_expired", "Live ticket has expired", {
|
|
129
170
|
retryable: true,
|
|
@@ -145,6 +186,7 @@ export class EditableArtifactLiveTicketAuthority {
|
|
|
145
186
|
...record,
|
|
146
187
|
scope: editableArtifactScope(record.scope),
|
|
147
188
|
actor: Object.freeze({ ...record.actor }) as EditableArtifactActor,
|
|
189
|
+
...(sourceSessionAuthority ? { sourceSessionAuthority } : {}),
|
|
148
190
|
});
|
|
149
191
|
}
|
|
150
192
|
}
|
|
@@ -194,6 +236,49 @@ function validateTicketRecord(record: EditableArtifactLiveTicketRecord): void {
|
|
|
194
236
|
assertIsoTimestamp(record.expiresAt, "ticket expiresAt");
|
|
195
237
|
}
|
|
196
238
|
|
|
239
|
+
function decodeSourceSessionAuthority(
|
|
240
|
+
token: string,
|
|
241
|
+
): EditableArtifactLiveSourceSessionAuthority | undefined {
|
|
242
|
+
const compressed = token.includes(COMPRESSED_SOURCE_AUTHORITY_MARKER);
|
|
243
|
+
const marker = compressed ? COMPRESSED_SOURCE_AUTHORITY_MARKER : SOURCE_AUTHORITY_MARKER;
|
|
244
|
+
const separator = token.lastIndexOf(marker);
|
|
245
|
+
if (separator === -1) return undefined;
|
|
246
|
+
try {
|
|
247
|
+
return SourceSessionAuthority.parse(
|
|
248
|
+
JSON.parse(
|
|
249
|
+
(compressed
|
|
250
|
+
? inflateRawSync(Buffer.from(token.slice(separator + marker.length), "base64url"), {
|
|
251
|
+
maxOutputLength: MAX_SOURCE_AUTHORITY_BYTES,
|
|
252
|
+
})
|
|
253
|
+
: Buffer.from(token.slice(separator + marker.length), "base64url")
|
|
254
|
+
).toString("utf8"),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
} catch {
|
|
258
|
+
throw new EditableArtifactLiveError(
|
|
259
|
+
"invalid_ticket",
|
|
260
|
+
"Live ticket source authority is invalid",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function assertSourceSessionAuthority(
|
|
266
|
+
authority: EditableArtifactLiveSourceSessionAuthority,
|
|
267
|
+
scope: EditableArtifactScope,
|
|
268
|
+
actor: EditableArtifactActor,
|
|
269
|
+
): void {
|
|
270
|
+
if (
|
|
271
|
+
authority.grant.accountId !== scope.accountId ||
|
|
272
|
+
authority.grant.workspaceId !== scope.workspaceId ||
|
|
273
|
+
authority.grant.subjectId !== actor.subjectId
|
|
274
|
+
) {
|
|
275
|
+
throw new EditableArtifactLiveError(
|
|
276
|
+
"invalid_ticket",
|
|
277
|
+
"Live ticket source scope does not match",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
197
282
|
function assertModality(value: EditableArtifactModality): EditableArtifactModality {
|
|
198
283
|
if (value !== "document" && value !== "spreadsheet" && value !== "presentation") {
|
|
199
284
|
throw new TypeError("ticket modality is invalid");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { EDITABLE_ARTIFACT_LIVE_WIRE_VERSION } from "@opengeni/contracts/editable-artifact-live";
|
|
2
|
+
import type { AccessGrant } from "@opengeni/contracts";
|
|
3
|
+
import type { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
2
4
|
import type {
|
|
3
5
|
EditableArtifactActor,
|
|
4
6
|
EditableArtifactCausalFrontier,
|
|
@@ -14,6 +16,13 @@ import type {
|
|
|
14
16
|
|
|
15
17
|
export const EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION = EDITABLE_ARTIFACT_LIVE_WIRE_VERSION;
|
|
16
18
|
|
|
19
|
+
/** Authenticated by the digest of the complete one-use token, never client claims. */
|
|
20
|
+
export type EditableArtifactLiveSourceSessionAuthority = Readonly<{
|
|
21
|
+
sessionId: string;
|
|
22
|
+
grant: AccessGrant;
|
|
23
|
+
externalContinuation?: ExternalActorContinuation | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
17
26
|
export type EditableArtifactLiveTicket = Readonly<{
|
|
18
27
|
artifactId: EditableArtifactId;
|
|
19
28
|
modality: EditableArtifactModality;
|
|
@@ -30,6 +39,7 @@ export type EditableArtifactLiveTicketRecord = Readonly<{
|
|
|
30
39
|
artifactId: EditableArtifactId;
|
|
31
40
|
modality: EditableArtifactModality;
|
|
32
41
|
actor: EditableArtifactActor;
|
|
42
|
+
sourceSessionAuthority?: EditableArtifactLiveSourceSessionAuthority;
|
|
33
43
|
/** Delegated ceiling captured by the authenticated ticket-minting request. */
|
|
34
44
|
allowEdit: boolean;
|
|
35
45
|
protocolVersion: number;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isModelAvailableForNewSelection,
|
|
3
|
+
policyChargesCreditsForModel,
|
|
4
|
+
policyProviderIdForModel,
|
|
5
|
+
resolveModelProvider,
|
|
6
|
+
runnableLatencyModesForModel,
|
|
7
|
+
withCodexCatalogProvider,
|
|
8
|
+
withXaiSubscriptionCatalogProvider,
|
|
9
|
+
type Settings,
|
|
10
|
+
} from "@opengeni/config";
|
|
11
|
+
import {
|
|
12
|
+
evaluateWorkspaceModelPolicy,
|
|
13
|
+
type GoalAdmissionPausedReason,
|
|
14
|
+
type Session,
|
|
15
|
+
} from "@opengeni/contracts";
|
|
16
|
+
import { isCodexBilledModel } from "@opengeni/codex";
|
|
17
|
+
import {
|
|
18
|
+
checkSessionUsageLimitAdmission,
|
|
19
|
+
getWorkspaceModelPolicy,
|
|
20
|
+
type Database,
|
|
21
|
+
} from "@opengeni/db";
|
|
22
|
+
import { resolveWorkspaceCatalogSettings } from "./model-catalog";
|
|
23
|
+
import { agentRunAdmissionDenial } from "./billing/agent-run-admission";
|
|
24
|
+
|
|
25
|
+
export type GoalAdmissionBlock = { pausedReason: GoalAdmissionPausedReason; message: string };
|
|
26
|
+
|
|
27
|
+
/** Catalog membership never grants access to a connection or changes the selected model. */
|
|
28
|
+
export function goalContinuationModelDecision(input: {
|
|
29
|
+
settings: Settings;
|
|
30
|
+
workspaceModelPolicy: Awaited<ReturnType<typeof getWorkspaceModelPolicy>>;
|
|
31
|
+
inheritedModel: string;
|
|
32
|
+
codexCompactionMode?: Session["codexCompactionMode"];
|
|
33
|
+
latencyMode?: Session["latencyMode"];
|
|
34
|
+
}): { model: string; blocked: string | null; pausedReason?: GoalAdmissionPausedReason } {
|
|
35
|
+
const catalogSettings = input.settings.supergrokSubscriptionEnabled
|
|
36
|
+
? withXaiSubscriptionCatalogProvider(
|
|
37
|
+
input.settings.codexSubscriptionEnabled
|
|
38
|
+
? withCodexCatalogProvider(input.settings)
|
|
39
|
+
: input.settings,
|
|
40
|
+
)
|
|
41
|
+
: input.settings.codexSubscriptionEnabled
|
|
42
|
+
? withCodexCatalogProvider(input.settings)
|
|
43
|
+
: input.settings;
|
|
44
|
+
const model = input.inheritedModel;
|
|
45
|
+
if (!resolveModelProvider(catalogSettings, model)) {
|
|
46
|
+
return {
|
|
47
|
+
model,
|
|
48
|
+
blocked: "The selected model is unavailable. Choose an available model before resuming.",
|
|
49
|
+
pausedReason: "model_unavailable",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!isModelAvailableForNewSelection(catalogSettings, model)) {
|
|
53
|
+
return {
|
|
54
|
+
model,
|
|
55
|
+
blocked: "The selected model has been retired. Choose an available model before resuming.",
|
|
56
|
+
pausedReason: "model_unavailable",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const policyVerdict =
|
|
60
|
+
input.workspaceModelPolicy === null
|
|
61
|
+
? null
|
|
62
|
+
: evaluateWorkspaceModelPolicy(input.workspaceModelPolicy, {
|
|
63
|
+
providerId: policyProviderIdForModel(catalogSettings, model),
|
|
64
|
+
modelId: model,
|
|
65
|
+
chargesCredits: policyChargesCreditsForModel(catalogSettings, model),
|
|
66
|
+
});
|
|
67
|
+
if (policyVerdict && !policyVerdict.allowed) {
|
|
68
|
+
return {
|
|
69
|
+
model,
|
|
70
|
+
blocked:
|
|
71
|
+
policyVerdict.reason === "credits"
|
|
72
|
+
? "Opengeni credits are turned off in this workspace. Choose a model paid by a subscription or API key before resuming."
|
|
73
|
+
: "Workspace policy blocks the selected model. Choose an allowed model before resuming.",
|
|
74
|
+
pausedReason: "model_policy",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (input.codexCompactionMode === "remote_v2" && !isCodexBilledModel(model)) {
|
|
78
|
+
return {
|
|
79
|
+
model,
|
|
80
|
+
blocked: "This conversation requires a Codex model. Choose a Codex model before resuming.",
|
|
81
|
+
pausedReason: "model_policy",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
!runnableLatencyModesForModel(catalogSettings, model).includes(input.latencyMode ?? "standard")
|
|
86
|
+
) {
|
|
87
|
+
return {
|
|
88
|
+
model,
|
|
89
|
+
blocked:
|
|
90
|
+
"The selected model does not support this conversation's latency mode. Choose a supported mode or model before resuming.",
|
|
91
|
+
pausedReason: "model_policy",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { model, blocked: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The same scoped, secret-free catalog used at ordinary turn admission. */
|
|
98
|
+
export async function resolveGoalModelAdmission(
|
|
99
|
+
db: Database,
|
|
100
|
+
settings: Settings,
|
|
101
|
+
input: {
|
|
102
|
+
accountId: string;
|
|
103
|
+
workspaceId: string;
|
|
104
|
+
model: string;
|
|
105
|
+
codexCompactionMode?: Session["codexCompactionMode"];
|
|
106
|
+
latencyMode?: Session["latencyMode"];
|
|
107
|
+
},
|
|
108
|
+
) {
|
|
109
|
+
const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
|
|
110
|
+
accountId: input.accountId,
|
|
111
|
+
workspaceId: input.workspaceId,
|
|
112
|
+
retainedProductModelId: input.model,
|
|
113
|
+
});
|
|
114
|
+
const workspaceModelPolicy = await getWorkspaceModelPolicy(db, input.workspaceId);
|
|
115
|
+
return {
|
|
116
|
+
settings: catalog.settings,
|
|
117
|
+
...goalContinuationModelDecision({
|
|
118
|
+
settings: catalog.settings,
|
|
119
|
+
workspaceModelPolicy,
|
|
120
|
+
inheritedModel: input.model,
|
|
121
|
+
...(input.codexCompactionMode ? { codexCompactionMode: input.codexCompactionMode } : {}),
|
|
122
|
+
...(input.latencyMode ? { latencyMode: input.latencyMode } : {}),
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Shared with scheduled work; reasons describe the actual admission gate. */
|
|
128
|
+
export async function goalRunBudgetBlocked(
|
|
129
|
+
services: Parameters<typeof agentRunAdmissionDenial>[0],
|
|
130
|
+
input: Omit<Parameters<typeof agentRunAdmissionDenial>[1], "requestedAgentRuns">,
|
|
131
|
+
): Promise<GoalAdmissionBlock | null> {
|
|
132
|
+
const denial = await agentRunAdmissionDenial(services, { ...input, requestedAgentRuns: 1 });
|
|
133
|
+
if (denial === null) return null;
|
|
134
|
+
const blocks: Record<NonNullable<typeof denial>, GoalAdmissionBlock> = {
|
|
135
|
+
insufficient_credits: services.entitlements
|
|
136
|
+
? {
|
|
137
|
+
pausedReason: "usage_policy",
|
|
138
|
+
message: "The application's usage policy blocks another run. Resume when it allows.",
|
|
139
|
+
}
|
|
140
|
+
: {
|
|
141
|
+
pausedReason: "credits",
|
|
142
|
+
message: "Insufficient Opengeni credits. Add credits before resuming.",
|
|
143
|
+
},
|
|
144
|
+
allowance_exhausted: {
|
|
145
|
+
pausedReason: "allowance",
|
|
146
|
+
message: "Opengeni usage allowance exhausted. Resume when your allowance is available.",
|
|
147
|
+
},
|
|
148
|
+
monthly_model_cost_limit: {
|
|
149
|
+
pausedReason: "budget",
|
|
150
|
+
message: "Monthly model spending limit reached. Resume when the spending limit allows.",
|
|
151
|
+
},
|
|
152
|
+
monthly_agent_run_limit: {
|
|
153
|
+
pausedReason: "usage_limit",
|
|
154
|
+
message: "Monthly agent run limit reached. Resume when the run limit allows.",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
return blocks[denial];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Per-task usage limit gate for goal continuation and resume. A
|
|
162
|
+
* reached root limit blocks with its own reason; raising or clearing the limit
|
|
163
|
+
* never resumes a goal by itself. A read failure throws (never unlimited).
|
|
164
|
+
*/
|
|
165
|
+
export async function goalSessionUsageLimitBlocked(
|
|
166
|
+
db: Database,
|
|
167
|
+
input: { accountId: string; workspaceId: string; sessionId: string },
|
|
168
|
+
): Promise<GoalAdmissionBlock | null> {
|
|
169
|
+
// Server-internal: the goal's session is already authorized, so a private
|
|
170
|
+
// root is enforced rather than read as missing; a missing tree fails closed.
|
|
171
|
+
const admission = await checkSessionUsageLimitAdmission(db, { ...input, runtime: true });
|
|
172
|
+
if (admission.allowed) return null;
|
|
173
|
+
return { pausedReason: "session_usage_limit", message: admission.refusal.message };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class GoalResumeBlockedError extends Error {
|
|
177
|
+
constructor(
|
|
178
|
+
readonly pausedReason: GoalAdmissionPausedReason,
|
|
179
|
+
message: string,
|
|
180
|
+
) {
|
|
181
|
+
super(message);
|
|
182
|
+
this.name = "GoalResumeBlockedError";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Called under the goal transition's locks, before counters, events or wakes change. */
|
|
187
|
+
export async function assertGoalResumeAllowed(
|
|
188
|
+
services: Parameters<typeof agentRunAdmissionDenial>[0] & { catalogSourceSettings?: Settings },
|
|
189
|
+
session: Pick<Session, "accountId" | "workspaceId" | "model" | "codexCompactionMode"> &
|
|
190
|
+
Partial<Pick<Session, "latencyMode" | "id">>,
|
|
191
|
+
causalTurn: { initiatingHumanSubjectId: string | null } | null,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (session.id) {
|
|
194
|
+
const limit = await goalSessionUsageLimitBlocked(services.db, {
|
|
195
|
+
accountId: session.accountId,
|
|
196
|
+
workspaceId: session.workspaceId,
|
|
197
|
+
sessionId: session.id,
|
|
198
|
+
});
|
|
199
|
+
if (limit) throw new GoalResumeBlockedError(limit.pausedReason, limit.message);
|
|
200
|
+
}
|
|
201
|
+
const decision = await resolveGoalModelAdmission(
|
|
202
|
+
services.db,
|
|
203
|
+
services.catalogSourceSettings ?? services.settings,
|
|
204
|
+
session,
|
|
205
|
+
);
|
|
206
|
+
if (decision.blocked) {
|
|
207
|
+
throw new GoalResumeBlockedError(decision.pausedReason!, decision.blocked);
|
|
208
|
+
}
|
|
209
|
+
const block = await goalRunBudgetBlocked(
|
|
210
|
+
{ ...services, settings: decision.settings },
|
|
211
|
+
{
|
|
212
|
+
accountId: session.accountId,
|
|
213
|
+
workspaceId: session.workspaceId,
|
|
214
|
+
model: decision.model,
|
|
215
|
+
// Resuming does not lend the clicking user's allowance or credentials.
|
|
216
|
+
initiatingHumanSubjectId: causalTurn?.initiatingHumanSubjectId ?? null,
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
if (block) throw new GoalResumeBlockedError(block.pausedReason, block.message);
|
|
220
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A task usage limit the trusted server applies to a root session it creates
|
|
5
|
+
* in this process, as the host actor `subjectId`. Published Site agents use
|
|
6
|
+
* it: site-runtime creates each conversation as `site:<siteId>` through an
|
|
7
|
+
* in-process dispatch, and the per-conversation budget must be in place in the
|
|
8
|
+
* create transaction, before the first model call. Only code in this process
|
|
9
|
+
* can set it; a request cannot.
|
|
10
|
+
*/
|
|
11
|
+
export type HostSessionUsageLimit = {
|
|
12
|
+
/** The service principal the session is created as; other callers are ignored. */
|
|
13
|
+
subjectId: string;
|
|
14
|
+
limitMicros: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const limits = new AsyncLocalStorage<HostSessionUsageLimit>();
|
|
18
|
+
|
|
19
|
+
export function withHostSessionUsageLimit<T>(limit: HostSessionUsageLimit, run: () => T): T {
|
|
20
|
+
return limits.run(limit, run);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function currentHostSessionUsageLimit(): HostSessionUsageLimit | undefined {
|
|
24
|
+
return limits.getStore();
|
|
25
|
+
}
|