@opengeni/api-router 0.12.7 → 0.14.3
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/app.d.ts +18 -38
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth.d.ts +35 -0
- package/dist/{chunk-SD5GJTZ4.js → chunk-36PW33ND.js} +3744 -638
- package/dist/chunk-36PW33ND.js.map +1 -0
- package/dist/codex-redemption-security.d.ts +15 -0
- package/dist/github-access.d.ts +12 -0
- package/dist/github-browser-flow.d.ts +14 -0
- package/dist/http/auth.d.ts +3 -0
- package/dist/http/common.d.ts +3 -0
- package/dist/http/sse.d.ts +70 -0
- package/dist/index.d.ts +11 -13
- package/dist/index.js +1 -1
- package/dist/integrations/google-drive.d.ts +76 -0
- package/dist/integrations/oauth-client.d.ts +42 -0
- package/dist/integrations/provider-domain.d.ts +7 -0
- package/dist/integrations/slack-bot.d.ts +343 -0
- package/dist/mcp/documents.d.ts +8 -0
- package/dist/mcp/files.d.ts +5 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/session-view.d.ts +191 -0
- package/dist/mcp/toolspace.d.ts +61 -0
- package/dist/model-catalog.d.ts +39 -0
- package/dist/observability.d.ts +3 -0
- package/dist/routes/api-keys.d.ts +3 -0
- package/dist/routes/billing.d.ts +16 -0
- package/dist/routes/capabilities.d.ts +3 -0
- package/dist/routes/catalog-assets.d.ts +5 -0
- package/dist/routes/codex.d.ts +10 -0
- package/dist/routes/connections.d.ts +3 -0
- package/dist/routes/documents.d.ts +3 -0
- package/dist/routes/enrollments.d.ts +3 -0
- package/dist/routes/environments.d.ts +5 -0
- package/dist/routes/files.d.ts +4 -0
- package/dist/routes/github.d.ts +3 -0
- package/dist/routes/insights.d.ts +3 -0
- package/dist/routes/install.d.ts +5 -0
- package/dist/routes/machines.d.ts +3 -0
- package/dist/routes/packs.d.ts +3 -0
- package/dist/routes/preference-registry.d.ts +5 -0
- package/dist/routes/rigs.d.ts +3 -0
- package/dist/routes/scheduled-tasks.d.ts +3 -0
- package/dist/routes/sessions.d.ts +15 -0
- package/dist/routes/social.d.ts +3 -0
- package/dist/routes/transcriptions.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +59 -0
- package/dist/routes/workspace-instruction-policies.d.ts +3 -0
- package/dist/routes/workspace-state.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +5 -0
- package/dist/sandbox/access.d.ts +21 -0
- package/dist/sandbox/auth-callout.d.ts +30 -0
- package/dist/sandbox/channel-a.d.ts +37 -0
- package/dist/sandbox/enrollment.d.ts +120 -0
- package/dist/sandbox/machines.d.ts +29 -0
- package/dist/sandbox/metrics-ingestion.d.ts +128 -0
- package/dist/sandbox/rematerialize.d.ts +24 -0
- package/dist/sandbox/viewer.d.ts +251 -0
- package/dist/transcription/providers/azure-openai.d.ts +9 -0
- package/dist/transcription/providers/codex-subscription.d.ts +9 -0
- package/dist/transcription/providers/openai.d.ts +9 -0
- package/dist/transcription/service.d.ts +10 -0
- package/dist/workspace-state-projection.d.ts +16 -0
- package/package.json +13 -13
- package/src/app.ts +75 -5
- package/src/github-access.ts +1 -0
- package/src/integrations/google-drive.ts +869 -0
- package/src/integrations/oauth-client.ts +41 -6
- package/src/integrations/slack-bot.ts +756 -26
- package/src/mcp/server.ts +226 -42
- package/src/mcp/session-view.ts +1 -0
- package/src/mcp/toolspace.ts +17 -9
- package/src/routes/connections.ts +137 -7
- package/src/routes/documents.ts +13 -2
- package/src/routes/github.ts +327 -20
- package/src/routes/insights.ts +30 -0
- package/src/routes/preference-registry.ts +482 -0
- package/src/routes/sessions.ts +22 -23
- package/src/routes/transcriptions.ts +155 -0
- package/src/routes/workspace-capture.ts +108 -8
- package/src/routes/workspace-state.ts +61 -0
- package/src/sandbox/channel-a.ts +2 -0
- package/src/sandbox/rematerialize.ts +111 -4
- package/src/sandbox/viewer.ts +29 -12
- package/src/transcription/providers/azure-openai.ts +45 -0
- package/src/transcription/providers/codex-subscription.ts +100 -0
- package/src/transcription/providers/openai.ts +93 -0
- package/src/transcription/service.ts +121 -0
- package/src/workspace-state-projection.ts +244 -0
- package/dist/chunk-SD5GJTZ4.js.map +0 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActivatePreferenceRegistryRevisionRequest,
|
|
3
|
+
ChangePreferenceRegistryScopeRequest,
|
|
4
|
+
CorrectPreferenceRegistryRequest,
|
|
5
|
+
CreatePreferenceRegistryProposalRequest,
|
|
6
|
+
DeactivatePreferenceRegistryRequest,
|
|
7
|
+
PreferenceRegistryConflictResponse,
|
|
8
|
+
PreferenceRegistryDetailResponse,
|
|
9
|
+
PreferenceRegistryFullContentRequest,
|
|
10
|
+
PreferenceRegistryFullContent,
|
|
11
|
+
PreferenceRegistryListQuery,
|
|
12
|
+
PreferenceRegistryListResponse,
|
|
13
|
+
PreferenceRegistryMutationResponse,
|
|
14
|
+
PreferenceRegistryRecord,
|
|
15
|
+
PreferenceRegistrySnapshot,
|
|
16
|
+
RejectPreferenceRegistryProposalRequest,
|
|
17
|
+
SupersedePreferenceRegistryRequest,
|
|
18
|
+
type AccessGrant,
|
|
19
|
+
type PreferenceRegistryScope,
|
|
20
|
+
} from "@opengeni/contracts";
|
|
21
|
+
import {
|
|
22
|
+
hasPermission,
|
|
23
|
+
requireAccessGrant,
|
|
24
|
+
requireAccessGrantAuthorization,
|
|
25
|
+
type AccessGrantAuthorization,
|
|
26
|
+
type ApiRouteDeps,
|
|
27
|
+
} from "@opengeni/core";
|
|
28
|
+
import {
|
|
29
|
+
activatePreferenceRegistryRevision,
|
|
30
|
+
changePreferenceRegistryScope,
|
|
31
|
+
correctPreferenceRegistry,
|
|
32
|
+
createPreferenceRegistryProposal,
|
|
33
|
+
deactivatePreferenceRegistry,
|
|
34
|
+
getOrCreatePreferenceRegistrySnapshot,
|
|
35
|
+
getPreferenceRegistryDetail,
|
|
36
|
+
getPreferenceRegistryDetailForAttempt,
|
|
37
|
+
getPreferenceRegistryFullContent,
|
|
38
|
+
listPreferenceRegistry,
|
|
39
|
+
listPreferenceRegistryForAttempt,
|
|
40
|
+
PreferenceRegistryConflictError,
|
|
41
|
+
PreferenceRegistryInitiatorError,
|
|
42
|
+
PreferenceRegistryInvalidOperationError,
|
|
43
|
+
PreferenceRegistryNotFoundError,
|
|
44
|
+
PreferenceRegistryStableKeyConflictError,
|
|
45
|
+
rejectPreferenceRegistryProposal,
|
|
46
|
+
supersedePreferenceRegistry,
|
|
47
|
+
type PreferenceRegistryAttemptClaims,
|
|
48
|
+
} from "@opengeni/db";
|
|
49
|
+
import type { Context, Hono } from "hono";
|
|
50
|
+
import { HTTPException } from "hono/http-exception";
|
|
51
|
+
import { z } from "zod";
|
|
52
|
+
|
|
53
|
+
const Id = z.string().uuid();
|
|
54
|
+
|
|
55
|
+
async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
56
|
+
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
57
|
+
if (!parsed.success)
|
|
58
|
+
throw new HTTPException(422, { message: "Invalid preference registry request" });
|
|
59
|
+
return parsed.data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exactAttemptClaims(grant: AccessGrant) {
|
|
63
|
+
const metadata = grant.metadata ?? {};
|
|
64
|
+
const values = {
|
|
65
|
+
sessionId: metadata["sessionId"],
|
|
66
|
+
turnId: metadata["turnId"],
|
|
67
|
+
attemptId: metadata["attemptId"],
|
|
68
|
+
executionGeneration: metadata["executionGeneration"],
|
|
69
|
+
};
|
|
70
|
+
const present = Object.values(values).filter((value) => value !== undefined).length;
|
|
71
|
+
if (present === 0) return null;
|
|
72
|
+
if (
|
|
73
|
+
typeof values.sessionId !== "string" ||
|
|
74
|
+
typeof values.turnId !== "string" ||
|
|
75
|
+
typeof values.attemptId !== "string" ||
|
|
76
|
+
typeof values.executionGeneration !== "number" ||
|
|
77
|
+
!Number.isSafeInteger(values.executionGeneration) ||
|
|
78
|
+
values.executionGeneration < 1 ||
|
|
79
|
+
values.executionGeneration > 2_147_483_647
|
|
80
|
+
) {
|
|
81
|
+
throw new HTTPException(403, { message: "Incomplete signed preference attempt authority" });
|
|
82
|
+
}
|
|
83
|
+
return values as {
|
|
84
|
+
sessionId: string;
|
|
85
|
+
turnId: string;
|
|
86
|
+
attemptId: string;
|
|
87
|
+
executionGeneration: number;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requiredAttemptClaims(grant: AccessGrant): PreferenceRegistryAttemptClaims {
|
|
92
|
+
const claims = exactAttemptClaims(grant);
|
|
93
|
+
if (!claims) {
|
|
94
|
+
throw new HTTPException(403, {
|
|
95
|
+
message: "Preference snapshot retrieval requires a signed session, turn, and attempt",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
accountId: grant.accountId,
|
|
100
|
+
workspaceId: grant.workspaceId,
|
|
101
|
+
...claims,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function requireHumanMutation(access: AccessGrantAuthorization): void {
|
|
106
|
+
const { grant } = access;
|
|
107
|
+
if (
|
|
108
|
+
!access.contextIntegrity ||
|
|
109
|
+
access.authenticatedSubjectId !== grant.subjectId ||
|
|
110
|
+
grant.principalKind !== "human_session" ||
|
|
111
|
+
exactAttemptClaims(grant) !== null ||
|
|
112
|
+
grant.serviceInitiator ||
|
|
113
|
+
grant.serviceInitiatorContext ||
|
|
114
|
+
grant.subjectId.startsWith("api_key:")
|
|
115
|
+
) {
|
|
116
|
+
throw new HTTPException(403, {
|
|
117
|
+
message: "Preference activation and scope mutation require a direct human-authorized request",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function authorizePreferenceRegistryScopeMutation(
|
|
123
|
+
access: AccessGrantAuthorization,
|
|
124
|
+
scope: PreferenceRegistryScope,
|
|
125
|
+
): void {
|
|
126
|
+
const { grant } = access;
|
|
127
|
+
requireHumanMutation(access);
|
|
128
|
+
if (scope === "organization") {
|
|
129
|
+
// Deliberately do not use hasPermission: workspace:admin must not expand to
|
|
130
|
+
// account:admin for an organization-wide preference. Account authority is
|
|
131
|
+
// carried separately from the authenticated context and must match this
|
|
132
|
+
// exact workspace grant's account and subject.
|
|
133
|
+
if (!access.accountGrant?.permissions.includes("account:admin")) {
|
|
134
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (scope === "workspace" && !hasPermission(grant.permissions, "workspace:admin")) {
|
|
139
|
+
throw new HTTPException(403, { message: "missing permission: workspace:admin" });
|
|
140
|
+
}
|
|
141
|
+
// User scope is always self-only: the route derives the target from the
|
|
142
|
+
// authenticated actor and never accepts a subject selector.
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function preferenceError(context: Context, error: unknown): Response {
|
|
146
|
+
if (error instanceof PreferenceRegistryConflictError) {
|
|
147
|
+
return context.json(
|
|
148
|
+
PreferenceRegistryConflictResponse.parse({
|
|
149
|
+
code: error.code,
|
|
150
|
+
message: error.message,
|
|
151
|
+
currentRevisionId: error.currentRevisionId,
|
|
152
|
+
scopeVersion: error.scopeVersion,
|
|
153
|
+
}),
|
|
154
|
+
409,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (error instanceof PreferenceRegistryNotFoundError) {
|
|
158
|
+
return context.json({ code: "PREFERENCE_REGISTRY_NOT_FOUND", message: error.message }, 404);
|
|
159
|
+
}
|
|
160
|
+
if (error instanceof PreferenceRegistryStableKeyConflictError) {
|
|
161
|
+
return context.json({ code: error.code, message: error.message }, 409);
|
|
162
|
+
}
|
|
163
|
+
if (error instanceof PreferenceRegistryInvalidOperationError) {
|
|
164
|
+
return context.json(
|
|
165
|
+
{ code: "INVALID_PREFERENCE_REGISTRY_OPERATION", message: error.message },
|
|
166
|
+
422,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (error instanceof PreferenceRegistryInitiatorError) {
|
|
170
|
+
return context.json(
|
|
171
|
+
{ code: "PREFERENCE_REGISTRY_ATTEMPT_REJECTED", message: error.message },
|
|
172
|
+
403,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function preferenceId(context: Context): string {
|
|
179
|
+
const parsed = Id.safeParse(context.req.param("preferenceId"));
|
|
180
|
+
if (!parsed.success) throw new HTTPException(422, { message: "Invalid preference id" });
|
|
181
|
+
return parsed.data;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function registerPreferenceRegistryRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
185
|
+
const base = "/v1/workspaces/:workspaceId/preferences";
|
|
186
|
+
|
|
187
|
+
app.get(base, async (context) => {
|
|
188
|
+
const workspaceId = context.req.param("workspaceId");
|
|
189
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
190
|
+
const query = PreferenceRegistryListQuery.safeParse({
|
|
191
|
+
scope: context.req.query("scope"),
|
|
192
|
+
status: context.req.query("status"),
|
|
193
|
+
limit: context.req.query("limit"),
|
|
194
|
+
});
|
|
195
|
+
if (!query.success)
|
|
196
|
+
throw new HTTPException(422, { message: "Invalid preference registry query" });
|
|
197
|
+
try {
|
|
198
|
+
const claims = exactAttemptClaims(grant);
|
|
199
|
+
const result = claims
|
|
200
|
+
? await listPreferenceRegistryForAttempt(deps.db, {
|
|
201
|
+
accountId: grant.accountId,
|
|
202
|
+
workspaceId,
|
|
203
|
+
...claims,
|
|
204
|
+
...query.data,
|
|
205
|
+
})
|
|
206
|
+
: await listPreferenceRegistry(deps.db, {
|
|
207
|
+
workspaceId,
|
|
208
|
+
subjectId: grant.subjectId,
|
|
209
|
+
...query.data,
|
|
210
|
+
});
|
|
211
|
+
return context.json(PreferenceRegistryListResponse.parse(result));
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return preferenceError(context, error);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.post(`${base}/proposals`, async (context) => {
|
|
218
|
+
const workspaceId = context.req.param("workspaceId");
|
|
219
|
+
const access = await requireAccessGrantAuthorization(
|
|
220
|
+
context,
|
|
221
|
+
deps,
|
|
222
|
+
workspaceId,
|
|
223
|
+
"workspace:read",
|
|
224
|
+
);
|
|
225
|
+
const { grant } = access;
|
|
226
|
+
const request = await parseBody(context, CreatePreferenceRegistryProposalRequest);
|
|
227
|
+
authorizePreferenceRegistryScopeMutation(access, request.scope);
|
|
228
|
+
try {
|
|
229
|
+
return context.json(
|
|
230
|
+
PreferenceRegistryRecord.parse(
|
|
231
|
+
await createPreferenceRegistryProposal(deps.db, {
|
|
232
|
+
...request,
|
|
233
|
+
accountId: grant.accountId,
|
|
234
|
+
workspaceId,
|
|
235
|
+
actorSubjectId: grant.subjectId,
|
|
236
|
+
principalKind: grant.principalKind,
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
201,
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return preferenceError(context, error);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
app.get(`${base}/summary`, async (context) => {
|
|
247
|
+
const workspaceId = context.req.param("workspaceId");
|
|
248
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
249
|
+
try {
|
|
250
|
+
return context.json(
|
|
251
|
+
PreferenceRegistrySnapshot.parse(
|
|
252
|
+
await getOrCreatePreferenceRegistrySnapshot(deps.db, requiredAttemptClaims(grant)),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return preferenceError(context, error);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
app.post(`${base}/full-content`, async (context) => {
|
|
261
|
+
const workspaceId = context.req.param("workspaceId");
|
|
262
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
263
|
+
const request = await parseBody(context, PreferenceRegistryFullContentRequest);
|
|
264
|
+
try {
|
|
265
|
+
return context.json(
|
|
266
|
+
PreferenceRegistryFullContent.parse(
|
|
267
|
+
await getPreferenceRegistryFullContent(
|
|
268
|
+
deps.db,
|
|
269
|
+
requiredAttemptClaims(grant),
|
|
270
|
+
request.retrievalHandle,
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return preferenceError(context, error);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
app.get(`${base}/:preferenceId`, async (context) => {
|
|
280
|
+
const workspaceId = context.req.param("workspaceId");
|
|
281
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
282
|
+
try {
|
|
283
|
+
const claims = exactAttemptClaims(grant);
|
|
284
|
+
const id = preferenceId(context);
|
|
285
|
+
const detail = claims
|
|
286
|
+
? await getPreferenceRegistryDetailForAttempt(deps.db, {
|
|
287
|
+
accountId: grant.accountId,
|
|
288
|
+
workspaceId,
|
|
289
|
+
...claims,
|
|
290
|
+
preferenceId: id,
|
|
291
|
+
})
|
|
292
|
+
: await getPreferenceRegistryDetail(deps.db, {
|
|
293
|
+
workspaceId,
|
|
294
|
+
subjectId: grant.subjectId,
|
|
295
|
+
preferenceId: id,
|
|
296
|
+
});
|
|
297
|
+
return context.json(PreferenceRegistryDetailResponse.parse(detail));
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return preferenceError(context, error);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
app.post(`${base}/:preferenceId/activate`, async (context) => {
|
|
304
|
+
const workspaceId = context.req.param("workspaceId");
|
|
305
|
+
const access = await requireAccessGrantAuthorization(
|
|
306
|
+
context,
|
|
307
|
+
deps,
|
|
308
|
+
workspaceId,
|
|
309
|
+
"workspace:read",
|
|
310
|
+
);
|
|
311
|
+
const { grant } = access;
|
|
312
|
+
const id = preferenceId(context);
|
|
313
|
+
const request = await parseBody(context, ActivatePreferenceRegistryRevisionRequest);
|
|
314
|
+
try {
|
|
315
|
+
return context.json(
|
|
316
|
+
PreferenceRegistryMutationResponse.parse(
|
|
317
|
+
await activatePreferenceRegistryRevision(deps.db, {
|
|
318
|
+
accountId: grant.accountId,
|
|
319
|
+
workspaceId,
|
|
320
|
+
actorSubjectId: grant.subjectId,
|
|
321
|
+
principalKind: grant.principalKind,
|
|
322
|
+
preferenceId: id,
|
|
323
|
+
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
324
|
+
...request,
|
|
325
|
+
}),
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return preferenceError(context, error);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
app.post(`${base}/:preferenceId/correct`, async (context) => {
|
|
334
|
+
const workspaceId = context.req.param("workspaceId");
|
|
335
|
+
const access = await requireAccessGrantAuthorization(
|
|
336
|
+
context,
|
|
337
|
+
deps,
|
|
338
|
+
workspaceId,
|
|
339
|
+
"workspace:read",
|
|
340
|
+
);
|
|
341
|
+
const { grant } = access;
|
|
342
|
+
const id = preferenceId(context);
|
|
343
|
+
const request = await parseBody(context, CorrectPreferenceRegistryRequest);
|
|
344
|
+
try {
|
|
345
|
+
return context.json(
|
|
346
|
+
PreferenceRegistryMutationResponse.parse(
|
|
347
|
+
await correctPreferenceRegistry(deps.db, {
|
|
348
|
+
...request,
|
|
349
|
+
accountId: grant.accountId,
|
|
350
|
+
workspaceId,
|
|
351
|
+
actorSubjectId: grant.subjectId,
|
|
352
|
+
principalKind: grant.principalKind,
|
|
353
|
+
preferenceId: id,
|
|
354
|
+
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
355
|
+
}),
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return preferenceError(context, error);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
app.post(`${base}/:preferenceId/scope`, async (context) => {
|
|
364
|
+
const workspaceId = context.req.param("workspaceId");
|
|
365
|
+
const access = await requireAccessGrantAuthorization(
|
|
366
|
+
context,
|
|
367
|
+
deps,
|
|
368
|
+
workspaceId,
|
|
369
|
+
"workspace:read",
|
|
370
|
+
);
|
|
371
|
+
const { grant } = access;
|
|
372
|
+
const id = preferenceId(context);
|
|
373
|
+
const request = await parseBody(context, ChangePreferenceRegistryScopeRequest);
|
|
374
|
+
try {
|
|
375
|
+
return context.json(
|
|
376
|
+
PreferenceRegistryMutationResponse.parse(
|
|
377
|
+
await changePreferenceRegistryScope(deps.db, {
|
|
378
|
+
...request,
|
|
379
|
+
accountId: grant.accountId,
|
|
380
|
+
workspaceId,
|
|
381
|
+
actorSubjectId: grant.subjectId,
|
|
382
|
+
principalKind: grant.principalKind,
|
|
383
|
+
preferenceId: id,
|
|
384
|
+
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
385
|
+
}),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return preferenceError(context, error);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
app.post(`${base}/:preferenceId/deactivate`, async (context) => {
|
|
394
|
+
const workspaceId = context.req.param("workspaceId");
|
|
395
|
+
const access = await requireAccessGrantAuthorization(
|
|
396
|
+
context,
|
|
397
|
+
deps,
|
|
398
|
+
workspaceId,
|
|
399
|
+
"workspace:read",
|
|
400
|
+
);
|
|
401
|
+
const { grant } = access;
|
|
402
|
+
const id = preferenceId(context);
|
|
403
|
+
const request = await parseBody(context, DeactivatePreferenceRegistryRequest);
|
|
404
|
+
try {
|
|
405
|
+
return context.json(
|
|
406
|
+
PreferenceRegistryMutationResponse.parse(
|
|
407
|
+
await deactivatePreferenceRegistry(deps.db, {
|
|
408
|
+
...request,
|
|
409
|
+
accountId: grant.accountId,
|
|
410
|
+
workspaceId,
|
|
411
|
+
actorSubjectId: grant.subjectId,
|
|
412
|
+
principalKind: grant.principalKind,
|
|
413
|
+
preferenceId: id,
|
|
414
|
+
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return preferenceError(context, error);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
app.post(`${base}/:preferenceId/supersede`, async (context) => {
|
|
424
|
+
const workspaceId = context.req.param("workspaceId");
|
|
425
|
+
const access = await requireAccessGrantAuthorization(
|
|
426
|
+
context,
|
|
427
|
+
deps,
|
|
428
|
+
workspaceId,
|
|
429
|
+
"workspace:read",
|
|
430
|
+
);
|
|
431
|
+
const { grant } = access;
|
|
432
|
+
const id = preferenceId(context);
|
|
433
|
+
const request = await parseBody(context, SupersedePreferenceRegistryRequest);
|
|
434
|
+
try {
|
|
435
|
+
return context.json(
|
|
436
|
+
PreferenceRegistryMutationResponse.parse(
|
|
437
|
+
await supersedePreferenceRegistry(deps.db, {
|
|
438
|
+
...request,
|
|
439
|
+
accountId: grant.accountId,
|
|
440
|
+
workspaceId,
|
|
441
|
+
actorSubjectId: grant.subjectId,
|
|
442
|
+
principalKind: grant.principalKind,
|
|
443
|
+
preferenceId: id,
|
|
444
|
+
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
445
|
+
}),
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return preferenceError(context, error);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
app.post(`${base}/:preferenceId/reject`, async (context) => {
|
|
454
|
+
const workspaceId = context.req.param("workspaceId");
|
|
455
|
+
const access = await requireAccessGrantAuthorization(
|
|
456
|
+
context,
|
|
457
|
+
deps,
|
|
458
|
+
workspaceId,
|
|
459
|
+
"workspace:read",
|
|
460
|
+
);
|
|
461
|
+
const { grant } = access;
|
|
462
|
+
const id = preferenceId(context);
|
|
463
|
+
const request = await parseBody(context, RejectPreferenceRegistryProposalRequest);
|
|
464
|
+
try {
|
|
465
|
+
return context.json(
|
|
466
|
+
PreferenceRegistryMutationResponse.parse(
|
|
467
|
+
await rejectPreferenceRegistryProposal(deps.db, {
|
|
468
|
+
...request,
|
|
469
|
+
accountId: grant.accountId,
|
|
470
|
+
workspaceId,
|
|
471
|
+
actorSubjectId: grant.subjectId,
|
|
472
|
+
principalKind: grant.principalKind,
|
|
473
|
+
preferenceId: id,
|
|
474
|
+
authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
|
|
475
|
+
}),
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return preferenceError(context, error);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
listSessionHumanInputRequests,
|
|
77
77
|
listSessionIdsInGroup,
|
|
78
78
|
listSessionsForSubject,
|
|
79
|
+
getLatestStartedSessionTurn,
|
|
79
80
|
listSessionTurns,
|
|
80
81
|
projectEffectiveControlForRelatedAccess,
|
|
81
82
|
projectSessionForRelatedAccess,
|
|
@@ -102,6 +103,7 @@ import {
|
|
|
102
103
|
SessionContextBusyError,
|
|
103
104
|
HumanInputResponseValidationError,
|
|
104
105
|
latestWorkspaceCapture,
|
|
106
|
+
sessionLatestWorkspaceCapture,
|
|
105
107
|
workspaceCaptureAtRevision,
|
|
106
108
|
type AppendEventInput,
|
|
107
109
|
type SandboxOpenPtySessionRow,
|
|
@@ -167,12 +169,17 @@ import {
|
|
|
167
169
|
} from "@opengeni/core";
|
|
168
170
|
import { assertSessionExists, boundedLimit } from "../http/common";
|
|
169
171
|
import { sseSessionStream } from "../http/sse";
|
|
170
|
-
import {
|
|
172
|
+
import {
|
|
173
|
+
serveWorkspaceCapture,
|
|
174
|
+
serveWorkspaceCaptureFile,
|
|
175
|
+
WorkspaceCaptureManifestCache,
|
|
176
|
+
} from "./workspace-capture";
|
|
171
177
|
|
|
172
178
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
173
179
|
|
|
174
180
|
export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
175
181
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
182
|
+
const workspaceCaptureManifestCache = new WorkspaceCaptureManifestCache();
|
|
176
183
|
const ptyIdentity = (pty: SandboxOpenPtySessionRow): SandboxPtyProcessIdentity => ({
|
|
177
184
|
leaseId: pty.leaseId,
|
|
178
185
|
sandboxGroupId: pty.sandboxGroupId,
|
|
@@ -1071,6 +1078,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1071
1078
|
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1072
1079
|
const sessionId = c.req.param("sessionId");
|
|
1073
1080
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
1081
|
+
// Exact turn that most recently emitted durable `turn.started` — the same
|
|
1082
|
+
// boundary goal continuations use for inherited model/effort. Queued-only
|
|
1083
|
+
// or preflight-rejected turns are deliberately excluded.
|
|
1084
|
+
if (c.req.query("latestStarted") === "1" || c.req.query("latestStarted") === "true") {
|
|
1085
|
+
const latest = await getLatestStartedSessionTurn(db, workspaceId, sessionId);
|
|
1086
|
+
return c.json(latest ? [latest] : []);
|
|
1087
|
+
}
|
|
1074
1088
|
return c.json(
|
|
1075
1089
|
await listSessionTurns(db, workspaceId, sessionId, boundedLimit(c.req.query("limit"))),
|
|
1076
1090
|
);
|
|
@@ -1276,10 +1290,9 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1276
1290
|
text: payload.text,
|
|
1277
1291
|
turnInstructions: payload.turnInstructions ?? null,
|
|
1278
1292
|
resources: payload.resources,
|
|
1279
|
-
tools: payload.tools,
|
|
1280
|
-
toolsProvided: userMessagePayloadHasOwnProperty({ payload: raw }, "tools"),
|
|
1281
1293
|
model: payload.model ?? null,
|
|
1282
1294
|
reasoningEffort: payload.reasoningEffort ?? null,
|
|
1295
|
+
latencyMode: payload.latencyMode ?? null,
|
|
1283
1296
|
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
1284
1297
|
delivery: "steer",
|
|
1285
1298
|
origin: "human",
|
|
@@ -1320,10 +1333,9 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1320
1333
|
text: event.payload.text,
|
|
1321
1334
|
turnInstructions: event.payload.turnInstructions ?? null,
|
|
1322
1335
|
resources: event.payload.resources ?? [],
|
|
1323
|
-
tools: event.payload.tools ?? [],
|
|
1324
|
-
toolsProvided: userMessagePayloadHasOwnProperty(rawEvent, "tools"),
|
|
1325
1336
|
model: event.payload.model ?? null,
|
|
1326
1337
|
reasoningEffort: event.payload.reasoningEffort ?? null,
|
|
1338
|
+
latencyMode: event.payload.latencyMode ?? null,
|
|
1327
1339
|
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
1328
1340
|
...(event.payload.controlEtag !== undefined
|
|
1329
1341
|
? { controlEtag: event.payload.controlEtag }
|
|
@@ -2120,16 +2132,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2120
2132
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
2121
2133
|
await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
2122
2134
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
2123
|
-
const
|
|
2124
|
-
if (!
|
|
2135
|
+
const lookup = await sessionLatestWorkspaceCapture(db, workspaceId, sessionId);
|
|
2136
|
+
if (!lookup.sessionExists) {
|
|
2125
2137
|
throw new HTTPException(404, { message: "session not found" });
|
|
2126
2138
|
}
|
|
2127
2139
|
if (!objectStorage) {
|
|
2128
2140
|
// No storage configured → no captures can exist. Cold-fallback, not an error.
|
|
2129
2141
|
return c.json({ available: false });
|
|
2130
2142
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2143
|
+
return c.json(
|
|
2144
|
+
await serveWorkspaceCapture(lookup.capture, objectStorage, workspaceCaptureManifestCache),
|
|
2145
|
+
);
|
|
2133
2146
|
});
|
|
2134
2147
|
|
|
2135
2148
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture/file", async (c) => {
|
|
@@ -2650,20 +2663,6 @@ function optionalEventSequence(raw: string | undefined): number | undefined {
|
|
|
2650
2663
|
return Math.floor(sequence);
|
|
2651
2664
|
}
|
|
2652
2665
|
|
|
2653
|
-
function hasOwnProperty(value: unknown, key: string): boolean {
|
|
2654
|
-
return Boolean(
|
|
2655
|
-
value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key),
|
|
2656
|
-
);
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
function userMessagePayloadHasOwnProperty(value: unknown, key: string): boolean {
|
|
2660
|
-
if (!value || typeof value !== "object") {
|
|
2661
|
-
return false;
|
|
2662
|
-
}
|
|
2663
|
-
const payload = (value as { payload?: unknown }).payload;
|
|
2664
|
-
return hasOwnProperty(payload, key);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
2666
|
/** Stable, value-free JSON errors for only the create-session boundary. */
|
|
2668
2667
|
export function sessionCreateErrorResponse(c: Context, error: unknown): Response {
|
|
2669
2668
|
if (error instanceof SessionSpawnDeniedError) {
|