@opengeni/api-router 3.2.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +15 -0
- package/dist/app.js +1 -1
- package/dist/billing-stripe-customer.d.ts +18 -0
- package/dist/billing-subscription-store.d.ts +34 -0
- package/dist/billing-subscriptions.d.ts +64 -0
- package/dist/{chunk-PQBHZ2YH.js → chunk-6DZ4H5JQ.js} +3133 -1711
- package/dist/{chunk-PQBHZ2YH.js.map → chunk-6DZ4H5JQ.js.map} +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/mcp/server.d.ts +11 -1
- package/dist/routes/browser-sessions.d.ts +23 -2
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/sandbox/channel-a.d.ts +10 -1
- package/dist/site-publishing/agent-data-tools.d.ts +5 -5
- package/dist/site-publishing/agent-session.d.ts +3 -2
- package/dist/site-publishing/runtime.d.ts +7 -6
- package/dist/site-publishing/service.d.ts +8 -11
- package/dist/site-publishing/session-request-authority.d.ts +3 -0
- package/package.json +24 -24
- package/src/access-grant-rls.ts +99 -10
- package/src/app.ts +105 -43
- package/src/billing-stripe-customer.ts +40 -0
- package/src/billing-subscription-store.ts +121 -0
- package/src/billing-subscriptions.ts +759 -0
- package/src/http/sse.ts +5 -1
- package/src/index.ts +3 -0
- package/src/mcp/action-catalog.gen.ts +113 -0
- package/src/mcp/action-schema-imports.gen.ts +5 -1
- package/src/mcp/server.ts +28 -0
- package/src/routes/billing.ts +161 -21
- package/src/routes/browser-sessions.ts +420 -26
- package/src/routes/computer-sessions.ts +102 -39
- package/src/routes/insights-usage.ts +22 -0
- package/src/routes/organization-memberships.ts +12 -0
- package/src/routes/sessions.ts +30 -5
- package/src/routes/site-runtime.ts +62 -20
- package/src/sandbox/channel-a.ts +18 -1
- package/src/sandbox/metrics-ingestion.ts +1 -0
- package/src/site-frame-guard.ts +11 -2
- package/src/site-publishing/agent-data-tools.ts +16 -8
- package/src/site-publishing/agent-session.ts +24 -6
- package/src/site-publishing/runtime.ts +29 -7
- package/src/site-publishing/service.ts +65 -77
- package/src/site-publishing/session-request-authority.ts +14 -0
|
@@ -58,12 +58,14 @@ import {
|
|
|
58
58
|
terminalizeStaleConnectedInteractionPlacement,
|
|
59
59
|
touchComputerSessionController,
|
|
60
60
|
type ComputerSessionControlRecord,
|
|
61
|
+
type EnrollmentRecord,
|
|
61
62
|
type LeaseSnapshot,
|
|
62
63
|
} from "@opengeni/db";
|
|
63
64
|
import {
|
|
64
65
|
hasPermission,
|
|
65
66
|
requireAccessGrant,
|
|
66
67
|
requireSessionAuthorization,
|
|
68
|
+
withSessionAuthorizationReadReuse,
|
|
67
69
|
relayConfigFromSettings,
|
|
68
70
|
resolveChannelAMachineConnection,
|
|
69
71
|
resolveSessionSandboxRuntime,
|
|
@@ -125,7 +127,12 @@ import {
|
|
|
125
127
|
observeComputerFrameEvidenceMismatch,
|
|
126
128
|
observeLifecycleResult,
|
|
127
129
|
} from "../interaction-metrics";
|
|
128
|
-
import {
|
|
130
|
+
import {
|
|
131
|
+
withChannelA,
|
|
132
|
+
withChannelARead,
|
|
133
|
+
type ChannelAHandle,
|
|
134
|
+
type ChannelAOperation,
|
|
135
|
+
} from "../sandbox/channel-a";
|
|
129
136
|
import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
|
|
130
137
|
import { wrongInteractionPlacementMessage } from "./interaction-placement-message";
|
|
131
138
|
import {
|
|
@@ -144,6 +151,14 @@ type ComputerPlacement = {
|
|
|
144
151
|
screenControlAllowed?: boolean;
|
|
145
152
|
};
|
|
146
153
|
|
|
154
|
+
type ActiveComputerControllerCallback<T> = (input: {
|
|
155
|
+
client: BrowserControlClient;
|
|
156
|
+
sessionClient: ReturnType<BrowserControlClient["computerSessionClient"]>;
|
|
157
|
+
record: ComputerSessionControlRecord;
|
|
158
|
+
binding: NonNullable<ComputerSessionControlRecord["session"]["controller"]>;
|
|
159
|
+
placement: ComputerPlacement;
|
|
160
|
+
}) => Promise<T>;
|
|
161
|
+
|
|
147
162
|
const MODEL_COMPUTER_FRAME_MAX_BYTES = 256 * 1024;
|
|
148
163
|
|
|
149
164
|
type ComputerFrameCaptureClient = {
|
|
@@ -1117,6 +1132,38 @@ export function registerComputerSessionRoutes(
|
|
|
1117
1132
|
},
|
|
1118
1133
|
);
|
|
1119
1134
|
|
|
1135
|
+
async function routedMachineConnection(
|
|
1136
|
+
handle: ChannelAHandle,
|
|
1137
|
+
grant: AccessGrant,
|
|
1138
|
+
sourceSession: Session,
|
|
1139
|
+
sandboxId: string,
|
|
1140
|
+
): Promise<EnrollmentRecord | null> {
|
|
1141
|
+
const machineAuthority = await handle.machineAuthority();
|
|
1142
|
+
const sandbox = await getSandbox(
|
|
1143
|
+
deps.db,
|
|
1144
|
+
{
|
|
1145
|
+
accountId: grant.accountId,
|
|
1146
|
+
workspaceId: sourceSession.workspaceId,
|
|
1147
|
+
subjectId: machineAuthority.subjectId,
|
|
1148
|
+
},
|
|
1149
|
+
sandboxId,
|
|
1150
|
+
);
|
|
1151
|
+
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
1152
|
+
throw new ComputerSessionStateError("Connected Machine placement is unavailable");
|
|
1153
|
+
}
|
|
1154
|
+
return await resolveChannelAMachineConnection(
|
|
1155
|
+
deps.db,
|
|
1156
|
+
{
|
|
1157
|
+
accountId: grant.accountId,
|
|
1158
|
+
workspaceId: sourceSession.workspaceId,
|
|
1159
|
+
sessionId: sourceSession.id,
|
|
1160
|
+
machineWorkspaceId: sandbox.workspaceId,
|
|
1161
|
+
enrollmentId: sandbox.enrollmentId,
|
|
1162
|
+
},
|
|
1163
|
+
machineAuthority,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1120
1167
|
async function withComputerPlacement<T>(
|
|
1121
1168
|
sourceSession: Session,
|
|
1122
1169
|
grant: AccessGrant,
|
|
@@ -1174,6 +1221,7 @@ export function registerComputerSessionRoutes(
|
|
|
1174
1221
|
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
1175
1222
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
1176
1223
|
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
1224
|
+
browserControlHttpSupported: enrollment.agentCapabilities.browserControlHttp === true,
|
|
1177
1225
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
1178
1226
|
enrollment.opStream === true &&
|
|
1179
1227
|
deps.bus.getOpStreamConnection
|
|
@@ -1237,33 +1285,15 @@ export function registerComputerSessionRoutes(
|
|
|
1237
1285
|
const resolved = await handle.routingSession.prime();
|
|
1238
1286
|
let screenControlAllowed = false;
|
|
1239
1287
|
if (operation !== "computer.end" && resolved.kind === "selfhosted" && resolved.sandboxId) {
|
|
1240
|
-
//
|
|
1241
|
-
// the
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
subjectId: machineAuthority.subjectId,
|
|
1250
|
-
},
|
|
1251
|
-
resolved.sandboxId,
|
|
1252
|
-
);
|
|
1253
|
-
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
1254
|
-
throw new ComputerSessionStateError("Connected Machine placement is unavailable");
|
|
1255
|
-
}
|
|
1256
|
-
const enrollment = await resolveChannelAMachineConnection(
|
|
1257
|
-
deps.db,
|
|
1258
|
-
{
|
|
1259
|
-
accountId: grant.accountId,
|
|
1260
|
-
workspaceId: sourceSession.workspaceId,
|
|
1261
|
-
sessionId: sourceSession.id,
|
|
1262
|
-
machineWorkspaceId: sandbox.workspaceId,
|
|
1263
|
-
enrollmentId: sandbox.enrollmentId,
|
|
1264
|
-
},
|
|
1265
|
-
machineAuthority,
|
|
1266
|
-
);
|
|
1288
|
+
// The request already resolved this machine's live runner when it
|
|
1289
|
+
// pinned the route; otherwise look the routed machine up as the
|
|
1290
|
+
// routing did: an agent acts for the person who started its turn, so
|
|
1291
|
+
// a personal (Only me) machine stays reachable, subject to that
|
|
1292
|
+
// attempt's per-chat permission.
|
|
1293
|
+
const enrollment =
|
|
1294
|
+
handle.pinnedMachine?.sandboxId === resolved.sandboxId
|
|
1295
|
+
? handle.pinnedMachine.enrollment
|
|
1296
|
+
: await routedMachineConnection(handle, grant, sourceSession, resolved.sandboxId);
|
|
1267
1297
|
if (!enrollment?.connectionInstanceId) {
|
|
1268
1298
|
throw new ComputerSessionStateError("Connected Machine is unavailable");
|
|
1269
1299
|
}
|
|
@@ -1476,14 +1506,37 @@ export function registerComputerSessionRoutes(
|
|
|
1476
1506
|
computerSessionId: string,
|
|
1477
1507
|
authorizationOperation: SessionAuthorizationOperation,
|
|
1478
1508
|
channelOperation: ChannelAOperation,
|
|
1479
|
-
callback:
|
|
1480
|
-
client: BrowserControlClient;
|
|
1481
|
-
sessionClient: ReturnType<BrowserControlClient["computerSessionClient"]>;
|
|
1482
|
-
record: ComputerSessionControlRecord;
|
|
1483
|
-
binding: NonNullable<ComputerSessionControlRecord["session"]["controller"]>;
|
|
1484
|
-
placement: ComputerPlacement;
|
|
1485
|
-
}) => Promise<T>,
|
|
1509
|
+
callback: ActiveComputerControllerCallback<T>,
|
|
1486
1510
|
recoverMissing = true,
|
|
1511
|
+
): Promise<T> {
|
|
1512
|
+
// The source-session check and the machine-route check that follows it
|
|
1513
|
+
// prove the same live agent attempt; they share those reads. Everything
|
|
1514
|
+
// from the controller call onward reads fresh.
|
|
1515
|
+
return await withSessionAuthorizationReadReuse((authorizationReads) =>
|
|
1516
|
+
withActiveComputerControllerReads(
|
|
1517
|
+
context,
|
|
1518
|
+
grant,
|
|
1519
|
+
workspaceId,
|
|
1520
|
+
computerSessionId,
|
|
1521
|
+
authorizationOperation,
|
|
1522
|
+
channelOperation,
|
|
1523
|
+
callback,
|
|
1524
|
+
recoverMissing,
|
|
1525
|
+
authorizationReads,
|
|
1526
|
+
),
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
async function withActiveComputerControllerReads<T>(
|
|
1531
|
+
context: Context,
|
|
1532
|
+
grant: AccessGrant,
|
|
1533
|
+
workspaceId: string,
|
|
1534
|
+
computerSessionId: string,
|
|
1535
|
+
authorizationOperation: SessionAuthorizationOperation,
|
|
1536
|
+
channelOperation: ChannelAOperation,
|
|
1537
|
+
callback: ActiveComputerControllerCallback<T>,
|
|
1538
|
+
recoverMissing: boolean,
|
|
1539
|
+
authorizationReads: { handOffToToolDispatch(): void },
|
|
1487
1540
|
): Promise<T> {
|
|
1488
1541
|
try {
|
|
1489
1542
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -1491,11 +1544,20 @@ export function registerComputerSessionRoutes(
|
|
|
1491
1544
|
workspaceId,
|
|
1492
1545
|
computerSessionId,
|
|
1493
1546
|
});
|
|
1494
|
-
|
|
1495
|
-
|
|
1547
|
+
// Only a cached managed controller can skip the source session; read it
|
|
1548
|
+
// alongside authorization otherwise. A denial still wins.
|
|
1549
|
+
const sourceSessionNeeded = record.session.placement?.kind !== "sandbox_group";
|
|
1550
|
+
const [authorized, sourceRead] = await Promise.allSettled([
|
|
1551
|
+
authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation),
|
|
1552
|
+
sourceSessionNeeded
|
|
1553
|
+
? requireSourceSession(deps, workspaceId, record.sourceSessionId)
|
|
1554
|
+
: Promise.resolve(null),
|
|
1555
|
+
]);
|
|
1556
|
+
if (authorized.status === "rejected") throw authorized.reason;
|
|
1557
|
+
if (sourceRead.status === "rejected") throw sourceRead.reason;
|
|
1558
|
+
let sourceSession: Session | null = sourceRead.value;
|
|
1496
1559
|
if (record.session.placement?.kind === "connected_machine") {
|
|
1497
|
-
|
|
1498
|
-
if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
|
|
1560
|
+
if (sourceSession?.activeSandboxId !== record.session.placement.sandboxId) {
|
|
1499
1561
|
return await throwComputerSourcePlacementChanged(
|
|
1500
1562
|
grant,
|
|
1501
1563
|
record.sourceSessionId,
|
|
@@ -1517,6 +1579,7 @@ export function registerComputerSessionRoutes(
|
|
|
1517
1579
|
throw new ComputerSessionStateError("ComputerSession controller authority changed");
|
|
1518
1580
|
}
|
|
1519
1581
|
const run = async (placement: ComputerPlacement): Promise<T> => {
|
|
1582
|
+
authorizationReads.handOffToToolDispatch();
|
|
1520
1583
|
// The active controller binding is the provisioning fence. Live
|
|
1521
1584
|
// input connects to it directly instead of re-ensuring the sidecar.
|
|
1522
1585
|
const client = connectController(grant, record, placement);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InsightsCallsResponse,
|
|
3
3
|
InsightsUsageResponse,
|
|
4
|
+
InsightsSiteUsageQuery,
|
|
5
|
+
InsightsSiteUsageResponse,
|
|
4
6
|
OrganizationInsightsCallsQuery,
|
|
5
7
|
OrganizationInsightsUsageQuery,
|
|
6
8
|
WorkspaceInsightsCallsQuery,
|
|
@@ -10,10 +12,12 @@ import type { AccessContext } from "@opengeni/contracts";
|
|
|
10
12
|
import {
|
|
11
13
|
accountScopedApiKeyWorkspaceAuthority,
|
|
12
14
|
getInsightsUsage,
|
|
15
|
+
getInsightsSiteUsage,
|
|
13
16
|
hasPermission,
|
|
14
17
|
listInsightsCalls,
|
|
15
18
|
requireAccessContext,
|
|
16
19
|
requireAccessGrant,
|
|
20
|
+
siteViewerForGrant,
|
|
17
21
|
type ApiRouteDeps,
|
|
18
22
|
type InsightsQueryScope,
|
|
19
23
|
} from "@opengeni/core";
|
|
@@ -221,6 +225,24 @@ export function registerInsightsUsageRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
221
225
|
});
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
app.get("/v1/workspaces/:workspaceId/insights/sites", async (c) => {
|
|
229
|
+
const workspaceId = c.req.param("workspaceId");
|
|
230
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
231
|
+
const query = InsightsSiteUsageQuery.safeParse(insightsQueryParameters(c.req.queries()));
|
|
232
|
+
if (!query.success) throw new HTTPException(400, { message: "invalid Site Insights query" });
|
|
233
|
+
const viewer = await siteViewerForGrant(deps.db, grant);
|
|
234
|
+
const response = await insightsWithFriendlyTimeout(() =>
|
|
235
|
+
getInsightsSiteUsage(deps.db, {
|
|
236
|
+
accountId: grant.accountId,
|
|
237
|
+
workspaceId,
|
|
238
|
+
query: query.data,
|
|
239
|
+
viewerSubjectId: viewer.subjectId,
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
c.header("cache-control", "private, no-store");
|
|
243
|
+
return c.json(InsightsSiteUsageResponse.parse(response));
|
|
244
|
+
});
|
|
245
|
+
|
|
224
246
|
app.get("/v1/workspaces/:workspaceId/insights/usage", async (c) => {
|
|
225
247
|
const workspaceId = c.req.param("workspaceId");
|
|
226
248
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
@@ -247,6 +247,18 @@ function parseId(schema: z.ZodString, value: string, label: string): string {
|
|
|
247
247
|
|
|
248
248
|
function rethrowMembershipError(error: unknown, resourceLimitMessage?: string): never {
|
|
249
249
|
const sqlState = nestedPostgresSqlState(error);
|
|
250
|
+
if (sqlState === "P0S01") {
|
|
251
|
+
throw new HTTPException(402, {
|
|
252
|
+
res: Response.json(
|
|
253
|
+
{
|
|
254
|
+
error: "seat_limit_reached",
|
|
255
|
+
message: "Organization seat limit reached. Upgrade your plan to add people.",
|
|
256
|
+
action: { type: "upgrade", href: "/billing" },
|
|
257
|
+
},
|
|
258
|
+
{ status: 402 },
|
|
259
|
+
),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
250
262
|
if (sqlState === "54000" && resourceLimitMessage) {
|
|
251
263
|
throw new HTTPException(409, {
|
|
252
264
|
message: resourceLimitMessage,
|
package/src/routes/sessions.ts
CHANGED
|
@@ -30,7 +30,12 @@ import {
|
|
|
30
30
|
SITE_FRAME_SESSION_CREATE_FIELDS,
|
|
31
31
|
siteFrameBodyRefusal,
|
|
32
32
|
} from "@opengeni/contracts/site-session-http";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
requireWorkspace,
|
|
35
|
+
scheduledSessionIds,
|
|
36
|
+
currentSiteViewerRlsContext,
|
|
37
|
+
nestedPostgresSqlState,
|
|
38
|
+
} from "@opengeni/db";
|
|
34
39
|
import {
|
|
35
40
|
currentSiteSessionOrigin,
|
|
36
41
|
isVerifiedDelegatedHumanAuthorization,
|
|
@@ -329,6 +334,7 @@ import {
|
|
|
329
334
|
} from "@opengeni/core";
|
|
330
335
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
331
336
|
import { SessionRetryConflictError } from "@opengeni/db";
|
|
337
|
+
import { reauthorizeSiteSessionRequest } from "../site-publishing/session-request-authority";
|
|
332
338
|
import {
|
|
333
339
|
attachViewer,
|
|
334
340
|
detachViewer,
|
|
@@ -761,7 +767,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
761
767
|
// nothing else: the session gets the minimal Site profile. Only the Site
|
|
762
768
|
// itself, running its published agent as `site:<siteId>`, chooses tools.
|
|
763
769
|
const siteOrigin = origin ?? currentSiteSessionOrigin();
|
|
764
|
-
if (siteOrigin &&
|
|
770
|
+
if (siteOrigin && currentSiteViewerRlsContext()?.siteId !== siteOrigin.siteId) {
|
|
765
771
|
const refused = siteFrameBodyRefusal("sessions.create", "/sessions", payload);
|
|
766
772
|
if (refused)
|
|
767
773
|
return c.json(
|
|
@@ -778,6 +784,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
778
784
|
siteId: siteOrigin.siteId,
|
|
779
785
|
versionId: siteOrigin.versionId ?? null,
|
|
780
786
|
});
|
|
787
|
+
// Ordinary Site viewers use native Only-me ownership. An agent preview
|
|
788
|
+
// inherits its parent's native visibility; the Site audience binding
|
|
789
|
+
// additionally restricts that execution to the parent's real owner.
|
|
790
|
+
if (grant.principalKind !== "agent_attempt")
|
|
791
|
+
(payload as Record<string, unknown>).visibility = "private";
|
|
781
792
|
}
|
|
782
793
|
const create = () =>
|
|
783
794
|
createSessionForRequest(deps, grant, workspaceId, payload, authorization);
|
|
@@ -993,6 +1004,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
993
1004
|
...(query.includeTotals ? { includeTotals: true } : {}),
|
|
994
1005
|
...(query.needsYouOnly ? { needsYouOnly: true } : {}),
|
|
995
1006
|
...(query.contentArchivedOnly ? { contentArchivedOnly: true } : {}),
|
|
1007
|
+
...(query.mineOnly ? { mineOnly: true } : {}),
|
|
996
1008
|
...(query.includePinned === false ? { includePinned: false } : {}),
|
|
997
1009
|
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
998
1010
|
...(query.sortBy ? { sortBy: query.sortBy } : {}),
|
|
@@ -3597,6 +3609,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
3597
3609
|
reauthorizeAfterMs:
|
|
3598
3610
|
authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
3599
3611
|
reauthorize: async () => {
|
|
3612
|
+
await reauthorizeSiteSessionRequest(c.req.raw);
|
|
3600
3613
|
const freshGrant = await requireFreshAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
3601
3614
|
await requireSessionAuthorization(deps, freshGrant, {
|
|
3602
3615
|
sessionId,
|
|
@@ -5671,7 +5684,10 @@ export function sessionTenancyHttpError(error: unknown): Error {
|
|
|
5671
5684
|
retryable: false,
|
|
5672
5685
|
});
|
|
5673
5686
|
}
|
|
5674
|
-
if (
|
|
5687
|
+
if (
|
|
5688
|
+
error instanceof SessionTenancyNotActivatedError ||
|
|
5689
|
+
nestedPostgresSqlState(error) === "P0793"
|
|
5690
|
+
) {
|
|
5675
5691
|
return new ApiHttpError(409, {
|
|
5676
5692
|
code: "conflict",
|
|
5677
5693
|
message: "Only-me chats are not enabled for this organization.",
|
|
@@ -5820,6 +5836,7 @@ export function sessionListQuery(
|
|
|
5820
5836
|
includeTotals: boolean;
|
|
5821
5837
|
needsYouOnly: boolean;
|
|
5822
5838
|
contentArchivedOnly: boolean;
|
|
5839
|
+
mineOnly: boolean;
|
|
5823
5840
|
archivedOnly: boolean;
|
|
5824
5841
|
sortBy: "updatedAt" | "createdAt" | "name" | undefined;
|
|
5825
5842
|
archiveStatus: "active" | "archived" | "all" | undefined;
|
|
@@ -5869,17 +5886,20 @@ export function sessionListQuery(
|
|
|
5869
5886
|
throw new HTTPException(400, { message: 'includePinned must be "true" or "false"' });
|
|
5870
5887
|
}
|
|
5871
5888
|
const includePinned = query.includePinned !== "false";
|
|
5872
|
-
for (const key of ["includeTotals", "needsYouOnly", "contentArchivedOnly"]) {
|
|
5889
|
+
for (const key of ["includeTotals", "needsYouOnly", "contentArchivedOnly", "mineOnly"]) {
|
|
5873
5890
|
if (query[key] !== undefined && !["true", "false"].includes(query[key]!))
|
|
5874
5891
|
throw new HTTPException(400, { message: `${key} must be "true" or "false"` });
|
|
5875
5892
|
}
|
|
5876
5893
|
const includeTotals = query.includeTotals === "true";
|
|
5877
5894
|
const needsYouOnly = query.needsYouOnly === "true";
|
|
5878
5895
|
const contentArchivedOnly = query.contentArchivedOnly === "true";
|
|
5896
|
+
const mineOnly = query.mineOnly === "true";
|
|
5879
5897
|
if (includeTotals && (!allowCursor || (parentSessionId !== "null" && !pinsOnly)))
|
|
5880
5898
|
throw new HTTPException(400, { message: "includeTotals requires a root page" });
|
|
5881
5899
|
if (needsYouOnly && !allowCursor)
|
|
5882
5900
|
throw new HTTPException(400, { message: 'needsYouOnly requires view="page"' });
|
|
5901
|
+
if (mineOnly && !allowCursor)
|
|
5902
|
+
throw new HTTPException(400, { message: 'mineOnly requires view="page"' });
|
|
5883
5903
|
if (pinsOnly && !includePinned) {
|
|
5884
5904
|
throw new HTTPException(400, { message: "pinsOnly requires includePinned" });
|
|
5885
5905
|
}
|
|
@@ -5975,6 +5995,7 @@ export function sessionListQuery(
|
|
|
5975
5995
|
const hasPageFilters =
|
|
5976
5996
|
needsYouOnly ||
|
|
5977
5997
|
contentArchivedOnly ||
|
|
5998
|
+
mineOnly ||
|
|
5978
5999
|
originSiteId !== undefined ||
|
|
5979
6000
|
channelId !== undefined ||
|
|
5980
6001
|
createdByKind !== undefined ||
|
|
@@ -6010,6 +6031,7 @@ export function sessionListQuery(
|
|
|
6010
6031
|
includeTotals,
|
|
6011
6032
|
needsYouOnly,
|
|
6012
6033
|
contentArchivedOnly,
|
|
6034
|
+
mineOnly,
|
|
6013
6035
|
archivedOnly,
|
|
6014
6036
|
sortBy: sortBy.data,
|
|
6015
6037
|
archiveStatus: archiveStatus.data,
|
|
@@ -6552,7 +6574,10 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
|
|
|
6552
6574
|
403,
|
|
6553
6575
|
);
|
|
6554
6576
|
}
|
|
6555
|
-
if (
|
|
6577
|
+
if (
|
|
6578
|
+
error instanceof SessionTenancyNotActivatedError ||
|
|
6579
|
+
nestedPostgresSqlState(error) === "P0793"
|
|
6580
|
+
) {
|
|
6556
6581
|
return c.json(
|
|
6557
6582
|
{
|
|
6558
6583
|
code: "SESSION_TENANCY_NOT_ACTIVATED",
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
ReasoningEffort,
|
|
4
|
+
LatencyMode,
|
|
5
|
+
permissionsRequiredByFirstPartyTools,
|
|
3
6
|
DeleteSessionQueueItemRequest,
|
|
4
7
|
EditSessionQueueItemRequest,
|
|
5
8
|
matchSiteOperation,
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
type ToolRef,
|
|
31
34
|
} from "@opengeni/contracts";
|
|
32
35
|
import {
|
|
36
|
+
cancelPublishedSiteSession,
|
|
33
37
|
withHostSessionUsageLimit,
|
|
34
38
|
withSiteSessionOrigin,
|
|
35
39
|
admissibleWorkspaceModel,
|
|
@@ -42,6 +46,7 @@ import {
|
|
|
42
46
|
consumeSiteAuthCode,
|
|
43
47
|
finishSiteRuntimeAdmission,
|
|
44
48
|
withSiteRuntimeAdmission,
|
|
49
|
+
withSiteViewerRlsContext,
|
|
45
50
|
createSiteAbuseReport,
|
|
46
51
|
getSiteWorkspaceMemberPermissions,
|
|
47
52
|
readSitePlatformUsage,
|
|
@@ -65,7 +70,6 @@ import {
|
|
|
65
70
|
type SiteRuntimeConversationRecord,
|
|
66
71
|
} from "@opengeni/db";
|
|
67
72
|
import {
|
|
68
|
-
SITE_AGENT_DATA_TOOL_PERMISSIONS,
|
|
69
73
|
siteAgentDataToolsForStart,
|
|
70
74
|
siteAgentInstructions,
|
|
71
75
|
sitePublishedDataViewer,
|
|
@@ -145,7 +149,7 @@ import {
|
|
|
145
149
|
configuredSitesDomain,
|
|
146
150
|
healSiteEdgeIfStale,
|
|
147
151
|
notifySiteAbuse,
|
|
148
|
-
|
|
152
|
+
setSitePublishingCancellation,
|
|
149
153
|
siteHostMapRecord,
|
|
150
154
|
sitePublishingError,
|
|
151
155
|
} from "../site-publishing/service";
|
|
@@ -212,6 +216,9 @@ const CreateConversationRequest = z
|
|
|
212
216
|
.object({
|
|
213
217
|
initialMessage: z.string().trim().min(1).max(MESSAGE_MAX_CHARS),
|
|
214
218
|
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
219
|
+
model: z.string().min(1).optional(),
|
|
220
|
+
reasoningEffort: ReasoningEffort.optional(),
|
|
221
|
+
latencyMode: LatencyMode.optional(),
|
|
215
222
|
})
|
|
216
223
|
.passthrough();
|
|
217
224
|
const SendMessageRequest = z
|
|
@@ -497,8 +504,8 @@ type RuntimeRequest = {
|
|
|
497
504
|
export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
498
505
|
const dispatch = async (request: Request): Promise<Response> =>
|
|
499
506
|
await app.fetch(markLocalInternalDispatch(request));
|
|
500
|
-
//
|
|
501
|
-
|
|
507
|
+
// Lifecycle cancellation is a native internal command, never viewer dispatch.
|
|
508
|
+
setSitePublishingCancellation(deps.settings, (input) => cancelPublishedSiteSession(deps, input));
|
|
502
509
|
|
|
503
510
|
const handler = async (context: Context): Promise<Response> => {
|
|
504
511
|
const domain = configuredSitesDomain(deps.settings);
|
|
@@ -1232,7 +1239,7 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1232
1239
|
throw unsupported();
|
|
1233
1240
|
};
|
|
1234
1241
|
|
|
1235
|
-
/** The viewer's own
|
|
1242
|
+
/** The viewer's own conversations, with native member or viewer-bound outsider authority. */
|
|
1236
1243
|
const sessions = async (
|
|
1237
1244
|
request: RuntimeRequest,
|
|
1238
1245
|
id: string,
|
|
@@ -1246,6 +1253,20 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1246
1253
|
// carries the Site and version headers the API's Site frame guard
|
|
1247
1254
|
// validates; sessions and turns it starts record this Site and version.
|
|
1248
1255
|
const siteOrigin = { siteId: host.site.id, title: host.site.title, versionId: host.versionId };
|
|
1256
|
+
const reauthorizeViewer = async () => {
|
|
1257
|
+
const liveHost = await requireServedSiteRuntimeHost(deps, host.domain, request.fields.host);
|
|
1258
|
+
const liveViewer = await resolveSiteRuntimeViewer(deps, liveHost, request.fields);
|
|
1259
|
+
if (
|
|
1260
|
+
liveHost.site.id !== host.site.id ||
|
|
1261
|
+
liveViewer.principal !== viewer.principal ||
|
|
1262
|
+
liveViewer.kind !== viewer.kind ||
|
|
1263
|
+
liveViewer.sessionId !== viewer.sessionId
|
|
1264
|
+
)
|
|
1265
|
+
throw siteNotAvailableForViewer(
|
|
1266
|
+
"agent",
|
|
1267
|
+
"This conversation is no longer available to this viewer",
|
|
1268
|
+
);
|
|
1269
|
+
};
|
|
1249
1270
|
let pendingAdmission: { sessionId: string; admissionId: string } | undefined;
|
|
1250
1271
|
const send = async (
|
|
1251
1272
|
method: string,
|
|
@@ -1253,16 +1274,20 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1253
1274
|
body?: unknown,
|
|
1254
1275
|
extraPermissions?: readonly Permission[],
|
|
1255
1276
|
) => {
|
|
1277
|
+
await reauthorizeViewer();
|
|
1256
1278
|
const execute = () =>
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1279
|
+
withSiteViewerRlsContext({ siteId: host.site.id, principal: viewer.principal }, () =>
|
|
1280
|
+
withSiteSessionOrigin(siteOrigin, async () =>
|
|
1281
|
+
dispatch(
|
|
1282
|
+
await siteRuntimeSessionRequest(deps, host, viewer, {
|
|
1283
|
+
origin: url.origin,
|
|
1284
|
+
method,
|
|
1285
|
+
path,
|
|
1286
|
+
reauthorize: reauthorizeViewer,
|
|
1287
|
+
...(body === undefined ? {} : { body }),
|
|
1288
|
+
...(extraPermissions ? { extraPermissions } : {}),
|
|
1289
|
+
}),
|
|
1290
|
+
),
|
|
1266
1291
|
),
|
|
1267
1292
|
);
|
|
1268
1293
|
const pending = pendingAdmission;
|
|
@@ -1344,6 +1369,11 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1344
1369
|
if (viewer.kind === "guest" || viewer.kind === "user")
|
|
1345
1370
|
await requireSitePlatformOpen(deps, "guest_agent");
|
|
1346
1371
|
const models = await siteViewerModels(host, viewer, limits);
|
|
1372
|
+
if (input.model && !models.allowed.includes(input.model))
|
|
1373
|
+
throw siteNotAvailableForViewer(
|
|
1374
|
+
"agent_model",
|
|
1375
|
+
"This model is not available to you on this Site",
|
|
1376
|
+
);
|
|
1347
1377
|
if (models.unavailable)
|
|
1348
1378
|
throw siteNotAvailableForViewer(
|
|
1349
1379
|
"agent_model",
|
|
@@ -1402,6 +1432,11 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1402
1432
|
}
|
|
1403
1433
|
}
|
|
1404
1434
|
// Settle the viewer's recent spend so the day budget sees it.
|
|
1435
|
+
if (!repair && viewer.kind !== "member" && deps.settings.privacyOnlyMeAllowed === false)
|
|
1436
|
+
throw siteNotAvailableForViewer(
|
|
1437
|
+
"private_conversations",
|
|
1438
|
+
"Private Site conversations are disabled by this deployment",
|
|
1439
|
+
);
|
|
1405
1440
|
for (const conversation of await listOwnedSiteRuntimeConversations(deps.db, host.scope, {
|
|
1406
1441
|
siteId: host.site.id,
|
|
1407
1442
|
viewerPrincipal: viewer.principal,
|
|
@@ -1455,7 +1490,9 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1455
1490
|
host.scope,
|
|
1456
1491
|
host.site.id,
|
|
1457
1492
|
viewer.kind,
|
|
1493
|
+
viewer.kind === "member" ? (memberPermissions ?? []) : undefined,
|
|
1458
1494
|
);
|
|
1495
|
+
const dataToolPermissions = permissionsRequiredByFirstPartyTools(dataTools);
|
|
1459
1496
|
// One transaction counts the start and reserves a per-turn estimate
|
|
1460
1497
|
// against the visitor's and the Site's windows; only the funded allowance
|
|
1461
1498
|
// becomes the session's usage limit, set in the create transaction. The
|
|
@@ -1491,15 +1528,14 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1491
1528
|
const body = {
|
|
1492
1529
|
initialMessage: input.initialMessage,
|
|
1493
1530
|
requestedSessionId,
|
|
1531
|
+
...(viewer.kind === "member" ? { visibility: "private" } : {}),
|
|
1494
1532
|
startMode: "standard",
|
|
1495
1533
|
agentAccess: "session",
|
|
1496
1534
|
memoryScope: "off",
|
|
1497
1535
|
rigId: null,
|
|
1498
1536
|
// Only this Site's data tools; never management or workspace tools.
|
|
1499
1537
|
firstPartyMcpTools: dataTools,
|
|
1500
|
-
...(dataTools.length > 0
|
|
1501
|
-
? { firstPartyMcpPermissions: [...SITE_AGENT_DATA_TOOL_PERMISSIONS] }
|
|
1502
|
-
: {}),
|
|
1538
|
+
...(dataTools.length > 0 ? { firstPartyMcpPermissions: dataToolPermissions } : {}),
|
|
1503
1539
|
agent: {
|
|
1504
1540
|
capabilities: {
|
|
1505
1541
|
from: "none",
|
|
@@ -1511,7 +1547,13 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1511
1547
|
// No sandbox: a Site's agent answers from the page and the Site's
|
|
1512
1548
|
// data, with no shell or workspace files.
|
|
1513
1549
|
sandboxBackend: "none",
|
|
1514
|
-
...(repair
|
|
1550
|
+
...(repair
|
|
1551
|
+
? { model: repair.model }
|
|
1552
|
+
: input.model || models.default
|
|
1553
|
+
? { model: input.model ?? models.default }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
1556
|
+
...(input.latencyMode === undefined ? {} : { latencyMode: input.latencyMode }),
|
|
1515
1557
|
instructions: siteAgentInstructions(host, viewer, dataTools, agent.instructions),
|
|
1516
1558
|
// Always explicit: an empty list means no workspace tools at all.
|
|
1517
1559
|
tools: mcpTools,
|
|
@@ -1525,8 +1567,7 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1525
1567
|
},
|
|
1526
1568
|
...(input.idempotencyKey ? { idempotencyKey: `site-runtime:${requestedSessionId}` } : {}),
|
|
1527
1569
|
};
|
|
1528
|
-
const start = () =>
|
|
1529
|
-
send("POST", base, body, dataTools.length > 0 ? SITE_AGENT_DATA_TOOL_PERMISSIONS : []);
|
|
1570
|
+
const start = () => send("POST", base, body, dataToolPermissions);
|
|
1530
1571
|
// Workspace members get no Site budget at all: their conversation is an
|
|
1531
1572
|
// ordinary session under the workspace's normal limits, so no session
|
|
1532
1573
|
// usage limit is stamped on it, whatever the Site's limits say.
|
|
@@ -1694,6 +1735,7 @@ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1694
1735
|
viewer,
|
|
1695
1736
|
conversation.sessionId,
|
|
1696
1737
|
conversation.toolIds,
|
|
1738
|
+
viewer.kind === "member" ? (memberPermissions ?? []) : undefined,
|
|
1697
1739
|
);
|
|
1698
1740
|
const usage = await settleSiteRuntimeSpend(deps, host, conversation);
|
|
1699
1741
|
// Clear the stamped session cap before dropping the durable conversation
|
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
SandboxPaidComputeAdmissionError,
|
|
44
44
|
withSandboxProviderReadLock,
|
|
45
45
|
type Database,
|
|
46
|
+
type EnrollmentRecord,
|
|
46
47
|
type LeaseSnapshot,
|
|
47
48
|
} from "@opengeni/db";
|
|
48
49
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
@@ -207,6 +208,15 @@ export type ChannelAHandle = {
|
|
|
207
208
|
* who started an agent's turn, or the calling person. Routes that look up
|
|
208
209
|
* the routed machine again must use it rather than the raw caller grant. */
|
|
209
210
|
machineAuthority: () => Promise<ChannelAMachineAuthority>;
|
|
211
|
+
/** For a machine-home request, the Connected Machine and live runner this
|
|
212
|
+
* request was pinned to under `machineAuthority`. The routing session fails
|
|
213
|
+
* rather than follow a changed pointer, so a route that resolved this same
|
|
214
|
+
* sandbox can use it instead of looking the machine up again. */
|
|
215
|
+
pinnedMachine: {
|
|
216
|
+
sandboxId: string;
|
|
217
|
+
sandboxWorkspaceId: string;
|
|
218
|
+
enrollment: EnrollmentRecord;
|
|
219
|
+
} | null;
|
|
210
220
|
requestId: string;
|
|
211
221
|
};
|
|
212
222
|
|
|
@@ -629,6 +639,7 @@ async function withChannelAOperation<T>(
|
|
|
629
639
|
routed: EstablishedSandboxSession,
|
|
630
640
|
lease: LeaseSnapshot | null,
|
|
631
641
|
homeSession: ChannelASession,
|
|
642
|
+
pinnedMachine: ChannelAHandle["pinnedMachine"] = null,
|
|
632
643
|
): Promise<T> => {
|
|
633
644
|
const emit = async (events: { type: string; payload: unknown }[]): Promise<void> => {
|
|
634
645
|
await appendAndPublishEvents(
|
|
@@ -667,6 +678,7 @@ async function withChannelAOperation<T>(
|
|
|
667
678
|
homeSession,
|
|
668
679
|
routingSession,
|
|
669
680
|
machineAuthority,
|
|
681
|
+
pinnedMachine,
|
|
670
682
|
requestId,
|
|
671
683
|
});
|
|
672
684
|
// The direct request has accepted the result in memory. Finalize every
|
|
@@ -770,6 +782,7 @@ async function withChannelAOperation<T>(
|
|
|
770
782
|
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
771
783
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
772
784
|
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
785
|
+
browserControlHttpSupported: enrollment.agentCapabilities.browserControlHttp === true,
|
|
773
786
|
...(settings.agentOpStreamEnabled === true &&
|
|
774
787
|
enrollment?.opStream === true &&
|
|
775
788
|
bus.getOpStreamConnection
|
|
@@ -812,7 +825,11 @@ async function withChannelAOperation<T>(
|
|
|
812
825
|
},
|
|
813
826
|
established,
|
|
814
827
|
);
|
|
815
|
-
return await runEstablished(routed, null, established.session as ChannelASession
|
|
828
|
+
return await runEstablished(routed, null, established.session as ChannelASession, {
|
|
829
|
+
sandboxId: sandbox.id,
|
|
830
|
+
sandboxWorkspaceId: originWorkspaceId,
|
|
831
|
+
enrollment,
|
|
832
|
+
});
|
|
816
833
|
} catch (error) {
|
|
817
834
|
observeChannelAOperationFailure(services, {
|
|
818
835
|
workspaceId,
|
|
@@ -843,6 +843,7 @@ export function helloRuntimeCapabilities(hello: Hello): Record<string, boolean>
|
|
|
843
843
|
operationCpuQuota: caps.operationCpuQuota === true,
|
|
844
844
|
transactionalFsWrite: caps.transactionalFsWrite === true,
|
|
845
845
|
credentialRenew: caps.credentialRenew === true,
|
|
846
|
+
browserControlHttp: caps.browserControlHttp === true,
|
|
846
847
|
screenControl: caps.consentedScreenControl === true,
|
|
847
848
|
...(caps.macPermissions ? enrollmentMacPermissionCapabilities(caps.macPermissions) : {}),
|
|
848
849
|
};
|