@opengeni/core 0.2.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/README.md +14 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +2627 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/access/index.ts +186 -0
- package/src/billing/limits.ts +207 -0
- package/src/dependencies.ts +70 -0
- package/src/domain/capabilities.ts +959 -0
- package/src/domain/environments.ts +115 -0
- package/src/domain/packs.ts +241 -0
- package/src/domain/resources.ts +221 -0
- package/src/domain/scheduled-tasks.ts +321 -0
- package/src/domain/sessions.ts +812 -0
- package/src/domain/workspace-members.ts +80 -0
- package/src/index.ts +59 -0
- package/src/managed-auth-type.ts +20 -0
- package/src/sandbox/fleet.ts +460 -0
- package/src/sandbox/routing.ts +127 -0
- package/src/sandbox-types.ts +61 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { verifyDelegatedAccessToken, type AccessContext, type AccessGrant, type Permission } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
bootstrapWorkspace,
|
|
5
|
+
ensureManagedAccessForUser,
|
|
6
|
+
findActiveApiKeyByHash,
|
|
7
|
+
getWorkspaceGrant,
|
|
8
|
+
requireWorkspace,
|
|
9
|
+
type Database,
|
|
10
|
+
} from "@opengeni/db";
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
import { HTTPException } from "hono/http-exception";
|
|
13
|
+
import type { ManagedAuth } from "../managed-auth-type";
|
|
14
|
+
|
|
15
|
+
const bearerPrefix = "Bearer ";
|
|
16
|
+
|
|
17
|
+
export type AccessDeps = {
|
|
18
|
+
db: Database;
|
|
19
|
+
settings: Settings;
|
|
20
|
+
managedAuth?: ManagedAuth | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function requireAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext> {
|
|
24
|
+
const context = await resolveAccessContext(c, deps);
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new HTTPException(401, { message: "authentication required" });
|
|
27
|
+
}
|
|
28
|
+
return context;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function requireAccessGrant(c: Context, deps: AccessDeps, workspaceId: string, permission?: Permission): Promise<AccessGrant> {
|
|
32
|
+
const context = await requireAccessContext(c, deps);
|
|
33
|
+
const grant = context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId)
|
|
34
|
+
?? await getWorkspaceGrant(deps.db, context.subjectId, workspaceId);
|
|
35
|
+
if (!grant) {
|
|
36
|
+
const workspace = await requireWorkspace(deps.db, workspaceId).catch(() => null);
|
|
37
|
+
if (!workspace) {
|
|
38
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
39
|
+
}
|
|
40
|
+
throw new HTTPException(403, { message: "workspace access denied" });
|
|
41
|
+
}
|
|
42
|
+
if (permission) {
|
|
43
|
+
requirePermission(grant, permission);
|
|
44
|
+
}
|
|
45
|
+
return grant;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function requirePermission(grant: AccessGrant, permission: Permission): void {
|
|
49
|
+
if (!hasPermission(grant.permissions, permission)) {
|
|
50
|
+
throw new HTTPException(403, { message: `missing permission: ${permission}` });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasPermission(permissions: Permission[], permission: Permission): boolean {
|
|
55
|
+
return permissions.includes(permission) || permissions.includes("workspace:admin");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
|
|
59
|
+
if (deps.settings.productAccessMode === "local") {
|
|
60
|
+
return await bootstrapWorkspace(deps.db, {
|
|
61
|
+
accountExternalSource: "opengeni:local",
|
|
62
|
+
accountExternalId: "default",
|
|
63
|
+
accountName: "Local",
|
|
64
|
+
workspaceExternalSource: "opengeni:local",
|
|
65
|
+
workspaceExternalId: "default",
|
|
66
|
+
workspaceName: "Local",
|
|
67
|
+
subjectId: "dev",
|
|
68
|
+
subjectLabel: "Local dev",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (deps.settings.productAccessMode === "configured") {
|
|
73
|
+
const delegated = await delegatedAccessContext(c, deps, "configured");
|
|
74
|
+
if (delegated) {
|
|
75
|
+
return delegated;
|
|
76
|
+
}
|
|
77
|
+
if (deps.settings.delegationSecret) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return await bootstrapWorkspace(deps.db, {
|
|
81
|
+
accountExternalSource: "opengeni:configured",
|
|
82
|
+
accountExternalId: "default",
|
|
83
|
+
accountName: "Configured",
|
|
84
|
+
workspaceExternalSource: "opengeni:configured",
|
|
85
|
+
workspaceExternalId: "default",
|
|
86
|
+
workspaceName: "Configured",
|
|
87
|
+
subjectId: configuredSubject(c),
|
|
88
|
+
subjectLabel: "Configured key",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const bearer = bearerToken(c);
|
|
93
|
+
if (bearer) {
|
|
94
|
+
const delegated = await delegatedAccessContext(c, deps, "managed", bearer);
|
|
95
|
+
if (delegated) {
|
|
96
|
+
return delegated;
|
|
97
|
+
}
|
|
98
|
+
const apiKey = await findActiveApiKeyByHash(deps.db, await sha256Hex(bearer));
|
|
99
|
+
if (apiKey) {
|
|
100
|
+
const accountPermissions = apiKey.workspaceId
|
|
101
|
+
? apiKey.permissions.filter((permission) => permission === "billing:read" || permission === "billing:manage")
|
|
102
|
+
: apiKey.permissions;
|
|
103
|
+
return {
|
|
104
|
+
mode: "managed",
|
|
105
|
+
subjectId: `api_key:${apiKey.id}`,
|
|
106
|
+
subjectLabel: apiKey.name,
|
|
107
|
+
accountGrants: [{
|
|
108
|
+
accountId: apiKey.accountId,
|
|
109
|
+
subjectId: `api_key:${apiKey.id}`,
|
|
110
|
+
subjectLabel: apiKey.name,
|
|
111
|
+
permissions: accountPermissions,
|
|
112
|
+
}],
|
|
113
|
+
workspaceGrants: apiKey.workspaceId ? [{
|
|
114
|
+
workspaceId: apiKey.workspaceId,
|
|
115
|
+
accountId: apiKey.accountId,
|
|
116
|
+
subjectId: `api_key:${apiKey.id}`,
|
|
117
|
+
subjectLabel: apiKey.name,
|
|
118
|
+
permissions: apiKey.permissions,
|
|
119
|
+
}] : [],
|
|
120
|
+
defaultAccountId: apiKey.accountId,
|
|
121
|
+
defaultWorkspaceId: apiKey.workspaceId,
|
|
122
|
+
} satisfies AccessContext;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (deps.managedAuth) {
|
|
127
|
+
const session = await deps.managedAuth.api.getSession({ headers: c.req.raw.headers });
|
|
128
|
+
if (session?.user) {
|
|
129
|
+
return await ensureManagedAccessForUser(deps.db, {
|
|
130
|
+
userId: session.user.id,
|
|
131
|
+
email: session.user.email,
|
|
132
|
+
name: session.user.name,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function delegatedAccessContext(c: Context, deps: AccessDeps, mode: "configured" | "managed", token = bearerToken(c)): Promise<AccessContext | null> {
|
|
141
|
+
if (!token || !deps.settings.delegationSecret) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const payload = await verifyDelegatedAccessToken(deps.settings.delegationSecret, token);
|
|
145
|
+
if (!payload) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
mode,
|
|
150
|
+
subjectId: payload.subjectId,
|
|
151
|
+
...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
|
|
152
|
+
accountGrants: [{
|
|
153
|
+
accountId: payload.accountId,
|
|
154
|
+
subjectId: payload.subjectId,
|
|
155
|
+
...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
|
|
156
|
+
permissions: payload.permissions,
|
|
157
|
+
}],
|
|
158
|
+
workspaceGrants: [{
|
|
159
|
+
workspaceId: payload.workspaceId,
|
|
160
|
+
accountId: payload.accountId,
|
|
161
|
+
subjectId: payload.subjectId,
|
|
162
|
+
...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
|
|
163
|
+
permissions: payload.permissions,
|
|
164
|
+
// sessionId is worker-asserted (HMAC-signed token claim), not agent
|
|
165
|
+
// controlled; it scopes session-bound MCP tools such as goal management.
|
|
166
|
+
metadata: { delegated: true, ...(payload.sessionId ? { sessionId: payload.sessionId } : {}) },
|
|
167
|
+
}],
|
|
168
|
+
defaultAccountId: payload.accountId,
|
|
169
|
+
defaultWorkspaceId: payload.workspaceId,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function configuredSubject(c: Context): string {
|
|
174
|
+
const header = c.req.header("x-opengeni-subject");
|
|
175
|
+
return header && header.trim().length > 0 ? `configured:${header.trim()}` : "configured:key";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function bearerToken(c: Context): string | null {
|
|
179
|
+
const authorization = c.req.header("authorization");
|
|
180
|
+
return authorization?.startsWith(bearerPrefix) ? authorization.slice(bearerPrefix.length) : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function sha256Hex(value: string): Promise<string> {
|
|
184
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
185
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
186
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { configuredStaticUsageLimits } from "@opengeni/config";
|
|
2
|
+
import type { LimitAction, LimitDecision } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
countActiveApiKeysForWorkspace,
|
|
5
|
+
countScheduledTasksForWorkspace,
|
|
6
|
+
countWorkspacesForAccount,
|
|
7
|
+
getBillingBalance,
|
|
8
|
+
isCodexBilledTurn,
|
|
9
|
+
recordUsageEvent,
|
|
10
|
+
sumUsageQuantity,
|
|
11
|
+
} from "@opengeni/db";
|
|
12
|
+
import { HTTPException } from "hono/http-exception";
|
|
13
|
+
import type { ApiRouteDeps } from "../dependencies";
|
|
14
|
+
|
|
15
|
+
export type LimitCheckInput = {
|
|
16
|
+
accountId: string;
|
|
17
|
+
workspaceId?: string;
|
|
18
|
+
action: LimitAction;
|
|
19
|
+
quantity?: number;
|
|
20
|
+
// The turn's model id, when the action represents an agent turn. When this is a
|
|
21
|
+
// Codex-billed turn (codex/<slug> + feature enabled + active workspace
|
|
22
|
+
// credential) the turn is paid by the user's ChatGPT/Codex plan and consumes
|
|
23
|
+
// ZERO OpenGeni credits, so the credit-balance + model-cost + token gates are
|
|
24
|
+
// skipped. Non-model infra actions (workspace/api_key/schedule create) leave
|
|
25
|
+
// this undefined and are unaffected.
|
|
26
|
+
model?: string | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function requireLimit(deps: ApiRouteDeps, input: LimitCheckInput): Promise<void> {
|
|
30
|
+
const decision = await checkLimit(deps, input);
|
|
31
|
+
if (decision.allowed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
throw new HTTPException(decision.code === "insufficient_credits" ? 402 : 429, { message: decision.message });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function checkLimit(deps: ApiRouteDeps, input: LimitCheckInput): Promise<LimitDecision> {
|
|
38
|
+
// Resolve the canonical codex-billed predicate ONCE. Returns false for any
|
|
39
|
+
// action that carries no model (infra caps) or any codex/<slug> model without
|
|
40
|
+
// an active credential — so the bypass never triggers on the prefix alone.
|
|
41
|
+
const codexBilled = input.workspaceId
|
|
42
|
+
? await isCodexBilledTurn({ db: deps.db, settings: deps.settings, workspaceId: input.workspaceId, model: input.model })
|
|
43
|
+
: false;
|
|
44
|
+
const creditDecision = await checkCreditBalance(deps, input, codexBilled);
|
|
45
|
+
if (!creditDecision.allowed) {
|
|
46
|
+
return creditDecision;
|
|
47
|
+
}
|
|
48
|
+
if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
|
|
49
|
+
return { allowed: true };
|
|
50
|
+
}
|
|
51
|
+
return await checkStaticCaps(deps, input, codexBilled);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function checkCreditBalance(deps: ApiRouteDeps, input: LimitCheckInput, codexBilled: boolean): Promise<LimitDecision> {
|
|
55
|
+
if (codexBilled) {
|
|
56
|
+
return { allowed: true }; // paid by the user's ChatGPT/Codex plan — zero OpenGeni credits
|
|
57
|
+
}
|
|
58
|
+
if (!usesCreditLimits(deps) || !isCostlyAction(input.action)) {
|
|
59
|
+
return { allowed: true };
|
|
60
|
+
}
|
|
61
|
+
const balance = await getBillingBalance(deps.db, input.accountId);
|
|
62
|
+
if (balance.balanceMicros > 0) {
|
|
63
|
+
return { allowed: true };
|
|
64
|
+
}
|
|
65
|
+
return { allowed: false, code: "insufficient_credits", message: "insufficient OpenGeni credits" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function checkStaticCaps(deps: ApiRouteDeps, input: LimitCheckInput, codexBilled: boolean): Promise<LimitDecision> {
|
|
69
|
+
const limits = configuredStaticUsageLimits(deps.settings);
|
|
70
|
+
if (limits.maxMonthlyCostMicrosPerAccount && isCostlyAction(input.action) && !codexBilled) {
|
|
71
|
+
const used = await sumUsageQuantity(deps.db, {
|
|
72
|
+
accountId: input.accountId,
|
|
73
|
+
eventType: "model.cost",
|
|
74
|
+
since: startOfUtcMonth(),
|
|
75
|
+
});
|
|
76
|
+
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
77
|
+
return blocked("max_monthly_cost_micros_per_account", `monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
switch (input.action) {
|
|
81
|
+
case "workspace:create": {
|
|
82
|
+
if (!limits.maxWorkspacesPerAccount) {
|
|
83
|
+
return { allowed: true };
|
|
84
|
+
}
|
|
85
|
+
const count = await countWorkspacesForAccount(deps.db, input.accountId);
|
|
86
|
+
return count < limits.maxWorkspacesPerAccount
|
|
87
|
+
? { allowed: true }
|
|
88
|
+
: blocked("max_workspaces_per_account", `workspace limit reached (${limits.maxWorkspacesPerAccount})`);
|
|
89
|
+
}
|
|
90
|
+
case "api_key:create": {
|
|
91
|
+
if (!limits.maxApiKeysPerWorkspace || !input.workspaceId) {
|
|
92
|
+
return { allowed: true };
|
|
93
|
+
}
|
|
94
|
+
const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
|
|
95
|
+
return count < limits.maxApiKeysPerWorkspace
|
|
96
|
+
? { allowed: true }
|
|
97
|
+
: blocked("max_api_keys_per_workspace", `API key limit reached (${limits.maxApiKeysPerWorkspace})`);
|
|
98
|
+
}
|
|
99
|
+
case "schedule:create": {
|
|
100
|
+
if (!limits.maxSchedulesPerWorkspace || !input.workspaceId) {
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
103
|
+
const count = await countScheduledTasksForWorkspace(deps.db, input.workspaceId);
|
|
104
|
+
return count < limits.maxSchedulesPerWorkspace
|
|
105
|
+
? { allowed: true }
|
|
106
|
+
: blocked("max_schedules_per_workspace", `scheduled task limit reached (${limits.maxSchedulesPerWorkspace})`);
|
|
107
|
+
}
|
|
108
|
+
case "file:upload": {
|
|
109
|
+
if (!limits.maxFileUploadBytes || !input.quantity) {
|
|
110
|
+
return { allowed: true };
|
|
111
|
+
}
|
|
112
|
+
return input.quantity <= limits.maxFileUploadBytes
|
|
113
|
+
? { allowed: true }
|
|
114
|
+
: blocked("max_file_upload_bytes", `file upload exceeds static limit of ${limits.maxFileUploadBytes} bytes`);
|
|
115
|
+
}
|
|
116
|
+
case "agent_run:create": {
|
|
117
|
+
if (!limits.maxMonthlyAgentRunsPerWorkspace || !input.workspaceId) {
|
|
118
|
+
return { allowed: true };
|
|
119
|
+
}
|
|
120
|
+
const used = await sumUsageQuantity(deps.db, {
|
|
121
|
+
workspaceId: input.workspaceId,
|
|
122
|
+
eventType: "agent_run.created",
|
|
123
|
+
since: startOfUtcMonth(),
|
|
124
|
+
});
|
|
125
|
+
const requested = input.quantity ?? 0;
|
|
126
|
+
return used + requested <= limits.maxMonthlyAgentRunsPerWorkspace
|
|
127
|
+
? { allowed: true }
|
|
128
|
+
: blocked("max_monthly_agent_runs_per_workspace", `monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`);
|
|
129
|
+
}
|
|
130
|
+
case "tokens:consume": {
|
|
131
|
+
if (codexBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
|
|
132
|
+
return { allowed: true };
|
|
133
|
+
}
|
|
134
|
+
const used = await sumUsageQuantity(deps.db, {
|
|
135
|
+
workspaceId: input.workspaceId,
|
|
136
|
+
eventType: "model.tokens",
|
|
137
|
+
since: startOfUtcMonth(),
|
|
138
|
+
});
|
|
139
|
+
const requested = input.quantity ?? 0;
|
|
140
|
+
return used + requested <= limits.maxMonthlyTokensPerWorkspace
|
|
141
|
+
? { allowed: true }
|
|
142
|
+
: blocked("max_monthly_tokens_per_workspace", `monthly token limit reached (${limits.maxMonthlyTokensPerWorkspace})`);
|
|
143
|
+
}
|
|
144
|
+
case "document:index": {
|
|
145
|
+
if (!limits.maxDocumentIndexedChunksPerWorkspace || !input.workspaceId) {
|
|
146
|
+
return { allowed: true };
|
|
147
|
+
}
|
|
148
|
+
const used = await sumUsageQuantity(deps.db, {
|
|
149
|
+
workspaceId: input.workspaceId,
|
|
150
|
+
eventType: "document.indexed",
|
|
151
|
+
since: startOfUtcMonth(),
|
|
152
|
+
});
|
|
153
|
+
const requested = input.quantity ?? 0;
|
|
154
|
+
return used + requested <= limits.maxDocumentIndexedChunksPerWorkspace
|
|
155
|
+
? { allowed: true }
|
|
156
|
+
: blocked("max_document_indexed_chunks_per_workspace", `monthly document indexing limit reached (${limits.maxDocumentIndexedChunksPerWorkspace} chunks)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function recordWorkspaceUsage(deps: ApiRouteDeps, input: {
|
|
162
|
+
accountId: string;
|
|
163
|
+
workspaceId: string;
|
|
164
|
+
subjectId?: string | null;
|
|
165
|
+
eventType:
|
|
166
|
+
| "agent_run.created"
|
|
167
|
+
| "file.uploaded"
|
|
168
|
+
| "document.indexed"
|
|
169
|
+
| "scheduled_task.fired";
|
|
170
|
+
quantity: number;
|
|
171
|
+
unit: string;
|
|
172
|
+
sourceResourceType: string;
|
|
173
|
+
sourceResourceId: string;
|
|
174
|
+
idempotencyKey: string;
|
|
175
|
+
}): Promise<void> {
|
|
176
|
+
await recordUsageEvent(deps.db, {
|
|
177
|
+
accountId: input.accountId,
|
|
178
|
+
workspaceId: input.workspaceId,
|
|
179
|
+
subjectId: input.subjectId ?? null,
|
|
180
|
+
eventType: input.eventType,
|
|
181
|
+
quantity: input.quantity,
|
|
182
|
+
unit: input.unit,
|
|
183
|
+
sourceResourceType: input.sourceResourceType,
|
|
184
|
+
sourceResourceId: input.sourceResourceId,
|
|
185
|
+
idempotencyKey: input.idempotencyKey,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function usesCreditLimits(deps: ApiRouteDeps): boolean {
|
|
190
|
+
return deps.settings.billingMode === "stripe" || deps.settings.usageLimitsMode === "managed";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isCostlyAction(action: LimitAction): boolean {
|
|
194
|
+
return action === "agent_run:create"
|
|
195
|
+
|| action === "tokens:consume"
|
|
196
|
+
|| action === "file:upload"
|
|
197
|
+
|| action === "document:index";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function blocked(code: string, message: string): LimitDecision {
|
|
201
|
+
return { allowed: false, code, message };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function startOfUtcMonth(): Date {
|
|
205
|
+
const now = new Date();
|
|
206
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
207
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import type { Document, ScheduledTask } from "@opengeni/contracts";
|
|
3
|
+
import type { Database } from "@opengeni/db";
|
|
4
|
+
import type { DocumentServices } from "@opengeni/documents";
|
|
5
|
+
import type { EventBus } from "@opengeni/events";
|
|
6
|
+
import type { Observability } from "@opengeni/observability";
|
|
7
|
+
import type { createObjectStorage } from "@opengeni/storage";
|
|
8
|
+
import type { ManagedAuth } from "./managed-auth-type";
|
|
9
|
+
import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
|
|
10
|
+
|
|
11
|
+
export type SessionWorkflowClient = {
|
|
12
|
+
signalUserMessage: (input: { sessionId: string; eventId: string; workflowId: string }) => Promise<void>;
|
|
13
|
+
wakeSessionWorkflow: (input: { accountId: string; workspaceId: string; sessionId: string; workflowId: string }) => Promise<void>;
|
|
14
|
+
signalApprovalDecision: (input: { sessionId: string; eventId: string; workflowId: string }) => Promise<void>;
|
|
15
|
+
// Interrupt must reach the workflow whether or not an execution is currently
|
|
16
|
+
// running: a long-lived session that has gone idle (its workflow returned
|
|
17
|
+
// after markSessionIdle) has NO running execution, so a plain
|
|
18
|
+
// getHandle().signal() throws WorkflowNotFoundError -> a 500 that leaves an
|
|
19
|
+
// operator unable to stop a session. signalWithStart start-or-signals, so it
|
|
20
|
+
// needs the session-workflow args (accountId/workspaceId) to start a fresh
|
|
21
|
+
// run when none is live; the buffered `interrupt` signal is then honored by
|
|
22
|
+
// the workflow's idle-interrupt path (pause goal + mark idle).
|
|
23
|
+
signalInterrupt: (input: { accountId: string; workspaceId: string; sessionId: string; eventId: string; workflowId: string }) => Promise<void>;
|
|
24
|
+
syncScheduledTask: (input: { task: ScheduledTask }) => Promise<void>;
|
|
25
|
+
deleteScheduledTaskSchedule: (input: { temporalScheduleId: string }) => Promise<void>;
|
|
26
|
+
triggerScheduledTask: (input: { task: ScheduledTask; agentRunUsageIdempotencyKey?: string; triggerWorkflowId?: string }) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type DocumentIndexClient = {
|
|
30
|
+
indexDocument: (input: { accountId: string; workspaceId: string; documentId: string }) => Promise<Document | void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type AppDependencies = {
|
|
34
|
+
settings: Settings;
|
|
35
|
+
db: Database;
|
|
36
|
+
bus: EventBus;
|
|
37
|
+
workflowClient: SessionWorkflowClient;
|
|
38
|
+
documentIndexer?: DocumentIndexClient;
|
|
39
|
+
documentServices?: DocumentServices;
|
|
40
|
+
observability?: Observability;
|
|
41
|
+
githubStateSecret?: string;
|
|
42
|
+
managedAuth?: ManagedAuth | null;
|
|
43
|
+
// The API process's OWN agent-loop-free sandbox client (constructed from
|
|
44
|
+
// settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
|
|
45
|
+
// This is the foundation of the API-direct control plane: the API resumes
|
|
46
|
+
// boxes by id in-process, no Temporal/worker for non-turn ops. Optional on
|
|
47
|
+
// construction (createApp builds it from settings when absent) so existing
|
|
48
|
+
// tests that pass a minimal deps bag keep working.
|
|
49
|
+
sandboxClient?: ApiSandboxClient;
|
|
50
|
+
/**
|
|
51
|
+
* Resume a box by id from a serialized resume_state envelope (the lease's
|
|
52
|
+
* `resume_state` + `resume_backend_id` from P1.1) and return a live session
|
|
53
|
+
* for a single in-process op. resume → use → drop; the lease owns lifecycle,
|
|
54
|
+
* the returned handle does NOT own the box. Throws SandboxResumeError on a
|
|
55
|
+
* backend mismatch or a resume failure.
|
|
56
|
+
*/
|
|
57
|
+
resumeBoxById?: (input: ResumeBoxByIdInput) => Promise<ResumedSandboxSession>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type ObjectStorageDependency = ReturnType<typeof createObjectStorage>;
|
|
61
|
+
|
|
62
|
+
export type ApiRouteDeps = AppDependencies & {
|
|
63
|
+
objectStorage: ObjectStorageDependency;
|
|
64
|
+
githubStateSecret: string;
|
|
65
|
+
documentIndexer: DocumentIndexClient;
|
|
66
|
+
getDocumentServices: () => DocumentServices;
|
|
67
|
+
// Resolved by createApp from settings: routes always get a concrete
|
|
68
|
+
// resumeBoxById (it throws SandboxResumeError when sandboxBackend=none).
|
|
69
|
+
resumeBoxById: (input: ResumeBoxByIdInput) => Promise<ResumedSandboxSession>;
|
|
70
|
+
};
|