@opengeni/api-router 3.0.1 → 4.0.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/admission-trace.d.ts +8 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
- package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
- package/dist/connection-ownership.d.ts +1 -2
- package/dist/fatal-process-boundary.d.ts +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +5 -65
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +119 -11
- package/dist/integrations/oauth-profiles.d.ts +8 -13
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +22 -1
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/codex.d.ts +28 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/sessions.d.ts +24 -2
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/workspace-tool-gateway.d.ts +3 -1
- package/package.json +15 -15
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +141 -94
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/connection-ownership.ts +2 -9
- package/src/editable-artifact-production.ts +6 -0
- package/src/fatal-process-boundary.ts +17 -2
- package/src/index.ts +3 -4
- package/src/integrations/atlassian.ts +184 -76
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +51 -15
- package/src/integrations/github-lens-connect.ts +7 -28
- package/src/integrations/google-drive.ts +296 -125
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +231 -132
- package/src/integrations/oauth-profiles.ts +10 -45
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +57 -82
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/editable-artifacts.ts +7 -2
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +25 -8
- package/src/mcp/server.ts +135 -43
- package/src/mcp-oauth.ts +336 -34
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/automations.ts +5 -13
- package/src/routes/billing.ts +52 -0
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/codex.ts +4 -2
- package/src/routes/connect.ts +90 -31
- package/src/routes/connections.ts +487 -307
- package/src/routes/files.ts +21 -12
- package/src/routes/github.ts +48 -19
- package/src/routes/insights.ts +17 -7
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/organization-memberships.ts +22 -2
- package/src/routes/plugins.ts +42 -4
- package/src/routes/pr-review-github.ts +67 -57
- package/src/routes/pr-review.ts +74 -71
- package/src/routes/rigs.ts +32 -13
- package/src/routes/scheduled-tasks.ts +6 -6
- package/src/routes/sessions.ts +233 -11
- package/src/routes/skill-content.ts +19 -3
- package/src/routes/social.ts +24 -13
- package/src/sandbox/channel-a.ts +7 -1
- package/src/sandbox/rematerialize.ts +2 -1
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/workspace-tool-gateway.ts +31 -31
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
package/src/routes/rigs.ts
CHANGED
|
@@ -51,7 +51,7 @@ async function parseRigRequest<T>(c: Context, schema: ZodType<T>, label: string)
|
|
|
51
51
|
throw new ApiHttpError(422, {
|
|
52
52
|
code: "validation_failed",
|
|
53
53
|
message: imageOverrideUnsupported
|
|
54
|
-
? "
|
|
54
|
+
? "Sandbox Environment base-image overrides are not supported; Sandbox Environments use the deployment-managed platform sandbox."
|
|
55
55
|
: `Invalid ${label}.`,
|
|
56
56
|
retryable: false,
|
|
57
57
|
details: {
|
|
@@ -77,7 +77,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
77
77
|
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId, permission);
|
|
78
78
|
const rigId = c.req.param("rigId");
|
|
79
79
|
if (!rigId) {
|
|
80
|
-
throw new HTTPException(400, { message: "
|
|
80
|
+
throw new HTTPException(400, { message: "sandbox environment id is required" });
|
|
81
81
|
}
|
|
82
82
|
const rig = await requireRigForApi(db, authorization.grant, rigId);
|
|
83
83
|
if (
|
|
@@ -126,7 +126,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
126
126
|
// The verifying transition already committed. Keep its deterministic
|
|
127
127
|
// attempt identity retryable instead of reporting an opaque 5xx after a
|
|
128
128
|
// durable mutation or inventing a second attempt after an ambiguous start.
|
|
129
|
-
deps.observability?.warn("
|
|
129
|
+
deps.observability?.warn("sandbox environment change verification start failed", {
|
|
130
130
|
workspaceId,
|
|
131
131
|
changeId,
|
|
132
132
|
attempt,
|
|
@@ -163,11 +163,14 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
163
163
|
// Creation already committed. The rig remains fully usable through its
|
|
164
164
|
// runtime setup fallback and can be re-verified explicitly; never turn a
|
|
165
165
|
// successful create into an unretryable 5xx because Temporal was down.
|
|
166
|
-
deps.observability?.warn(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
deps.observability?.warn(
|
|
167
|
+
"initial sandbox environment provider-image verification start failed",
|
|
168
|
+
{
|
|
169
|
+
workspaceId,
|
|
170
|
+
versionId,
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
},
|
|
173
|
+
);
|
|
171
174
|
return false;
|
|
172
175
|
}
|
|
173
176
|
}
|
|
@@ -183,7 +186,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
183
186
|
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
184
187
|
const grant = authorization.grant;
|
|
185
188
|
requirePermission(grant, "rigs:manage");
|
|
186
|
-
const payload = await parseRigRequest(
|
|
189
|
+
const payload = await parseRigRequest(
|
|
190
|
+
c,
|
|
191
|
+
CreateRigRequest,
|
|
192
|
+
"Sandbox Environment create request",
|
|
193
|
+
);
|
|
187
194
|
const allowOrganization =
|
|
188
195
|
payload.scope === "organization" &&
|
|
189
196
|
authorization.accountGrant?.permissions.includes("account:admin") === true;
|
|
@@ -222,7 +229,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
222
229
|
message: "missing permission: account:admin",
|
|
223
230
|
});
|
|
224
231
|
}
|
|
225
|
-
const payload = await parseRigRequest(
|
|
232
|
+
const payload = await parseRigRequest(
|
|
233
|
+
c,
|
|
234
|
+
UpdateRigRequest,
|
|
235
|
+
"Sandbox Environment update request",
|
|
236
|
+
);
|
|
226
237
|
return c.json(await updateRigForApi({ db }, grant, rig, payload, { allowOrganization }));
|
|
227
238
|
});
|
|
228
239
|
|
|
@@ -254,7 +265,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
254
265
|
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
|
|
255
266
|
const workspaceId = c.req.param("workspaceId");
|
|
256
267
|
const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:manage");
|
|
257
|
-
const payload = await parseRigRequest(
|
|
268
|
+
const payload = await parseRigRequest(
|
|
269
|
+
c,
|
|
270
|
+
RigDefinitionEditPayload,
|
|
271
|
+
"Sandbox Environment version request",
|
|
272
|
+
);
|
|
258
273
|
const version = await createRigVersionForApi({ db }, grant, rig, payload);
|
|
259
274
|
const started = await tryStartInitialVersionVerification(rig.workspaceId, version.id);
|
|
260
275
|
if (!started) c.header("OpenGeni-Rig-Verification", "deferred");
|
|
@@ -289,7 +304,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
289
304
|
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
|
|
290
305
|
const workspaceId = c.req.param("workspaceId");
|
|
291
306
|
const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:use");
|
|
292
|
-
const request = await parseRigRequest(
|
|
307
|
+
const request = await parseRigRequest(
|
|
308
|
+
c,
|
|
309
|
+
ProposeRigChangeRequest,
|
|
310
|
+
"Sandbox Environment change request",
|
|
311
|
+
);
|
|
293
312
|
const change = await proposeRigChangeForApi({ db }, grant, rig, request);
|
|
294
313
|
const verification = await startChangeVerification(rig.workspaceId, change.id);
|
|
295
314
|
if (!verification.started) c.header("OpenGeni-Rig-Verification", "deferred");
|
|
@@ -336,7 +355,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
336
355
|
const workspaceId = c.req.param("workspaceId");
|
|
337
356
|
const { rig } = await requireRigMutation(c, workspaceId, "rigs:use");
|
|
338
357
|
if (!rig.activeVersion) {
|
|
339
|
-
return c.json({ error: "
|
|
358
|
+
return c.json({ error: "sandbox environment has no active version" }, 422);
|
|
340
359
|
}
|
|
341
360
|
await startVersionVerification(rig.workspaceId, rig.activeVersion.id);
|
|
342
361
|
return c.json({ ok: true, versionId: rig.activeVersion.id }, 202);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
syncCreatedScheduledTask,
|
|
28
28
|
syncUpdatedScheduledTask,
|
|
29
29
|
updateScheduledTaskForApi,
|
|
30
|
+
triggerScheduledTaskForGrant,
|
|
30
31
|
validateScheduledTaskMachineTarget,
|
|
31
32
|
validateScheduledTaskTarget,
|
|
32
33
|
validatedScheduledTaskUpdate,
|
|
@@ -152,7 +153,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
152
153
|
});
|
|
153
154
|
const task = await updateScheduledTaskForApi(
|
|
154
155
|
db,
|
|
155
|
-
|
|
156
|
+
grant,
|
|
156
157
|
taskId,
|
|
157
158
|
update,
|
|
158
159
|
payload.agentLearning
|
|
@@ -168,7 +169,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
168
169
|
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
169
170
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
170
171
|
const previous = await captureScheduledTaskRestoreState(db, existing);
|
|
171
|
-
const task = await updateScheduledTaskForApi(db,
|
|
172
|
+
const task = await updateScheduledTaskForApi(db, grant, existing.id, {
|
|
172
173
|
status: "paused",
|
|
173
174
|
});
|
|
174
175
|
await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
|
|
@@ -203,7 +204,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
203
204
|
sessionAuthorization: deps.sessionAuthorization,
|
|
204
205
|
authorizationSurface: "http",
|
|
205
206
|
});
|
|
206
|
-
const task = await updateScheduledTaskForApi(db,
|
|
207
|
+
const task = await updateScheduledTaskForApi(db, grant, existing.id, update);
|
|
207
208
|
await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
|
|
208
209
|
return c.json(scheduledTaskForGrant(task, grant));
|
|
209
210
|
});
|
|
@@ -262,7 +263,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
262
263
|
? manualScheduledTaskTriggerUsageKey(workspaceId, task.id, triggerToken)
|
|
263
264
|
: `knowledge-source-sync:manual:${workspaceId}:${task.id}:${triggerToken}`;
|
|
264
265
|
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
265
|
-
await workflowClient
|
|
266
|
+
await triggerScheduledTaskForGrant(db, grant, workflowClient, {
|
|
266
267
|
task,
|
|
267
268
|
agentRunUsageIdempotencyKey,
|
|
268
269
|
triggerWorkflowId,
|
|
@@ -288,9 +289,8 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
288
289
|
const workspaceId = c.req.param("workspaceId");
|
|
289
290
|
const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
290
291
|
await deleteScheduledTaskWithDurableCleanup(deps, {
|
|
291
|
-
|
|
292
|
+
grant,
|
|
292
293
|
taskId: c.req.param("taskId"),
|
|
293
|
-
subjectId: grant.subjectId,
|
|
294
294
|
});
|
|
295
295
|
return c.json({ ok: true });
|
|
296
296
|
});
|
package/src/routes/sessions.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
|
|
2
|
+
import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
|
|
3
|
+
import { SessionMessageSearchRequest } from "@opengeni/contracts";
|
|
2
4
|
import { scheduledSessionIds } from "@opengeni/db";
|
|
3
5
|
import { withSiteSessionOrigin } from "@opengeni/core";
|
|
4
6
|
import { resolveSiteSessionOrigin } from "../site-session-origin";
|
|
7
|
+
import { SandboxRecoveryRequest } from "@opengeni/contracts";
|
|
8
|
+
import { getManagedHumanSandboxRecovery, consentManagedHumanSandboxRecovery } from "@opengeni/core";
|
|
9
|
+
import { SandboxRecoveryConflictError } from "@opengeni/db";
|
|
10
|
+
import { codexAccountJson } from "./codex";
|
|
11
|
+
import { getSessionCodexAccounts } from "@opengeni/db";
|
|
5
12
|
import {
|
|
6
13
|
AcknowledgeStreamRequest,
|
|
7
14
|
ApplySessionGoalRevisionRequest,
|
|
@@ -43,6 +50,7 @@ import {
|
|
|
43
50
|
RenewSessionRealtimeRequest,
|
|
44
51
|
SyncSessionRealtimeLedgerRequest,
|
|
45
52
|
SessionControlRequest,
|
|
53
|
+
SessionRetryRequest,
|
|
46
54
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
47
55
|
SessionEventPayloadMode,
|
|
48
56
|
SessionEventReadDirection,
|
|
@@ -257,6 +265,7 @@ import {
|
|
|
257
265
|
type ResolvedSessionAuthorization,
|
|
258
266
|
} from "@opengeni/core";
|
|
259
267
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
268
|
+
import { SessionRetryConflictError } from "@opengeni/db";
|
|
260
269
|
import {
|
|
261
270
|
attachViewer,
|
|
262
271
|
detachViewer,
|
|
@@ -274,6 +283,7 @@ import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../co
|
|
|
274
283
|
import {
|
|
275
284
|
acceptSessionUserMessage,
|
|
276
285
|
controlHumanSessionWorkstream,
|
|
286
|
+
retryFailedSession,
|
|
277
287
|
createSessionForRequest,
|
|
278
288
|
deleteHumanQueuePrompt,
|
|
279
289
|
editHumanQueuePrompt,
|
|
@@ -310,6 +320,7 @@ import {
|
|
|
310
320
|
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
311
321
|
import { ApiHttpError } from "../http/api-error";
|
|
312
322
|
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
323
|
+
import { recordAcceptedApiAdmission } from "../admission-trace";
|
|
313
324
|
|
|
314
325
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
315
326
|
|
|
@@ -681,6 +692,59 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
681
692
|
}
|
|
682
693
|
});
|
|
683
694
|
|
|
695
|
+
app.get("/v1/workspaces/:workspaceId/session-message-search", async (c) => {
|
|
696
|
+
const workspaceId = c.req.param("workspaceId");
|
|
697
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
698
|
+
c,
|
|
699
|
+
deps,
|
|
700
|
+
workspaceId,
|
|
701
|
+
"sessions:read",
|
|
702
|
+
);
|
|
703
|
+
const grant = authorization.grant;
|
|
704
|
+
// A single-session filter is not authority: both searches use the complete
|
|
705
|
+
// live host/agent list scope, plus ordinary private/archive/RLS boundaries.
|
|
706
|
+
let authorizationScope;
|
|
707
|
+
try {
|
|
708
|
+
authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
|
|
709
|
+
} catch (error) {
|
|
710
|
+
throw sessionAuthorizationHttpError(error);
|
|
711
|
+
}
|
|
712
|
+
const raw = c.req.query();
|
|
713
|
+
const parsed = SessionMessageSearchRequest.safeParse({
|
|
714
|
+
...raw,
|
|
715
|
+
...(raw.limit !== undefined ? { limit: Number(raw.limit) } : {}),
|
|
716
|
+
});
|
|
717
|
+
if (!parsed.success)
|
|
718
|
+
throw new HTTPException(400, { message: "Invalid session message search request" });
|
|
719
|
+
try {
|
|
720
|
+
return c.json(
|
|
721
|
+
await searchSessionMessagesForSubject(
|
|
722
|
+
db,
|
|
723
|
+
workspaceId,
|
|
724
|
+
parsed.data,
|
|
725
|
+
{
|
|
726
|
+
subjectId: grant.subjectId,
|
|
727
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
728
|
+
personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
|
|
729
|
+
},
|
|
730
|
+
{ signal: c.req.raw.signal },
|
|
731
|
+
),
|
|
732
|
+
);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (error instanceof SessionListAccessError)
|
|
735
|
+
throw new HTTPException(403, { message: error.message });
|
|
736
|
+
if (error instanceof SessionMessageSearchCursorError)
|
|
737
|
+
throw new HTTPException(400, { message: error.message });
|
|
738
|
+
// Clients are told to cancel superseded searches, so an abort here is a
|
|
739
|
+
// routine client disconnect, never a server failure. Match the existing
|
|
740
|
+
// client-gone convention (mcp/request-abort.ts): 499 with an empty body,
|
|
741
|
+
// which a departed client never reads and monitors never count as 5xx.
|
|
742
|
+
if (c.req.raw.signal.aborted || (error instanceof Error && error.name === "AbortError"))
|
|
743
|
+
return new Response(null, { status: 499 });
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
684
748
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
685
749
|
const workspaceId = c.req.param("workspaceId");
|
|
686
750
|
const authorization = await requireAccessGrantAuthorization(
|
|
@@ -708,6 +772,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
708
772
|
...(query.search ? { search: query.search } : {}),
|
|
709
773
|
...(query.pinsOnly ? { pinsOnly: true } : {}),
|
|
710
774
|
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
775
|
+
...(query.sortBy ? { sortBy: query.sortBy } : {}),
|
|
776
|
+
...(query.archiveStatus ? { archiveStatus: query.archiveStatus } : {}),
|
|
711
777
|
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
712
778
|
...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
|
|
713
779
|
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
@@ -1944,6 +2010,31 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1944
2010
|
});
|
|
1945
2011
|
});
|
|
1946
2012
|
|
|
2013
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-accounts", async (c) => {
|
|
2014
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2015
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
2016
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
2017
|
+
// authorizeSessionHttp has already enforced private-session and agent scope.
|
|
2018
|
+
const projection = await getSessionCodexAccounts(db, workspaceId, c.req.param("sessionId"));
|
|
2019
|
+
if (!projection) throw new HTTPException(404, { message: "session not found" });
|
|
2020
|
+
const activeAccountId = projection.rotation?.activeCredentialId ?? null;
|
|
2021
|
+
return c.json({
|
|
2022
|
+
accounts: projection.accounts.map((account) => codexAccountJson(account)),
|
|
2023
|
+
activeAccountId,
|
|
2024
|
+
settings: {
|
|
2025
|
+
rotationEnabled: projection.rotation?.rotationEnabled ?? false,
|
|
2026
|
+
rotationStrategy: "sharded",
|
|
2027
|
+
activeCredentialId: activeAccountId,
|
|
2028
|
+
},
|
|
2029
|
+
currentSelection: projection.currentSelection,
|
|
2030
|
+
currentAccount: projection.currentAccount
|
|
2031
|
+
? codexAccountJson(projection.currentAccount)
|
|
2032
|
+
: null,
|
|
2033
|
+
pinnedAccountId: projection.pinnedAccountId,
|
|
2034
|
+
lastAccountId: projection.lastAccountId,
|
|
2035
|
+
});
|
|
2036
|
+
});
|
|
2037
|
+
|
|
1947
2038
|
// Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
|
|
1948
2039
|
// "auto" clears the pin (the session follows the workspace active pointer); a
|
|
1949
2040
|
// uuid pins the session to that specific account. Overrides a capacity-blocked
|
|
@@ -2569,10 +2660,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2569
2660
|
|
|
2570
2661
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
2571
2662
|
const workspaceId = c.req.param("workspaceId");
|
|
2572
|
-
await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2663
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
2573
2664
|
const sessionId = c.req.param("sessionId");
|
|
2574
2665
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2575
|
-
const { event } = await clearSessionGoal(db, workspaceId, sessionId
|
|
2666
|
+
const { event } = await clearSessionGoal(db, workspaceId, sessionId, {
|
|
2667
|
+
actor: grant.principalKind === "agent_attempt" ? "agent" : "api",
|
|
2668
|
+
}).catch((error: unknown) => {
|
|
2669
|
+
if (error instanceof SessionControlConflictError) {
|
|
2670
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
2671
|
+
}
|
|
2672
|
+
throw error;
|
|
2673
|
+
});
|
|
2576
2674
|
if (event) {
|
|
2577
2675
|
try {
|
|
2578
2676
|
await bus.publish(workspaceId, sessionId, [event]);
|
|
@@ -3059,6 +3157,112 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3059
3157
|
}
|
|
3060
3158
|
});
|
|
3061
3159
|
|
|
3160
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
|
|
3161
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3162
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
3163
|
+
try {
|
|
3164
|
+
return c.json(
|
|
3165
|
+
await getManagedHumanSandboxRecovery(
|
|
3166
|
+
deps,
|
|
3167
|
+
authorization,
|
|
3168
|
+
workspaceId,
|
|
3169
|
+
c.req.param("sessionId"),
|
|
3170
|
+
),
|
|
3171
|
+
);
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
throw sessionTenancyHttpError(error);
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
|
|
3177
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
|
|
3178
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3179
|
+
const sessionId = c.req.param("sessionId");
|
|
3180
|
+
const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
|
|
3181
|
+
const parsed = SandboxRecoveryRequest.safeParse(await c.req.json().catch(() => null));
|
|
3182
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid checkpoint consent" });
|
|
3183
|
+
let consentCommitted = false;
|
|
3184
|
+
try {
|
|
3185
|
+
const receipt = await consentManagedHumanSandboxRecovery(
|
|
3186
|
+
deps,
|
|
3187
|
+
authorization,
|
|
3188
|
+
workspaceId,
|
|
3189
|
+
sessionId,
|
|
3190
|
+
parsed.data,
|
|
3191
|
+
);
|
|
3192
|
+
consentCommitted = true;
|
|
3193
|
+
const projection = await getManagedHumanSandboxRecovery(
|
|
3194
|
+
deps,
|
|
3195
|
+
authorization,
|
|
3196
|
+
workspaceId,
|
|
3197
|
+
sessionId,
|
|
3198
|
+
);
|
|
3199
|
+
if (
|
|
3200
|
+
projection.operationId === receipt.operationId &&
|
|
3201
|
+
projection.status === "consent_accepted"
|
|
3202
|
+
) {
|
|
3203
|
+
// Establish/verify only. No failed-turn Retry, Send, tool callback or
|
|
3204
|
+
// command replay. The ordinary cold->warming election owns creation.
|
|
3205
|
+
const session = await getSession(db, workspaceId, sessionId);
|
|
3206
|
+
if (session) {
|
|
3207
|
+
try {
|
|
3208
|
+
await withChannelARead(
|
|
3209
|
+
channelAServices,
|
|
3210
|
+
{
|
|
3211
|
+
accountId: authorization.grant.accountId,
|
|
3212
|
+
workspaceId,
|
|
3213
|
+
session,
|
|
3214
|
+
subjectId: authorization.grant.subjectId,
|
|
3215
|
+
},
|
|
3216
|
+
async () => undefined,
|
|
3217
|
+
);
|
|
3218
|
+
} catch {
|
|
3219
|
+
// Consent committed. Never turn a provider failure into a false
|
|
3220
|
+
// rejection of consent or leak native provider bindings/messages.
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return c.json({
|
|
3225
|
+
...receipt,
|
|
3226
|
+
recovery: await getManagedHumanSandboxRecovery(deps, authorization, workspaceId, sessionId),
|
|
3227
|
+
});
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
if (consentCommitted) {
|
|
3230
|
+
// A later authorization/read failure cannot revoke the committed
|
|
3231
|
+
// receipt. Return no session state, checkpoint or provider details.
|
|
3232
|
+
return c.json(
|
|
3233
|
+
{
|
|
3234
|
+
code: "upstream_unavailable",
|
|
3235
|
+
message:
|
|
3236
|
+
"Recovery status could not be confirmed. Check status without resubmitting consent.",
|
|
3237
|
+
outcomeUnknown: true,
|
|
3238
|
+
retryable: false,
|
|
3239
|
+
},
|
|
3240
|
+
503,
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
if (error instanceof SandboxRecoveryConflictError)
|
|
3244
|
+
return c.json({ code: error.code, message: error.message }, 409);
|
|
3245
|
+
if (error instanceof SessionCommandIdempotencyError) return commandConflictResponse(c, error);
|
|
3246
|
+
throw sessionTenancyHttpError(error);
|
|
3247
|
+
}
|
|
3248
|
+
});
|
|
3249
|
+
|
|
3250
|
+
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/retry", async (c) => {
|
|
3251
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3252
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
3253
|
+
const parsed = SessionRetryRequest.safeParse(await c.req.json().catch(() => null));
|
|
3254
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid session retry request" });
|
|
3255
|
+
try {
|
|
3256
|
+
return c.json(
|
|
3257
|
+
await retryFailedSession(deps, grant, workspaceId, c.req.param("sessionId"), parsed.data),
|
|
3258
|
+
);
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
if (error instanceof SessionRetryConflictError)
|
|
3261
|
+
return c.json({ code: error.code, message: error.message }, 409);
|
|
3262
|
+
return commandConflictResponse(c, error);
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3062
3266
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/control", async (c) => {
|
|
3063
3267
|
const workspaceId = c.req.param("workspaceId");
|
|
3064
3268
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
@@ -3121,10 +3325,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3121
3325
|
reasoningEffort: payload.reasoningEffort ?? null,
|
|
3122
3326
|
latencyMode: payload.latencyMode ?? null,
|
|
3123
3327
|
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
3124
|
-
|
|
3125
|
-
...(payload.selectedHostMcpDelegations
|
|
3126
|
-
? { selectedHostMcpDelegations: payload.selectedHostMcpDelegations }
|
|
3127
|
-
: {}),
|
|
3328
|
+
connectionAccounts: payload.connectionAccounts,
|
|
3128
3329
|
...(payload.personalResourceAttachment
|
|
3129
3330
|
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
3130
3331
|
: {}),
|
|
@@ -3140,6 +3341,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3140
3341
|
} catch (error) {
|
|
3141
3342
|
return commandConflictResponse(c, error);
|
|
3142
3343
|
}
|
|
3344
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3143
3345
|
return c.json(result, 202);
|
|
3144
3346
|
});
|
|
3145
3347
|
|
|
@@ -3163,6 +3365,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3163
3365
|
} catch (error) {
|
|
3164
3366
|
return commandConflictResponse(c, error);
|
|
3165
3367
|
}
|
|
3368
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3166
3369
|
return c.json(result, 202);
|
|
3167
3370
|
});
|
|
3168
3371
|
|
|
@@ -3206,10 +3409,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3206
3409
|
reasoningEffort: event.payload.reasoningEffort ?? null,
|
|
3207
3410
|
latencyMode: event.payload.latencyMode ?? null,
|
|
3208
3411
|
mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
|
|
3209
|
-
|
|
3210
|
-
...(event.payload.selectedHostMcpDelegations
|
|
3211
|
-
? { selectedHostMcpDelegations: event.payload.selectedHostMcpDelegations }
|
|
3212
|
-
: {}),
|
|
3412
|
+
connectionAccounts: event.payload.connectionAccounts,
|
|
3213
3413
|
...(event.payload.personalResourceAttachment
|
|
3214
3414
|
? { personalResourceAttachment: event.payload.personalResourceAttachment }
|
|
3215
3415
|
: {}),
|
|
@@ -3225,6 +3425,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3225
3425
|
} catch (error) {
|
|
3226
3426
|
return commandConflictResponse(c, error);
|
|
3227
3427
|
}
|
|
3428
|
+
recordAcceptedApiAdmission(deps.observability, result);
|
|
3228
3429
|
return c.json(result.accepted, 202);
|
|
3229
3430
|
}
|
|
3230
3431
|
|
|
@@ -4566,6 +4767,7 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4566
4767
|
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
4567
4768
|
if (suffix === "/background-commands" && verb === "GET") return "session.read";
|
|
4568
4769
|
if (suffix === "/model-context" && verb === "GET") return "session.read";
|
|
4770
|
+
if (suffix === "/codex-accounts" && verb === "GET") return "session.read";
|
|
4569
4771
|
if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
|
|
4570
4772
|
return "session.control";
|
|
4571
4773
|
}
|
|
@@ -4632,6 +4834,8 @@ export function sessionAuthorizationOperationForHttp(
|
|
|
4632
4834
|
// acceptSessionUserMessageWithOutcome after the body is parsed.
|
|
4633
4835
|
if (suffix === "/composer-draft/submit" && verb === "POST") return "session.append";
|
|
4634
4836
|
if (suffix === "/control" && verb === "POST") return "session.control";
|
|
4837
|
+
if (suffix === "/retry" && verb === "POST") return "session.control";
|
|
4838
|
+
if (suffix === "/sandbox-recovery" && ["GET", "POST"].includes(verb)) return "session.control";
|
|
4635
4839
|
if (suffix === "/steer" && verb === "POST") return "session.steer";
|
|
4636
4840
|
if (suffix === "/human-input-requests" && verb === "GET") {
|
|
4637
4841
|
return "session.human_input.read";
|
|
@@ -4795,7 +4999,7 @@ function eventEnumList<T extends string>(
|
|
|
4795
4999
|
});
|
|
4796
5000
|
}
|
|
4797
5001
|
|
|
4798
|
-
function sessionListQuery(
|
|
5002
|
+
export function sessionListQuery(
|
|
4799
5003
|
query: Record<string, string>,
|
|
4800
5004
|
allowCursor = true,
|
|
4801
5005
|
): {
|
|
@@ -4806,6 +5010,8 @@ function sessionListQuery(
|
|
|
4806
5010
|
search: string | undefined;
|
|
4807
5011
|
pinsOnly: boolean;
|
|
4808
5012
|
archivedOnly: boolean;
|
|
5013
|
+
sortBy: "updatedAt" | "createdAt" | "name" | undefined;
|
|
5014
|
+
archiveStatus: "active" | "archived" | "all" | undefined;
|
|
4809
5015
|
channelId: string | null | undefined;
|
|
4810
5016
|
createdBy: { kind: "subject" | "service"; subjectId: string } | undefined;
|
|
4811
5017
|
updatedFrom: Date | undefined;
|
|
@@ -4852,6 +5058,20 @@ function sessionListQuery(
|
|
|
4852
5058
|
throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
|
|
4853
5059
|
}
|
|
4854
5060
|
const archivedOnly = query.archivedOnly === "true";
|
|
5061
|
+
const sortBy = z.enum(["updatedAt", "createdAt", "name"]).optional().safeParse(query.sortBy);
|
|
5062
|
+
const archiveStatus = z
|
|
5063
|
+
.enum(["active", "archived", "all"])
|
|
5064
|
+
.optional()
|
|
5065
|
+
.safeParse(query.archiveStatus);
|
|
5066
|
+
if (!sortBy.success || !archiveStatus.success) {
|
|
5067
|
+
throw new HTTPException(400, { message: "Invalid session sortBy or archiveStatus" });
|
|
5068
|
+
}
|
|
5069
|
+
if (archivedOnly && archiveStatus.data !== undefined && archiveStatus.data !== "archived") {
|
|
5070
|
+
throw new HTTPException(400, { message: "archivedOnly conflicts with archiveStatus" });
|
|
5071
|
+
}
|
|
5072
|
+
if (pinsOnly && archiveStatus.data === "archived") {
|
|
5073
|
+
throw new HTTPException(400, { message: "pinsOnly cannot be combined with archived status" });
|
|
5074
|
+
}
|
|
4855
5075
|
const channelId = query.channelId;
|
|
4856
5076
|
if (
|
|
4857
5077
|
channelId !== undefined &&
|
|
@@ -4956,6 +5176,8 @@ function sessionListQuery(
|
|
|
4956
5176
|
search: search || undefined,
|
|
4957
5177
|
pinsOnly,
|
|
4958
5178
|
archivedOnly,
|
|
5179
|
+
sortBy: sortBy.data,
|
|
5180
|
+
archiveStatus: archiveStatus.data,
|
|
4959
5181
|
channelId: channelId === undefined ? undefined : channelId === "null" ? null : channelId,
|
|
4960
5182
|
createdBy:
|
|
4961
5183
|
createdByKind && createdBySubjectId
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { Context, Hono } from "hono";
|
|
3
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
4
|
import { SkillFile, type SkillWriteReceipt } from "@opengeni/contracts";
|
|
5
|
+
import { readSkillRemovalScope } from "@opengeni/db";
|
|
5
6
|
import {
|
|
6
7
|
approveSkill,
|
|
7
8
|
rejectSkill,
|
|
@@ -33,6 +34,7 @@ const saveRequest = z
|
|
|
33
34
|
.strict();
|
|
34
35
|
const revisionRequest = z
|
|
35
36
|
.object({
|
|
37
|
+
removalOperationId: z.uuid().optional(),
|
|
36
38
|
operationId: z.uuid(),
|
|
37
39
|
revisionId: z.uuid(),
|
|
38
40
|
expectedRevisionId: z.uuid().nullable(),
|
|
@@ -180,8 +182,22 @@ export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
180
182
|
{ accountId: access.grant.accountId, workspaceId, subjectId: access.grant.subjectId },
|
|
181
183
|
skillId,
|
|
182
184
|
);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
const scope =
|
|
186
|
+
current?.scope ??
|
|
187
|
+
(operation === "approve" && request.removalOperationId
|
|
188
|
+
? await readSkillRemovalScope(
|
|
189
|
+
deps.db,
|
|
190
|
+
{
|
|
191
|
+
accountId: access.grant.accountId,
|
|
192
|
+
workspaceId,
|
|
193
|
+
subjectId: access.grant.subjectId,
|
|
194
|
+
},
|
|
195
|
+
skillId,
|
|
196
|
+
request.operationId,
|
|
197
|
+
)
|
|
198
|
+
: null);
|
|
199
|
+
if (!scope) throw new HTTPException(404, { message: "Skill not found" });
|
|
200
|
+
authorizePreferenceRegistryScopeMutation(access, scope);
|
|
185
201
|
const apply =
|
|
186
202
|
operation === "approve"
|
|
187
203
|
? approveSkill
|
|
@@ -214,7 +230,7 @@ async function skillMutation(run: () => Promise<SkillWriteReceipt>): Promise<Ski
|
|
|
214
230
|
let current: unknown = error;
|
|
215
231
|
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
|
|
216
232
|
const candidate = current as { code?: unknown; cause?: unknown };
|
|
217
|
-
if (candidate.code === "23505" || candidate.code === "40001")
|
|
233
|
+
if (candidate.code === "23505" || candidate.code === "40001" || candidate.code === "55P03")
|
|
218
234
|
throw new HTTPException(409, {
|
|
219
235
|
message:
|
|
220
236
|
"Skill changed or this operation key was reused. Reload and reconcile the change.",
|
package/src/routes/social.ts
CHANGED
|
@@ -2,8 +2,10 @@ import {
|
|
|
2
2
|
CreateSocialConnectionRequest,
|
|
3
3
|
CreateSocialPostRequest,
|
|
4
4
|
OAuthStartResponse,
|
|
5
|
+
OrganizationIntegrationDeniedError,
|
|
5
6
|
SocialOAuthStartRequest,
|
|
6
7
|
} from "@opengeni/contracts";
|
|
8
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
7
9
|
import {
|
|
8
10
|
createSocialConnection,
|
|
9
11
|
createSocialPost,
|
|
@@ -47,19 +49,25 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
47
49
|
const payload = CreateSocialConnectionRequest.parse(await c.req.json());
|
|
48
50
|
try {
|
|
49
51
|
return c.json(
|
|
50
|
-
await
|
|
51
|
-
|
|
52
|
-
workspaceId,
|
|
53
|
-
provider
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
await withOrganizationIntegrationAcquisition(
|
|
53
|
+
db,
|
|
54
|
+
{ accountId: grant.accountId, workspaceId },
|
|
55
|
+
[payload.provider === "x" || payload.provider === "reddit" ? payload.provider : null],
|
|
56
|
+
(tx) =>
|
|
57
|
+
createSocialConnection(tx, {
|
|
58
|
+
accountId: grant.accountId,
|
|
59
|
+
workspaceId,
|
|
60
|
+
provider: payload.provider,
|
|
61
|
+
accountHandle: payload.accountHandle,
|
|
62
|
+
accountName: payload.accountName ?? null,
|
|
63
|
+
externalAccountId: payload.externalAccountId ?? null,
|
|
64
|
+
status: payload.status,
|
|
65
|
+
scopes: payload.scopes,
|
|
66
|
+
credentialRef: payload.credentialRef ?? null,
|
|
67
|
+
tokenMetadata: payload.tokenMetadata,
|
|
68
|
+
metadata: payload.metadata,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
63
71
|
201,
|
|
64
72
|
);
|
|
65
73
|
} catch (error) {
|
|
@@ -222,6 +230,9 @@ function parseConnectionIds(raw: string | undefined): string[] | undefined {
|
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
function socialHttpException(error: unknown): HTTPException {
|
|
233
|
+
if (error instanceof OrganizationIntegrationDeniedError) {
|
|
234
|
+
return new HTTPException(403, { message: error.message });
|
|
235
|
+
}
|
|
225
236
|
const message = error instanceof Error ? error.message : String(error);
|
|
226
237
|
if (message.includes("not found")) {
|
|
227
238
|
return new HTTPException(404, { message });
|