@opengeni/api-router 3.2.2 → 3.3.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/app.js +1 -1
- package/dist/{chunk-PQBHZ2YH.js → chunk-6CECHDZP.js} +481 -333
- package/dist/{chunk-PQBHZ2YH.js.map → chunk-6CECHDZP.js.map} +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- 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 +1 -0
- package/src/http/sse.ts +5 -1
- package/src/routes/browser-sessions.ts +61 -17
- package/src/routes/computer-sessions.ts +102 -39
- package/src/routes/insights-usage.ts +22 -0
- package/src/routes/sessions.ts +22 -4
- 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
|
@@ -83,8 +83,8 @@ export declare function scheduledTaskMcpSummary(task: ScheduledTask): {
|
|
|
83
83
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
84
84
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
85
85
|
action: {
|
|
86
|
-
readonly sourceId?: never;
|
|
87
86
|
kind: "agent_turn";
|
|
87
|
+
sourceId?: never;
|
|
88
88
|
sourceGeneration?: never;
|
|
89
89
|
sourceLifecycleGeneration?: never;
|
|
90
90
|
destination?: never;
|
|
@@ -221,8 +221,8 @@ export declare function boundScheduledTaskDetailMcp(task: ScheduledTask): {
|
|
|
221
221
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
222
222
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
223
223
|
action: {
|
|
224
|
-
readonly sourceId?: never;
|
|
225
224
|
kind: "agent_turn";
|
|
225
|
+
sourceId?: never;
|
|
226
226
|
sourceGeneration?: never;
|
|
227
227
|
sourceLifecycleGeneration?: never;
|
|
228
228
|
destination?: never;
|
|
@@ -408,8 +408,8 @@ export declare function boundScheduledTaskDetailMcp(task: ScheduledTask): {
|
|
|
408
408
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
409
409
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
410
410
|
action: {
|
|
411
|
-
readonly sourceId?: never;
|
|
412
411
|
kind: "agent_turn";
|
|
412
|
+
sourceId?: never;
|
|
413
413
|
sourceGeneration?: never;
|
|
414
414
|
sourceLifecycleGeneration?: never;
|
|
415
415
|
destination?: never;
|
|
@@ -601,8 +601,8 @@ export declare function boundScheduledTaskMcpPage(input: {
|
|
|
601
601
|
runMode: "existing_session" | "new_session_per_run" | "reusable_session";
|
|
602
602
|
overlapPolicy: "allow_concurrent" | "buffer_one" | "skip";
|
|
603
603
|
action: {
|
|
604
|
-
readonly sourceId?: never;
|
|
605
604
|
kind: "agent_turn";
|
|
605
|
+
sourceId?: never;
|
|
606
606
|
sourceGeneration?: never;
|
|
607
607
|
sourceLifecycleGeneration?: never;
|
|
608
608
|
destination?: never;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Settings } from "@opengeni/config";
|
|
2
2
|
import type { AccessGrant, Session } from "@opengeni/contracts";
|
|
3
|
-
import { type Database, type LeaseSnapshot } from "@opengeni/db";
|
|
3
|
+
import { type Database, type EnrollmentRecord, type LeaseSnapshot } from "@opengeni/db";
|
|
4
4
|
import { type EventBus } from "@opengeni/events";
|
|
5
5
|
import { type Observability } from "@opengeni/observability";
|
|
6
6
|
import type { ObjectStorage } from "@opengeni/storage";
|
|
@@ -56,6 +56,15 @@ export type ChannelAHandle = {
|
|
|
56
56
|
* who started an agent's turn, or the calling person. Routes that look up
|
|
57
57
|
* the routed machine again must use it rather than the raw caller grant. */
|
|
58
58
|
machineAuthority: () => Promise<ChannelAMachineAuthority>;
|
|
59
|
+
/** For a machine-home request, the Connected Machine and live runner this
|
|
60
|
+
* request was pinned to under `machineAuthority`. The routing session fails
|
|
61
|
+
* rather than follow a changed pointer, so a route that resolved this same
|
|
62
|
+
* sandbox can use it instead of looking the machine up again. */
|
|
63
|
+
pinnedMachine: {
|
|
64
|
+
sandboxId: string;
|
|
65
|
+
sandboxWorkspaceId: string;
|
|
66
|
+
enrollment: EnrollmentRecord;
|
|
67
|
+
} | null;
|
|
59
68
|
requestId: string;
|
|
60
69
|
};
|
|
61
70
|
export type EstablishedHandleCacheKind = "read" | "process" | "none";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type FirstPartyMcpToolName, type SiteLimitProfileKind } from "@opengeni/contracts";
|
|
2
|
-
import type
|
|
1
|
+
import { type FirstPartyMcpToolName, type Permission, type SiteLimitProfileKind } from "@opengeni/contracts";
|
|
2
|
+
import { type ApiRouteDeps } from "@opengeni/core";
|
|
3
3
|
import { type SiteRuntimeConversationViewerKind } from "@opengeni/db";
|
|
4
4
|
import type { PublishedSiteDataViewer } from "../site-data/service.js";
|
|
5
5
|
import { type SiteRuntimeHost } from "./runtime.js";
|
|
@@ -21,8 +21,6 @@ import { type SiteRuntimeHost } from "./runtime.js";
|
|
|
21
21
|
export declare const SITE_AGENT_DATA_TOOLS: readonly ["site_data_query", "site_data_write"];
|
|
22
22
|
export declare const SITE_AGENT_SQL_TOOLS: readonly ["site_sql_run"];
|
|
23
23
|
export declare const SITE_AGENT_MEMBER_SQL_TOOLS: readonly ["site_sql_query"];
|
|
24
|
-
/** The first-party permissions those tools need (Site Data writes need publish). */
|
|
25
|
-
export declare const SITE_AGENT_DATA_TOOL_PERMISSIONS: readonly ["artifacts:read", "artifacts:publish"];
|
|
26
24
|
export declare const SITE_AGENT_OTHER_SITE_MESSAGE = "This Site's assistant can only use this Site's data";
|
|
27
25
|
export declare const SITE_AGENT_MANAGEMENT_MESSAGE = "This Site's assistant can use the Site's data, not manage the Site";
|
|
28
26
|
/** The data tools one viewer's Site agent conversation gets, within the deployment's tools. */
|
|
@@ -32,6 +30,8 @@ export declare function siteAgentDataToolNames(input: {
|
|
|
32
30
|
siteSql: boolean;
|
|
33
31
|
/** The deployment's first-party tool allowlist. */
|
|
34
32
|
allowed: readonly string[];
|
|
33
|
+
/** Live member ceiling; omitted only where the native caller filters tools itself. */
|
|
34
|
+
memberPermissions?: readonly Permission[];
|
|
35
35
|
}): FirstPartyMcpToolName[];
|
|
36
36
|
/** Whether Site SQL is offered on this deployment and turned on for the Site. */
|
|
37
37
|
export declare function siteAgentSiteSqlOn(deps: Pick<ApiRouteDeps, "db" | "settings">, scope: {
|
|
@@ -42,7 +42,7 @@ export declare function siteAgentSiteSqlOn(deps: Pick<ApiRouteDeps, "db" | "sett
|
|
|
42
42
|
export declare function siteAgentDataToolsForStart(deps: Pick<ApiRouteDeps, "db" | "settings">, scope: {
|
|
43
43
|
accountId: string;
|
|
44
44
|
workspaceId: string;
|
|
45
|
-
}, siteId: string, viewerKind: SiteRuntimeConversationViewerKind): Promise<FirstPartyMcpToolName[]>;
|
|
45
|
+
}, siteId: string, viewerKind: SiteRuntimeConversationViewerKind, memberPermissions?: readonly Permission[]): Promise<FirstPartyMcpToolName[]>;
|
|
46
46
|
/** This Site's viewer (`user:<subject>` or `guest:<visitor>`) as Site Data and Site SQL see it. */
|
|
47
47
|
export declare function sitePublishedDataViewer(host: Pick<SiteRuntimeHost, "scope" | "site">, principal: string, guest: boolean): PublishedSiteDataViewer;
|
|
48
48
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type Permission } from "@opengeni/contracts";
|
|
1
2
|
import { type ApiRouteDeps } from "@opengeni/core";
|
|
2
3
|
import type { SiteRuntimeHost } from "./runtime.js";
|
|
3
4
|
import type { SiteRuntimeViewer } from "./viewer.js";
|
|
4
|
-
export declare const SITE_AGENT_DEFAULTS_VERSION =
|
|
5
|
+
export declare const SITE_AGENT_DEFAULTS_VERSION = 2;
|
|
5
6
|
/**
|
|
6
7
|
* Before admitting more work, converge pre-redesign published-host sessions to
|
|
7
8
|
* the same Site-local tools as a new conversation. This is a trusted host
|
|
@@ -13,4 +14,4 @@ export declare const SITE_AGENT_DEFAULTS_VERSION = 1;
|
|
|
13
14
|
* audit event and completion marker atomic. Failed writes remain retryable.
|
|
14
15
|
* Existing attempts keep their accepted execution policy; no turn is rewritten.
|
|
15
16
|
*/
|
|
16
|
-
export declare function normalizeSiteAgentSession(deps: Pick<ApiRouteDeps, "db" | "settings" | "bus" | "objectStorage">, host: SiteRuntimeHost, viewer: SiteRuntimeViewer, sessionId: string, retainedToolIds: readonly string[]): Promise<void>;
|
|
17
|
+
export declare function normalizeSiteAgentSession(deps: Pick<ApiRouteDeps, "db" | "settings" | "bus" | "objectStorage">, host: SiteRuntimeHost, viewer: SiteRuntimeViewer, sessionId: string, retainedToolIds: readonly string[], memberPermissions?: readonly Permission[]): Promise<void>;
|
|
@@ -7,8 +7,8 @@ import { type SiteRuntimeViewer } from "./viewer.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Site-runtime: the narrow API a Site gateway calls for one published Site
|
|
9
9
|
* host, after the gateway signature passed. Everything here works for exactly
|
|
10
|
-
* one Site: the host names it
|
|
11
|
-
*
|
|
10
|
+
* one Site: the host names it and the viewer is resolved for it. Members retain
|
|
11
|
+
* their human identity; outsiders use the viewer-bound `site:<siteId>` principal.
|
|
12
12
|
*/
|
|
13
13
|
export type SiteRuntimeDeps = Pick<ApiRouteDeps, "db" | "settings" | "objectStorage">;
|
|
14
14
|
export type SiteRuntimeHost = {
|
|
@@ -43,7 +43,7 @@ export declare function resolveSiteRuntimeViewer(deps: SiteRuntimeDeps, host: Si
|
|
|
43
43
|
ignoreViewAs?: boolean;
|
|
44
44
|
}): Promise<SiteRuntimeViewer>;
|
|
45
45
|
/** What a viewer cannot do on a Site: 403 with `details.code` `not_available_for_viewer`. */
|
|
46
|
-
export type SiteViewerFeature = "agent" | "agent_model" | "workspace_tools" | "site_data" | "site_sql";
|
|
46
|
+
export type SiteViewerFeature = "agent" | "private_conversations" | "agent_model" | "workspace_tools" | "site_data" | "site_sql";
|
|
47
47
|
export declare function siteNotAvailableForViewer(feature: SiteViewerFeature, message: string, extra?: Record<string, unknown>): import("../http/api-error.js").ApiHttpError;
|
|
48
48
|
/** The viewer capabilities a page reads from `site.config` to hide what won't work. */
|
|
49
49
|
export declare function siteViewerCapabilities(deps: SiteRuntimeDeps, host: SiteRuntimeHost, viewer: SiteRuntimeViewer, limits: SiteLimits): SiteViewerCapabilities;
|
|
@@ -160,9 +160,9 @@ export declare function meterSiteRuntime(deps: SiteRuntimeDeps, host: SiteRuntim
|
|
|
160
160
|
viewer?: SiteRuntimeViewer;
|
|
161
161
|
}): Promise<void>;
|
|
162
162
|
/**
|
|
163
|
-
* Run an ordinary session route in this process as the
|
|
164
|
-
* principal
|
|
165
|
-
*
|
|
163
|
+
* Run an ordinary session route in this process as the verified member, or
|
|
164
|
+
* the Site service principal for an outsider. The latter uses a 60-second
|
|
165
|
+
* delegated token plus the separate trusted viewer audience. The Site and the
|
|
166
166
|
* served version ride along as the request's Site origin, so new sessions and
|
|
167
167
|
* turns record where they came from. The route authorizes as usual.
|
|
168
168
|
*/
|
|
@@ -174,6 +174,7 @@ export declare function siteRuntimeSessionRequest(deps: SiteRuntimeDeps, host: S
|
|
|
174
174
|
path: string;
|
|
175
175
|
body?: unknown;
|
|
176
176
|
extraPermissions?: readonly Permission[];
|
|
177
|
+
reauthorize: () => Promise<void>;
|
|
177
178
|
}): Promise<Request>;
|
|
178
179
|
/** A forwarded response without cookies (site-runtime never sets any). */
|
|
179
180
|
export declare function siteRuntimeForwardedResponse(response: Response): Response;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ErrorCode, type Permission, type SiteHostMapRecord, type SiteHostnameAvailability, type SitePublication, type UpsertSitePublicationRequest } from "@opengeni/contracts";
|
|
2
2
|
import type { AccessGrant } from "@opengeni/contracts";
|
|
3
|
-
import { type ApiRouteDeps } from "@opengeni/core";
|
|
3
|
+
import { type ApiRouteDeps, type cancelPublishedSiteSession } from "@opengeni/core";
|
|
4
4
|
import { SitePublishingError, type PublishedSiteRecord, type SiteDataSite, type SiteHostnameEdgeState, type SitePublicationRecord, type SitePublishingScope, type StoredSiteLimitProfile } from "@opengeni/db";
|
|
5
5
|
import { ApiHttpError } from "../http/api-error.js";
|
|
6
6
|
import { type SiteContentScanner } from "./content-scanner.js";
|
|
@@ -40,13 +40,12 @@ export type SitePublishingEffects = {
|
|
|
40
40
|
};
|
|
41
41
|
/** Background effects running at once per process. */
|
|
42
42
|
export declare const SITE_EFFECT_CONCURRENCY = 4;
|
|
43
|
+
type SiteCancellation = (input: Parameters<typeof cancelPublishedSiteSession>[1]) => Promise<unknown>;
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* session routes. Without it (the operator script) cancellation is skipped.
|
|
45
|
+
* Internal host lifecycle adapter. This never grants the Site service bearer
|
|
46
|
+
* ordinary session access and never impersonates a conversation's viewer.
|
|
47
47
|
*/
|
|
48
|
-
export declare function
|
|
49
|
-
export declare function sitePublishingDispatch(settings: ApiRouteDeps["settings"]): ((request: Request) => Promise<Response>) | null;
|
|
48
|
+
export declare function setSitePublishingCancellation(settings: ApiRouteDeps["settings"], cancel: SiteCancellation): void;
|
|
50
49
|
type SitePublishingEffectsOptions = {
|
|
51
50
|
fetch?: typeof fetch;
|
|
52
51
|
backoffMs?: number;
|
|
@@ -164,11 +163,9 @@ export declare function siteServiceSessionRequest(deps: Pick<SitePublishingDeps,
|
|
|
164
163
|
* Stop a Site's viewers after a takedown, unpublish or narrower access:
|
|
165
164
|
* revoke every site-session now, and after commit cancel the Site's agent
|
|
166
165
|
* conversations that may still be working (in-flight turns and queued
|
|
167
|
-
* messages),
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* script) the cancellation is skipped and logged; per-conversation caps
|
|
171
|
-
* still bound what running turns can spend.
|
|
166
|
+
* messages), through the native cancel-only lifecycle adapter. The original
|
|
167
|
+
* actor survives queued background execution. No transcript access is granted.
|
|
168
|
+
* Every page is drained, including old conversations; there is no age cutoff.
|
|
172
169
|
*/
|
|
173
170
|
export declare function stopSiteViewers(deps: SitePublishingDeps, scope: SitePublishingScope, siteId: string, options: {
|
|
174
171
|
revokeSessions?: boolean;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function bindSiteSessionRequestAuthority(request: Request, reauthorize: () => Promise<void>): void;
|
|
2
|
+
/** Periodic SSE reauthorization must check the Site cookie, not only its delegate. */
|
|
3
|
+
export declare function reauthorizeSiteSessionRequest(request: Request): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Opengeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -45,29 +45,29 @@
|
|
|
45
45
|
"@keeper-security/secrets-manager-core": "17.6.0",
|
|
46
46
|
"@llamaindex/liteparse": "2.14.2",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
|
-
"@opengeni/agent-proto": "3.
|
|
49
|
-
"@opengeni/artifact-tool": "3.
|
|
50
|
-
"@opengeni/capabilities": "3.
|
|
51
|
-
"@opengeni/codemode": "3.
|
|
52
|
-
"@opengeni/codex": "3.
|
|
53
|
-
"@opengeni/config": "3.
|
|
54
|
-
"@opengeni/contracts": "3.
|
|
55
|
-
"@opengeni/core": "3.
|
|
56
|
-
"@opengeni/db": "3.
|
|
57
|
-
"@opengeni/documents": "3.
|
|
58
|
-
"@opengeni/events": "3.
|
|
59
|
-
"@opengeni/github": "3.
|
|
60
|
-
"@opengeni/interaction": "3.
|
|
61
|
-
"@opengeni/network": "3.
|
|
62
|
-
"@opengeni/observability": "3.
|
|
63
|
-
"@opengeni/runtime": "3.
|
|
64
|
-
"@opengeni/storage": "3.
|
|
65
|
-
"@opengeni/tool-gateway": "3.
|
|
66
|
-
"@opengeni/vaults": "3.
|
|
67
|
-
"@opengeni/vaults-keeper": "3.
|
|
68
|
-
"@opengeni/vaults-onepassword": "3.
|
|
69
|
-
"@opengeni/vaults-protonpass": "3.
|
|
70
|
-
"@opengeni/xai-subscription": "3.
|
|
48
|
+
"@opengeni/agent-proto": "3.3.0",
|
|
49
|
+
"@opengeni/artifact-tool": "3.3.0",
|
|
50
|
+
"@opengeni/capabilities": "3.3.0",
|
|
51
|
+
"@opengeni/codemode": "3.3.0",
|
|
52
|
+
"@opengeni/codex": "3.3.0",
|
|
53
|
+
"@opengeni/config": "3.3.0",
|
|
54
|
+
"@opengeni/contracts": "3.3.0",
|
|
55
|
+
"@opengeni/core": "3.3.0",
|
|
56
|
+
"@opengeni/db": "3.3.0",
|
|
57
|
+
"@opengeni/documents": "3.3.0",
|
|
58
|
+
"@opengeni/events": "3.3.0",
|
|
59
|
+
"@opengeni/github": "3.3.0",
|
|
60
|
+
"@opengeni/interaction": "3.3.0",
|
|
61
|
+
"@opengeni/network": "3.3.0",
|
|
62
|
+
"@opengeni/observability": "3.3.0",
|
|
63
|
+
"@opengeni/runtime": "3.3.0",
|
|
64
|
+
"@opengeni/storage": "3.3.0",
|
|
65
|
+
"@opengeni/tool-gateway": "3.3.0",
|
|
66
|
+
"@opengeni/vaults": "3.3.0",
|
|
67
|
+
"@opengeni/vaults-keeper": "3.3.0",
|
|
68
|
+
"@opengeni/vaults-onepassword": "3.3.0",
|
|
69
|
+
"@opengeni/vaults-protonpass": "3.3.0",
|
|
70
|
+
"@opengeni/xai-subscription": "3.3.0",
|
|
71
71
|
"@temporalio/client": "^1.17.0",
|
|
72
72
|
"ajv": "^8.20.0",
|
|
73
73
|
"better-auth": "^1.6.14",
|
package/src/access-grant-rls.ts
CHANGED
|
@@ -87,6 +87,7 @@ export async function withAccessGrantSessionRlsContext<T>(
|
|
|
87
87
|
{
|
|
88
88
|
subjectId: actor.subjectId,
|
|
89
89
|
initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
|
|
90
|
+
agentRootSessionId: actor.callerRootSessionId,
|
|
90
91
|
privateFileOwnerSubjectId:
|
|
91
92
|
learning.defaultScope === "personal" ? learning.subjectId : null,
|
|
92
93
|
},
|
package/src/http/sse.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getWorkspaceInteractionRevisionState,
|
|
8
8
|
listSessionEventPage,
|
|
9
9
|
listWorkspaceControlEvents,
|
|
10
|
+
getWorkspaceControlEvent,
|
|
10
11
|
readSessionEventStorageGapEnd,
|
|
11
12
|
type Database,
|
|
12
13
|
} from "@opengeni/db";
|
|
@@ -677,7 +678,10 @@ export async function sseWorkspaceControlStream(
|
|
|
677
678
|
}
|
|
678
679
|
}
|
|
679
680
|
}
|
|
680
|
-
|
|
681
|
+
// The bus is workspace-wide, not an authorization projection. Re-read the
|
|
682
|
+
// exact durable row under this stream's actor before delivering live data.
|
|
683
|
+
const visible = await getWorkspaceControlEvent(db, workspaceId, event.id);
|
|
684
|
+
if (visible) await writeFrame(formatWorkspaceControlEventSse(visible));
|
|
681
685
|
lastSent = event.sequence;
|
|
682
686
|
};
|
|
683
687
|
delivery = createLatestWinsDelivery(send, fail);
|
|
@@ -151,6 +151,7 @@ import {
|
|
|
151
151
|
import {
|
|
152
152
|
requireAccessGrant,
|
|
153
153
|
requireSessionAuthorization,
|
|
154
|
+
withSessionAuthorizationReadReuse,
|
|
154
155
|
recordWorkspaceUsage,
|
|
155
156
|
requireLimit,
|
|
156
157
|
relayConfigFromSettings,
|
|
@@ -2834,6 +2835,7 @@ export function registerBrowserSessionRoutes(
|
|
|
2834
2835
|
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
2835
2836
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
2836
2837
|
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
2838
|
+
browserControlHttpSupported: enrollment.agentCapabilities.browserControlHttp === true,
|
|
2837
2839
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
2838
2840
|
enrollment.opStream === true &&
|
|
2839
2841
|
deps.bus.getOpStreamConnection
|
|
@@ -3157,16 +3159,40 @@ export function registerBrowserSessionRoutes(
|
|
|
3157
3159
|
browserSessionId: string,
|
|
3158
3160
|
authorizationOperation: SessionAuthorizationOperation,
|
|
3159
3161
|
channelOperation: ChannelAOperation,
|
|
3160
|
-
callback:
|
|
3161
|
-
client: BrowserControlClient;
|
|
3162
|
-
sessionClient: ReturnType<BrowserControlClient["sessionClient"]>;
|
|
3163
|
-
record: BrowserSessionControlRecord;
|
|
3164
|
-
binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
|
|
3165
|
-
placement: BrowserPlacement;
|
|
3166
|
-
sourceAuthorization: ResolvedSessionAuthorization | null;
|
|
3167
|
-
}) => Promise<T>,
|
|
3162
|
+
callback: ActiveBrowserControllerCallback<T>,
|
|
3168
3163
|
recoverMissing = true,
|
|
3169
3164
|
stages?: InteractionRouteStages,
|
|
3165
|
+
): Promise<T> {
|
|
3166
|
+
// The source-session check and the machine-route check that follows it
|
|
3167
|
+
// prove the same live agent attempt; they share those reads. Everything
|
|
3168
|
+
// from the controller call onward reads fresh.
|
|
3169
|
+
return await withSessionAuthorizationReadReuse((authorizationReads) =>
|
|
3170
|
+
withActiveBrowserControllerReads(
|
|
3171
|
+
context,
|
|
3172
|
+
grant,
|
|
3173
|
+
workspaceId,
|
|
3174
|
+
browserSessionId,
|
|
3175
|
+
authorizationOperation,
|
|
3176
|
+
channelOperation,
|
|
3177
|
+
callback,
|
|
3178
|
+
recoverMissing,
|
|
3179
|
+
stages,
|
|
3180
|
+
authorizationReads,
|
|
3181
|
+
),
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
async function withActiveBrowserControllerReads<T>(
|
|
3186
|
+
context: Context,
|
|
3187
|
+
grant: AccessGrant,
|
|
3188
|
+
workspaceId: string,
|
|
3189
|
+
browserSessionId: string,
|
|
3190
|
+
authorizationOperation: SessionAuthorizationOperation,
|
|
3191
|
+
channelOperation: ChannelAOperation,
|
|
3192
|
+
callback: ActiveBrowserControllerCallback<T>,
|
|
3193
|
+
recoverMissing: boolean,
|
|
3194
|
+
stages: InteractionRouteStages | undefined,
|
|
3195
|
+
authorizationReads: { handOffToToolDispatch(): void },
|
|
3170
3196
|
): Promise<T> {
|
|
3171
3197
|
try {
|
|
3172
3198
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
@@ -3175,17 +3201,25 @@ export function registerBrowserSessionRoutes(
|
|
|
3175
3201
|
browserSessionId,
|
|
3176
3202
|
});
|
|
3177
3203
|
stages?.mark("record");
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3204
|
+
// A Connected Machine (or any placement without a cached controller URL)
|
|
3205
|
+
// always needs the source session; read it alongside authorization. A
|
|
3206
|
+
// denial still wins, and the session is only used once authorized.
|
|
3207
|
+
const sourceSessionNeeded =
|
|
3208
|
+
record.session.placement?.kind === "connected_machine" ||
|
|
3209
|
+
!record.controllerHostSandboxGroupId;
|
|
3210
|
+
const [authorized, sourceRead] = await Promise.allSettled([
|
|
3211
|
+
authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation),
|
|
3212
|
+
sourceSessionNeeded
|
|
3213
|
+
? requireSourceSession(deps, workspaceId, record.sourceSessionId)
|
|
3214
|
+
: Promise.resolve(null),
|
|
3215
|
+
]);
|
|
3216
|
+
if (authorized.status === "rejected") throw authorized.reason;
|
|
3217
|
+
if (sourceRead.status === "rejected") throw sourceRead.reason;
|
|
3218
|
+
const sourceAuthorization = authorized.value;
|
|
3184
3219
|
stages?.mark("authorize");
|
|
3185
|
-
let sourceSession: Session | null =
|
|
3220
|
+
let sourceSession: Session | null = sourceRead.value;
|
|
3186
3221
|
if (record.session.placement?.kind === "connected_machine") {
|
|
3187
|
-
|
|
3188
|
-
if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
|
|
3222
|
+
if (sourceSession?.activeSandboxId !== record.session.placement.sandboxId) {
|
|
3189
3223
|
return await throwBrowserSourcePlacementChanged(
|
|
3190
3224
|
grant,
|
|
3191
3225
|
record.sourceSessionId,
|
|
@@ -3217,6 +3251,7 @@ export function registerBrowserSessionRoutes(
|
|
|
3217
3251
|
placement: BrowserPlacement,
|
|
3218
3252
|
provisionedClient?: BrowserControlClient,
|
|
3219
3253
|
): Promise<T> => {
|
|
3254
|
+
authorizationReads.handOffToToolDispatch();
|
|
3220
3255
|
// An active binding already proves browserd was provisioned. Reusing
|
|
3221
3256
|
// it avoids restarting/checking the sidecar on every live input.
|
|
3222
3257
|
const client = provisionedClient ?? connectController(deps, grant, record, placement);
|
|
@@ -4631,6 +4666,15 @@ async function settleBrowserRevisionCaptureFailure(
|
|
|
4631
4666
|
});
|
|
4632
4667
|
}
|
|
4633
4668
|
|
|
4669
|
+
type ActiveBrowserControllerCallback<T> = (input: {
|
|
4670
|
+
client: BrowserControlClient;
|
|
4671
|
+
sessionClient: ReturnType<BrowserControlClient["sessionClient"]>;
|
|
4672
|
+
record: BrowserSessionControlRecord;
|
|
4673
|
+
binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
|
|
4674
|
+
placement: BrowserPlacement;
|
|
4675
|
+
sourceAuthorization: ResolvedSessionAuthorization | null;
|
|
4676
|
+
}) => Promise<T>;
|
|
4677
|
+
|
|
4634
4678
|
async function requireSourceSession(
|
|
4635
4679
|
deps: ApiRouteDeps,
|
|
4636
4680
|
workspaceId: string,
|
|
@@ -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);
|