@opengeni/core 1.0.1 → 2.1.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/index.d.ts +37 -0
- package/dist/application/new-session-drafts.d.ts +8 -1
- package/dist/application/session-commands.d.ts +18 -11
- package/dist/application/session-tenancy.d.ts +18 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
- package/dist/chunk-H7X52RI2.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
- package/dist/chunk-VCZFFEPB.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
- package/dist/chunk-ZYCNLYGH.js.map +1 -0
- package/dist/dependencies.d.ts +12 -1
- package/dist/domain/automations.d.ts +51 -0
- package/dist/domain/company-brain-governed-writes.d.ts +70 -0
- package/dist/domain/company-profile-agent-admin.d.ts +24 -0
- package/dist/domain/connection-authority.d.ts +97 -0
- package/dist/domain/connection-authority.js +169 -0
- package/dist/domain/connection-authority.js.map +1 -0
- package/dist/domain/editable-artifacts/service.d.ts +1 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/governed-learning-activation.d.ts +16 -0
- package/dist/domain/governed-learning-evaluator.d.ts +6 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/memory-slack-delivery.d.ts +1 -4
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/packs.d.ts +5 -1
- package/dist/domain/personal-connection-delegations.d.ts +34 -2
- package/dist/domain/pr-review.d.ts +149 -0
- package/dist/domain/remember.d.ts +69 -0
- package/dist/domain/resources.d.ts +22 -1
- package/dist/domain/scheduled-tasks.d.ts +43 -2
- package/dist/domain/session-tool-policy.d.ts +1 -1
- package/dist/domain/sessions.d.ts +40 -6
- package/dist/editable-artifact-live/application.d.ts +10 -2
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +0 -1
- package/dist/editable-artifact-live/ticket.d.ts +0 -1
- package/dist/editable-artifact-live/types.d.ts +5 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +2 -4
- package/dist/index.d.ts +13 -1
- package/dist/index.js +4827 -1164
- package/dist/index.js.map +1 -1
- package/dist/rigs/index.d.ts +22 -5
- package/dist/sandbox/fleet.d.ts +34 -6
- package/dist/sandbox/routing.d.ts +15 -1
- package/dist/sandbox/runtime-settings.d.ts +39 -0
- package/package.json +16 -11
- package/src/access/index.ts +151 -31
- package/src/application/new-session-drafts.ts +24 -2
- package/src/application/session-commands.ts +305 -71
- package/src/application/session-tenancy.ts +321 -0
- package/src/application/user-resource-grants.ts +140 -0
- package/src/billing/limits.ts +25 -15
- package/src/dependencies.ts +12 -1
- package/src/domain/automations.ts +196 -0
- package/src/domain/capabilities.ts +7 -1
- package/src/domain/company-brain-governed-writes.ts +330 -0
- package/src/domain/company-profile-agent-admin.ts +55 -0
- package/src/domain/connection-authority.ts +329 -0
- package/src/domain/editable-artifacts/agent-application.ts +17 -9
- package/src/domain/editable-artifacts/service.ts +12 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +16 -3
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/governed-learning-activation.ts +44 -0
- package/src/domain/governed-learning-evaluator.ts +49 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights.ts +9 -0
- package/src/domain/memory-slack-delivery.ts +0 -30
- package/src/domain/organization-membership-lifecycle.ts +9 -0
- package/src/domain/packs.ts +106 -3
- package/src/domain/personal-connection-delegations.ts +532 -51
- package/src/domain/pr-review.ts +671 -0
- package/src/domain/remember.ts +549 -0
- package/src/domain/resources.ts +131 -4
- package/src/domain/scheduled-tasks.ts +497 -76
- package/src/domain/session-tool-policy.ts +12 -9
- package/src/domain/sessions.ts +613 -166
- package/src/editable-artifact-live/application.ts +21 -3
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +13 -7
- package/src/editable-artifact-live/ticket.ts +1 -5
- package/src/editable-artifact-live/types.ts +6 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +13 -1
- package/src/rigs/index.ts +165 -48
- package/src/sandbox/fleet.ts +376 -30
- package/src/sandbox/routing.ts +91 -29
- package/src/sandbox/runtime-settings.ts +274 -0
- package/src/session-authorization.ts +17 -49
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/src/domain/durable-learning-slack-publication.ts +0 -202
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForkSessionResponse,
|
|
3
|
+
SessionTenancyCreateCapabilities,
|
|
4
|
+
UpdateSessionVisibilityResponse,
|
|
5
|
+
sessionVisibilityFromPublic,
|
|
6
|
+
sessionVisibilityToPublic,
|
|
7
|
+
type ForkSessionRequest,
|
|
8
|
+
type SessionAuthorizationSurface,
|
|
9
|
+
type UpdateSessionVisibilityRequest,
|
|
10
|
+
} from "@opengeni/contracts";
|
|
11
|
+
import {
|
|
12
|
+
forkSessionContent,
|
|
13
|
+
getPrivateSessionCreatePolicy,
|
|
14
|
+
getSessionEventForSubject,
|
|
15
|
+
isRetryableDatabaseTransportFailure,
|
|
16
|
+
nestedPostgresSqlState,
|
|
17
|
+
replayAppliedSessionFork,
|
|
18
|
+
sessionTenancyProductActivated,
|
|
19
|
+
SessionTenancyAccessError,
|
|
20
|
+
SessionTenancyNotActivatedError,
|
|
21
|
+
transitionSessionVisibility,
|
|
22
|
+
type Database,
|
|
23
|
+
type ForkSessionContentResult,
|
|
24
|
+
} from "@opengeni/db";
|
|
25
|
+
import { publishDurableSessionEvents, type EventBus } from "@opengeni/events";
|
|
26
|
+
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
27
|
+
import { requireSessionAuthorization } from "../session-authorization";
|
|
28
|
+
import type { AppDependencies } from "../dependencies";
|
|
29
|
+
|
|
30
|
+
type SessionTenancyDependencies = Pick<AppDependencies, "db" | "bus" | "sessionAuthorization">;
|
|
31
|
+
|
|
32
|
+
export class SessionTenancyManagedHumanRequiredError extends Error {
|
|
33
|
+
readonly name = "SessionTenancyManagedHumanRequiredError";
|
|
34
|
+
constructor() {
|
|
35
|
+
super("Session tenancy mutations require the owning managed-human session");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class SessionTenancyPersistenceOutcomeUnknownError extends Error {
|
|
40
|
+
readonly name = "SessionTenancyPersistenceOutcomeUnknownError";
|
|
41
|
+
constructor(options?: ErrorOptions) {
|
|
42
|
+
super("The session tenancy mutation outcome is unknown", options);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function requireCanonicalManagedHuman(
|
|
47
|
+
authorization: AccessGrantAuthorization,
|
|
48
|
+
workspaceId: string,
|
|
49
|
+
): void {
|
|
50
|
+
if (
|
|
51
|
+
!authorization.canonicalManagedHumanSession ||
|
|
52
|
+
!authorization.contextIntegrity ||
|
|
53
|
+
authorization.authenticatedSubjectId !== authorization.grant.subjectId ||
|
|
54
|
+
authorization.grant.workspaceId !== workspaceId
|
|
55
|
+
) {
|
|
56
|
+
throw new SessionTenancyManagedHumanRequiredError();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getManagedHumanSessionCreateCapabilities(
|
|
61
|
+
deps: Pick<SessionTenancyDependencies, "db">,
|
|
62
|
+
authorization: AccessGrantAuthorization,
|
|
63
|
+
workspaceId: string,
|
|
64
|
+
): Promise<SessionTenancyCreateCapabilities> {
|
|
65
|
+
requirePermission(authorization.grant, "sessions:create");
|
|
66
|
+
try {
|
|
67
|
+
requireCanonicalManagedHuman(authorization, workspaceId);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
70
|
+
return SessionTenancyCreateCapabilities.parse({
|
|
71
|
+
activated: false,
|
|
72
|
+
canCreatePrivate: false,
|
|
73
|
+
reason: "managed_session_required",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
// Mirror the database create fence exactly: a managed human's own Personal
|
|
79
|
+
// workspace needs only the operator readiness receipt, while a shared
|
|
80
|
+
// organization workspace additionally needs the owner/admin product setting.
|
|
81
|
+
let activated = false;
|
|
82
|
+
try {
|
|
83
|
+
const policy = await getPrivateSessionCreatePolicy(deps.db, {
|
|
84
|
+
workspaceId,
|
|
85
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
86
|
+
});
|
|
87
|
+
activated = policy.personalWorkspace ? policy.platformAvailable : policy.organizationEnabled;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// A canonical managed human without an active organization membership or
|
|
90
|
+
// stated workspace authority (legacy managed access) is simply not
|
|
91
|
+
// private-ready; the definer's 42501 is a capability answer here, not a
|
|
92
|
+
// failure. Every other error still propagates.
|
|
93
|
+
if (
|
|
94
|
+
!(error instanceof SessionTenancyAccessError) &&
|
|
95
|
+
nestedPostgresSqlState(error) !== "42501"
|
|
96
|
+
) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return SessionTenancyCreateCapabilities.parse({
|
|
101
|
+
activated,
|
|
102
|
+
canCreatePrivate: activated,
|
|
103
|
+
reason: activated ? "available" : "not_activated",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function requireManagedHumanPrivateSessionCreate(
|
|
108
|
+
deps: Pick<SessionTenancyDependencies, "db">,
|
|
109
|
+
authorization: AccessGrantAuthorization,
|
|
110
|
+
workspaceId: string,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
// Target-free readiness preflight only. The owner/admin organization setting
|
|
113
|
+
// is enforced by the create transaction itself (under the organization
|
|
114
|
+
// fence, after keyed replay resolution), so a committed keyed success still
|
|
115
|
+
// replays after the setting is disabled while a fresh key fails closed.
|
|
116
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:create"]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function requireSessionTenancyMutationGate(
|
|
120
|
+
deps: Pick<SessionTenancyDependencies, "db">,
|
|
121
|
+
authorization: AccessGrantAuthorization,
|
|
122
|
+
workspaceId: string,
|
|
123
|
+
permissions: ReadonlyArray<"sessions:read" | "sessions:create" | "sessions:control">,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
// These checks are deliberately target-free. A rejected principal must not
|
|
126
|
+
// cause a session lookup or embedding-host callback that distinguishes a
|
|
127
|
+
// missing, shared, or another owner's private session.
|
|
128
|
+
requireCanonicalManagedHuman(authorization, workspaceId);
|
|
129
|
+
for (const permission of permissions) requirePermission(authorization.grant, permission);
|
|
130
|
+
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
131
|
+
throw new SessionTenancyNotActivatedError();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function publishExactCommittedEvent(
|
|
136
|
+
deps: { db: Database; bus: EventBus },
|
|
137
|
+
input: {
|
|
138
|
+
workspaceId: string;
|
|
139
|
+
sessionId: string;
|
|
140
|
+
subjectId: string;
|
|
141
|
+
eventId: string | null;
|
|
142
|
+
eventSequence: number | null;
|
|
143
|
+
},
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
if (input.eventId === null && input.eventSequence === null) return;
|
|
146
|
+
if (input.eventId === null || input.eventSequence === null) {
|
|
147
|
+
console.error("[session-tenancy] committed event receipt is incomplete", {
|
|
148
|
+
errorClass: "SessionTenancyEventInvariantError",
|
|
149
|
+
errorCode: "session_tenancy_event_receipt_incomplete",
|
|
150
|
+
origin: "core",
|
|
151
|
+
sessionId: input.sessionId,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const event = await getSessionEventForSubject(
|
|
157
|
+
deps.db,
|
|
158
|
+
input.workspaceId,
|
|
159
|
+
input.subjectId,
|
|
160
|
+
input.eventId,
|
|
161
|
+
);
|
|
162
|
+
if (!event || event.sessionId !== input.sessionId || event.sequence !== input.eventSequence) {
|
|
163
|
+
console.error("[session-tenancy] committed event receipt did not resolve exactly", {
|
|
164
|
+
errorClass: "SessionTenancyEventInvariantError",
|
|
165
|
+
errorCode: "session_tenancy_event_receipt_mismatch",
|
|
166
|
+
origin: "core",
|
|
167
|
+
sessionId: input.sessionId,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await publishDurableSessionEvents(deps.bus, input.workspaceId, input.sessionId, [event]);
|
|
172
|
+
} catch {
|
|
173
|
+
// The mutation and event committed atomically before this read. Live fanout
|
|
174
|
+
// is repairable from the durable stream, so a publication-side failure must
|
|
175
|
+
// not turn a known successful mutation into an unknown client outcome.
|
|
176
|
+
console.warn("[session-tenancy] committed event live publication was deferred to replay", {
|
|
177
|
+
errorClass: "SessionTenancyEventPublishOperationError",
|
|
178
|
+
errorCode: "session_tenancy_event_publish_deferred",
|
|
179
|
+
origin: "core",
|
|
180
|
+
sessionId: input.sessionId,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function runTenancyMutation<T>(operation: () => Promise<T>): Promise<T> {
|
|
186
|
+
try {
|
|
187
|
+
return await operation();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (isRetryableDatabaseTransportFailure(error)) {
|
|
190
|
+
throw new SessionTenancyPersistenceOutcomeUnknownError({ cause: error });
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function projectAndPublishSessionFork(
|
|
197
|
+
deps: SessionTenancyDependencies,
|
|
198
|
+
actorSubjectId: string,
|
|
199
|
+
result: ForkSessionContentResult,
|
|
200
|
+
): Promise<ForkSessionResponse> {
|
|
201
|
+
const response = ForkSessionResponse.parse({
|
|
202
|
+
operationId: result.operationId,
|
|
203
|
+
eventId: result.eventId,
|
|
204
|
+
eventSequence: result.eventSequence,
|
|
205
|
+
sessionId: result.sessionId,
|
|
206
|
+
workspaceId: result.workspaceId,
|
|
207
|
+
visibility: sessionVisibilityToPublic(result.visibility),
|
|
208
|
+
authorityEpoch: result.authorityEpoch,
|
|
209
|
+
copiedHistoryItemCount: result.copiedHistoryItemCount,
|
|
210
|
+
replay: result.replay,
|
|
211
|
+
});
|
|
212
|
+
await publishExactCommittedEvent(deps, {
|
|
213
|
+
workspaceId: response.workspaceId,
|
|
214
|
+
sessionId: response.sessionId,
|
|
215
|
+
subjectId: actorSubjectId,
|
|
216
|
+
eventId: response.eventId,
|
|
217
|
+
eventSequence: response.eventSequence,
|
|
218
|
+
});
|
|
219
|
+
return response;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function updateManagedHumanSessionVisibility(
|
|
223
|
+
deps: SessionTenancyDependencies,
|
|
224
|
+
authorization: AccessGrantAuthorization,
|
|
225
|
+
workspaceId: string,
|
|
226
|
+
sessionId: string,
|
|
227
|
+
request: UpdateSessionVisibilityRequest,
|
|
228
|
+
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
229
|
+
): Promise<UpdateSessionVisibilityResponse> {
|
|
230
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:control"]);
|
|
231
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
232
|
+
sessionId,
|
|
233
|
+
operation: "session.visibility.write",
|
|
234
|
+
surface: authorizationSurface,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await runTenancyMutation(
|
|
238
|
+
async () =>
|
|
239
|
+
await transitionSessionVisibility(deps.db, {
|
|
240
|
+
workspaceId,
|
|
241
|
+
sessionId,
|
|
242
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
243
|
+
targetVisibility: sessionVisibilityFromPublic(request.visibility),
|
|
244
|
+
expectedAuthorityEpoch: request.expectedAuthorityEpoch,
|
|
245
|
+
operationKey: request.idempotencyKey,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
const response = UpdateSessionVisibilityResponse.parse({
|
|
249
|
+
operationId: result.operationId,
|
|
250
|
+
eventId: result.eventId,
|
|
251
|
+
eventSequence: result.eventSequence,
|
|
252
|
+
visibility: sessionVisibilityToPublic(result.visibility),
|
|
253
|
+
authorityEpoch: result.authorityEpoch,
|
|
254
|
+
changed: result.changed,
|
|
255
|
+
replay: result.replay,
|
|
256
|
+
revokedGrantCount: result.revokedGrantCount,
|
|
257
|
+
});
|
|
258
|
+
await publishExactCommittedEvent(deps, {
|
|
259
|
+
workspaceId,
|
|
260
|
+
sessionId,
|
|
261
|
+
subjectId: authorization.grant.subjectId,
|
|
262
|
+
eventId: response.eventId,
|
|
263
|
+
eventSequence: response.eventSequence,
|
|
264
|
+
});
|
|
265
|
+
return response;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function forkManagedHumanSession(
|
|
269
|
+
deps: SessionTenancyDependencies,
|
|
270
|
+
authorization: AccessGrantAuthorization,
|
|
271
|
+
workspaceId: string,
|
|
272
|
+
sourceSessionId: string,
|
|
273
|
+
request: ForkSessionRequest,
|
|
274
|
+
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
275
|
+
): Promise<ForkSessionResponse> {
|
|
276
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, [
|
|
277
|
+
"sessions:read",
|
|
278
|
+
"sessions:create",
|
|
279
|
+
]);
|
|
280
|
+
const forkInput = {
|
|
281
|
+
sourceWorkspaceId: workspaceId,
|
|
282
|
+
sourceSessionId,
|
|
283
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
284
|
+
destinationWorkspaceId: workspaceId,
|
|
285
|
+
destinationVisibility:
|
|
286
|
+
request.visibility === "private" ? ("user_private" as const) : ("workspace_shared" as const),
|
|
287
|
+
workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
|
|
288
|
+
operationKey: request.idempotencyKey,
|
|
289
|
+
};
|
|
290
|
+
const recoverAppliedReceipt = async () =>
|
|
291
|
+
await runTenancyMutation(async () => await replayAppliedSessionFork(deps.db, forkInput));
|
|
292
|
+
|
|
293
|
+
// A committed response is owned by the exact actor/key/request tuple and no
|
|
294
|
+
// longer depends on mutable source visibility. Resolve it before source/host
|
|
295
|
+
// authorization so a lost response remains recoverable after the source
|
|
296
|
+
// owner makes a formerly shared session private.
|
|
297
|
+
const applied = await recoverAppliedReceipt();
|
|
298
|
+
if (applied) {
|
|
299
|
+
return await projectAndPublishSessionFork(deps, authorization.grant.subjectId, applied);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
304
|
+
sessionId: sourceSessionId,
|
|
305
|
+
operation: "session.fork.create",
|
|
306
|
+
surface: authorizationSurface,
|
|
307
|
+
});
|
|
308
|
+
} catch (authorizationError) {
|
|
309
|
+
// Close the race where a duplicate request checked before the first commit
|
|
310
|
+
// but mutable source/host authority changed immediately after it. Only an
|
|
311
|
+
// exact applied receipt can convert the authorization failure into replay.
|
|
312
|
+
const racedApplied = await recoverAppliedReceipt();
|
|
313
|
+
if (racedApplied) {
|
|
314
|
+
return await projectAndPublishSessionFork(deps, authorization.grant.subjectId, racedApplied);
|
|
315
|
+
}
|
|
316
|
+
throw authorizationError;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result = await runTenancyMutation(async () => await forkSessionContent(deps.db, forkInput));
|
|
320
|
+
return await projectAndPublishSessionFork(deps, authorization.grant.subjectId, result);
|
|
321
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IssueUserResourceGrantRequest,
|
|
3
|
+
Permission,
|
|
4
|
+
SessionAuthorizationSurface,
|
|
5
|
+
UserResourceKind,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import {
|
|
8
|
+
issueSelfUserResourceGrant,
|
|
9
|
+
listSelfUserResourceAuthorities,
|
|
10
|
+
revokeSelfUserResourceGrant,
|
|
11
|
+
sessionTenancyProductActivated,
|
|
12
|
+
SessionTenancyNotActivatedError,
|
|
13
|
+
} from "@opengeni/db";
|
|
14
|
+
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
15
|
+
import type { AppDependencies } from "../dependencies";
|
|
16
|
+
import { requireSessionAuthorization } from "../session-authorization";
|
|
17
|
+
import { requireCanonicalManagedHuman } from "./session-tenancy";
|
|
18
|
+
|
|
19
|
+
type UserResourceGrantDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
|
|
20
|
+
|
|
21
|
+
const LIST_PERMISSIONS = {
|
|
22
|
+
connection: ["connections:read"],
|
|
23
|
+
document: ["documents:search"],
|
|
24
|
+
variable_set: ["variable-sets:list"],
|
|
25
|
+
rig: ["rigs:use"],
|
|
26
|
+
connected_machine: ["enrollments:read"],
|
|
27
|
+
} as const satisfies Record<UserResourceKind, readonly Permission[]>;
|
|
28
|
+
|
|
29
|
+
const ISSUE_PERMISSIONS = {
|
|
30
|
+
connection: ["connections:read"],
|
|
31
|
+
document: ["documents:search"],
|
|
32
|
+
variable_set: ["variable-sets:list", "variable-sets:attach", "variable-sets:use"],
|
|
33
|
+
rig: ["rigs:use"],
|
|
34
|
+
connected_machine: ["enrollments:read"],
|
|
35
|
+
} as const satisfies Record<UserResourceKind, readonly Permission[]>;
|
|
36
|
+
|
|
37
|
+
async function requireOwnerProductGate(
|
|
38
|
+
deps: Pick<UserResourceGrantDependencies, "db">,
|
|
39
|
+
authorization: AccessGrantAuthorization,
|
|
40
|
+
workspaceId: string,
|
|
41
|
+
permissions: readonly Permission[],
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
// Target-free gates must run before an optional target-session lookup or host
|
|
44
|
+
// callback so rejected principals cannot use grant management as an oracle.
|
|
45
|
+
requireOwnerAuthority(authorization, workspaceId, permissions);
|
|
46
|
+
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
47
|
+
throw new SessionTenancyNotActivatedError();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requireOwnerAuthority(
|
|
52
|
+
authorization: AccessGrantAuthorization,
|
|
53
|
+
workspaceId: string,
|
|
54
|
+
permissions: readonly Permission[],
|
|
55
|
+
): void {
|
|
56
|
+
requireCanonicalManagedHuman(authorization, workspaceId);
|
|
57
|
+
for (const permission of permissions) requirePermission(authorization.grant, permission);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function listManagedHumanUserResourceAuthorities(
|
|
61
|
+
deps: Pick<UserResourceGrantDependencies, "db">,
|
|
62
|
+
authorization: AccessGrantAuthorization,
|
|
63
|
+
workspaceId: string,
|
|
64
|
+
input: { resourceKind: UserResourceKind; cursor?: string | undefined; limit: number },
|
|
65
|
+
) {
|
|
66
|
+
requireOwnerAuthority(authorization, workspaceId, LIST_PERMISSIONS[input.resourceKind]);
|
|
67
|
+
// Before activation there cannot be usable personal-resource authority in
|
|
68
|
+
// this organization. Discovery therefore has an exact empty answer; only
|
|
69
|
+
// issue/revoke and runtime use remain behind the forward-only product gate.
|
|
70
|
+
// This keeps ordinary workspace-owned Rig/Variable Set selection independent
|
|
71
|
+
// from an optional personal-resource product that is not enabled.
|
|
72
|
+
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
73
|
+
return { authorities: [], nextCursor: null };
|
|
74
|
+
}
|
|
75
|
+
return await listSelfUserResourceAuthorities(deps.db, {
|
|
76
|
+
accountId: authorization.grant.accountId,
|
|
77
|
+
workspaceId,
|
|
78
|
+
subjectId: authorization.grant.subjectId,
|
|
79
|
+
...input,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function issueManagedHumanUserResourceGrant(
|
|
84
|
+
deps: UserResourceGrantDependencies,
|
|
85
|
+
authorization: AccessGrantAuthorization,
|
|
86
|
+
workspaceId: string,
|
|
87
|
+
authorityId: string,
|
|
88
|
+
request: IssueUserResourceGrantRequest,
|
|
89
|
+
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
90
|
+
) {
|
|
91
|
+
const modePermissions: Permission[] =
|
|
92
|
+
request.mode === "session" ? ["sessions:control"] : ["sessions:create"];
|
|
93
|
+
await requireOwnerProductGate(deps, authorization, workspaceId, [
|
|
94
|
+
...ISSUE_PERMISSIONS[request.resourceKind],
|
|
95
|
+
...modePermissions,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
if (request.mode === "session") {
|
|
99
|
+
// Contract refinement guarantees both fields. Keep the defensive check so
|
|
100
|
+
// direct core callers cannot accidentally weaken the target fence.
|
|
101
|
+
if (!request.sessionId || !request.expectedAuthorityEpoch) {
|
|
102
|
+
throw new TypeError("session grant requires its exact authority epoch");
|
|
103
|
+
}
|
|
104
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
105
|
+
sessionId: request.sessionId,
|
|
106
|
+
operation: "session.personal_resource.grant",
|
|
107
|
+
surface: authorizationSurface,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return await issueSelfUserResourceGrant(deps.db, {
|
|
112
|
+
accountId: authorization.grant.accountId,
|
|
113
|
+
workspaceId,
|
|
114
|
+
subjectId: authorization.grant.subjectId,
|
|
115
|
+
authorityId,
|
|
116
|
+
resourceKind: request.resourceKind,
|
|
117
|
+
mode: request.mode,
|
|
118
|
+
context: request.context,
|
|
119
|
+
sessionId: request.sessionId ?? null,
|
|
120
|
+
expectedAuthorityEpoch: request.expectedAuthorityEpoch ?? null,
|
|
121
|
+
workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function revokeManagedHumanUserResourceGrant(
|
|
126
|
+
deps: Pick<UserResourceGrantDependencies, "db">,
|
|
127
|
+
authorization: AccessGrantAuthorization,
|
|
128
|
+
workspaceId: string,
|
|
129
|
+
grantId: string,
|
|
130
|
+
) {
|
|
131
|
+
// Revocation narrows authority. It intentionally does not require the
|
|
132
|
+
// resource-specific permission that may have been removed since issuance.
|
|
133
|
+
await requireOwnerProductGate(deps, authorization, workspaceId, []);
|
|
134
|
+
return await revokeSelfUserResourceGrant(deps.db, {
|
|
135
|
+
accountId: authorization.grant.accountId,
|
|
136
|
+
workspaceId,
|
|
137
|
+
subjectId: authorization.grant.subjectId,
|
|
138
|
+
grantId,
|
|
139
|
+
});
|
|
140
|
+
}
|
package/src/billing/limits.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { configuredStaticUsageLimits } from "@opengeni/config";
|
|
1
|
+
import { configuredStaticUsageLimits, resolveModelProvider } from "@opengeni/config";
|
|
2
2
|
import type {
|
|
3
3
|
LimitAction,
|
|
4
4
|
LimitDecision,
|
|
@@ -25,12 +25,11 @@ export type LimitCheckInput = {
|
|
|
25
25
|
workspaceId?: string;
|
|
26
26
|
action: LimitAction;
|
|
27
27
|
quantity?: number;
|
|
28
|
-
// The turn's model id, when the action represents an agent turn.
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// this undefined and are unaffected.
|
|
28
|
+
// The turn's model id, when the action represents an agent turn. Externally
|
|
29
|
+
// billed models consume ZERO OpenGeni credits, so the credit-balance +
|
|
30
|
+
// model-cost + token gates are skipped. Connected subscriptions still require
|
|
31
|
+
// live workspace readiness; an explicitly anonymous deployment route is
|
|
32
|
+
// statically ready by definition. Non-model infra actions leave this undefined.
|
|
34
33
|
model?: string | null;
|
|
35
34
|
};
|
|
36
35
|
|
|
@@ -59,23 +58,34 @@ export async function checkLimit(
|
|
|
59
58
|
model: input.model,
|
|
60
59
|
})
|
|
61
60
|
: false;
|
|
62
|
-
const
|
|
61
|
+
const credentialFreeExternal = input.model
|
|
62
|
+
? (() => {
|
|
63
|
+
const resolved = resolveModelProvider(deps.settings, input.model);
|
|
64
|
+
return (
|
|
65
|
+
resolved?.model.billing.metering === "external" &&
|
|
66
|
+
resolved.model.credentialSource.kind === "deployment" &&
|
|
67
|
+
resolved.model.credentialSource.mechanism === "none"
|
|
68
|
+
);
|
|
69
|
+
})()
|
|
70
|
+
: false;
|
|
71
|
+
const externallyBilled = codexBilled || credentialFreeExternal;
|
|
72
|
+
const creditDecision = await checkCreditBalance(deps, input, externallyBilled);
|
|
63
73
|
if (!creditDecision.allowed) {
|
|
64
74
|
return creditDecision;
|
|
65
75
|
}
|
|
66
76
|
if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
|
|
67
77
|
return { allowed: true };
|
|
68
78
|
}
|
|
69
|
-
return await checkStaticCaps(deps, input,
|
|
79
|
+
return await checkStaticCaps(deps, input, externallyBilled);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
async function checkCreditBalance(
|
|
73
83
|
deps: LimitDependencies,
|
|
74
84
|
input: LimitCheckInput,
|
|
75
|
-
|
|
85
|
+
externallyBilled: boolean,
|
|
76
86
|
): Promise<LimitDecision> {
|
|
77
|
-
if (
|
|
78
|
-
return { allowed: true }; // paid
|
|
87
|
+
if (externallyBilled) {
|
|
88
|
+
return { allowed: true }; // paid outside OpenGeni — zero OpenGeni credits
|
|
79
89
|
}
|
|
80
90
|
if (!usesCreditLimits(deps) || !isCostlyAction(input.action)) {
|
|
81
91
|
return { allowed: true };
|
|
@@ -90,10 +100,10 @@ async function checkCreditBalance(
|
|
|
90
100
|
async function checkStaticCaps(
|
|
91
101
|
deps: LimitDependencies,
|
|
92
102
|
input: LimitCheckInput,
|
|
93
|
-
|
|
103
|
+
externallyBilled: boolean,
|
|
94
104
|
): Promise<LimitDecision> {
|
|
95
105
|
const limits = configuredStaticUsageLimits(deps.settings);
|
|
96
|
-
if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !
|
|
106
|
+
if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !externallyBilled) {
|
|
97
107
|
const used = await sumUsageQuantity(deps.db, {
|
|
98
108
|
accountId: input.accountId,
|
|
99
109
|
eventType: "model.cost",
|
|
@@ -172,7 +182,7 @@ async function checkStaticCaps(
|
|
|
172
182
|
);
|
|
173
183
|
}
|
|
174
184
|
case "tokens:consume": {
|
|
175
|
-
if (
|
|
185
|
+
if (externallyBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
|
|
176
186
|
return { allowed: true };
|
|
177
187
|
}
|
|
178
188
|
const used = await sumUsageQuantity(deps.db, {
|
package/src/dependencies.ts
CHANGED
|
@@ -25,6 +25,11 @@ import type {
|
|
|
25
25
|
} from "./editable-artifacts";
|
|
26
26
|
|
|
27
27
|
export type SessionWorkflowClient = {
|
|
28
|
+
triggerAutomationRun?: (input: {
|
|
29
|
+
accountId: string;
|
|
30
|
+
workspaceId: string;
|
|
31
|
+
runId: string;
|
|
32
|
+
}) => Promise<void>;
|
|
28
33
|
signalUserMessage: (input: {
|
|
29
34
|
sessionId: string;
|
|
30
35
|
eventId: string;
|
|
@@ -108,7 +113,11 @@ export type AppDependencies = {
|
|
|
108
113
|
editableArtifactOfficeImports?: EditableArtifactOfficeImportPort;
|
|
109
114
|
bus: EventBus;
|
|
110
115
|
workflowClient: SessionWorkflowClient;
|
|
111
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Injectable structural seam for replayable post-commit fanout and wake work.
|
|
118
|
+
* The historical name is retained for host compatibility; every interactive
|
|
119
|
+
* session command now uses this boundary, not only prompt submission.
|
|
120
|
+
*/
|
|
112
121
|
schedulePromptPostCommit?: (task: () => Promise<void>) => void;
|
|
113
122
|
/** Optional provider override for deterministic API/object-storage tests. */
|
|
114
123
|
objectStorage?: ObjectStorageDependency;
|
|
@@ -138,6 +147,8 @@ export type AppDependencies = {
|
|
|
138
147
|
managedAuth?: ManagedAuth | null;
|
|
139
148
|
/** Injectable Codex HTTP transport for deterministic API/provider tests. */
|
|
140
149
|
codexFetch?: typeof fetch;
|
|
150
|
+
/** Injectable GitHub transport for deterministic personal-OAuth tests. */
|
|
151
|
+
githubPersonalFetch?: typeof fetch;
|
|
141
152
|
/** Injectable xAI OAuth/subscription transport for deterministic API/provider tests. */
|
|
142
153
|
xaiFetch?: typeof fetch;
|
|
143
154
|
/** Injectable Slack Web API transport for deterministic bot-connection tests. */
|