@opengeni/api-router 4.4.1 → 4.5.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-3KRHXJPM.js → chunk-LXUAR3HS.js} +95 -20
- package/dist/{chunk-3KRHXJPM.js.map → chunk-LXUAR3HS.js.map} +1 -1
- package/dist/http/session-list-hints.d.ts +11 -1
- package/dist/http/sse.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/routes/client-errors.d.ts +1 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/package.json +24 -24
- package/src/app.ts +2 -0
- package/src/http/session-list-hints.ts +18 -5
- package/src/http/sse.ts +2 -2
- package/src/mcp/action-catalog.gen.ts +9 -0
- package/src/mcp/server.ts +12 -9
- package/src/routes/browser-sessions.ts +20 -0
- package/src/routes/interaction-resources.ts +2 -2
- package/src/routes/sessions.ts +30 -0
- package/src/routes/workspace-artifacts.ts +30 -0
|
@@ -5,13 +5,23 @@ export type SessionListHint = {
|
|
|
5
5
|
available: boolean;
|
|
6
6
|
/** null means reconcile all loaded scopes, including formerly visible rows. */
|
|
7
7
|
sessionIds: string[] | null;
|
|
8
|
+
/**
|
|
9
|
+
* The viewer's changed Site conversations, which ordinary lists omit. Only
|
|
10
|
+
* a surface that shows them (the rail's Site rows) needs to act on these.
|
|
11
|
+
*/
|
|
12
|
+
siteSessionIds?: string[];
|
|
13
|
+
};
|
|
14
|
+
/** Authorized hint IDs; a plain array has no Site conversations. */
|
|
15
|
+
export type SessionListHintIds = string[] | {
|
|
16
|
+
sessionIds: string[];
|
|
17
|
+
siteSessionIds: string[];
|
|
8
18
|
};
|
|
9
19
|
/** Transient commit hints; only the subsequent authorized list read is truth. */
|
|
10
20
|
export declare function subscribeSessionListHints(options: {
|
|
11
21
|
source: SessionListChangeSource;
|
|
12
22
|
workspaceId: string;
|
|
13
23
|
subjectId: string;
|
|
14
|
-
authorizeIds: (ids: string[]) => Promise<
|
|
24
|
+
authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
|
|
15
25
|
write: (hint: SessionListHint) => Promise<unknown>;
|
|
16
26
|
fail: (error: unknown) => void;
|
|
17
27
|
intervalMs: number;
|
package/dist/http/sse.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type SessionEvent } from "@opengeni/contracts";
|
|
|
2
2
|
import { type Database, type SessionListChangeSource } from "@opengeni/db";
|
|
3
3
|
import { type EventBus } from "@opengeni/events";
|
|
4
4
|
import type { Observability } from "@opengeni/observability";
|
|
5
|
+
import { type SessionListHintIds } from "./session-list-hints.js";
|
|
5
6
|
export declare const SSE_QUEUED_FRAME_MAX_COUNT = 1;
|
|
6
7
|
export declare const SSE_WRITE_STALL_TIMEOUT_MS = 30000;
|
|
7
8
|
export declare const SSE_HEARTBEAT_INTERVAL_MS = 15000;
|
|
@@ -78,7 +79,7 @@ export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
|
|
|
78
79
|
sessionLists?: {
|
|
79
80
|
source: SessionListChangeSource;
|
|
80
81
|
subjectId: string;
|
|
81
|
-
authorizeIds: (ids: string[]) => Promise<
|
|
82
|
+
authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
|
|
82
83
|
} | undefined;
|
|
83
84
|
};
|
|
84
85
|
/**
|
package/dist/index.js
CHANGED
|
@@ -121,6 +121,7 @@ export declare const ClientSignalReport: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
121
121
|
settings: "settings";
|
|
122
122
|
"setup-account": "setup-account";
|
|
123
123
|
"site-auth": "site-auth";
|
|
124
|
+
sites: "sites";
|
|
124
125
|
state: "state";
|
|
125
126
|
"variable-sets": "variable-sets";
|
|
126
127
|
}>;
|
|
@@ -19,6 +19,7 @@ export declare function sessionTenancyHttpError(error: unknown): Error;
|
|
|
19
19
|
export declare function sessionListQuery(query: Record<string, string>, allowCursor?: boolean): {
|
|
20
20
|
originSiteId: string | undefined;
|
|
21
21
|
siteAccessId: string | undefined;
|
|
22
|
+
siteConversations: "include" | "group" | undefined;
|
|
22
23
|
limit: string | undefined;
|
|
23
24
|
parentSessionId: string | null | undefined;
|
|
24
25
|
cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.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": "4.
|
|
49
|
-
"@opengeni/artifact-tool": "4.
|
|
50
|
-
"@opengeni/capabilities": "4.
|
|
51
|
-
"@opengeni/codemode": "4.
|
|
52
|
-
"@opengeni/codex": "4.
|
|
53
|
-
"@opengeni/config": "4.
|
|
54
|
-
"@opengeni/contracts": "4.
|
|
55
|
-
"@opengeni/core": "4.
|
|
56
|
-
"@opengeni/db": "4.
|
|
57
|
-
"@opengeni/documents": "4.
|
|
58
|
-
"@opengeni/events": "4.
|
|
59
|
-
"@opengeni/github": "4.
|
|
60
|
-
"@opengeni/interaction": "4.
|
|
61
|
-
"@opengeni/network": "4.
|
|
62
|
-
"@opengeni/observability": "4.
|
|
63
|
-
"@opengeni/runtime": "4.
|
|
64
|
-
"@opengeni/storage": "4.
|
|
65
|
-
"@opengeni/tool-gateway": "4.
|
|
66
|
-
"@opengeni/vaults": "4.
|
|
67
|
-
"@opengeni/vaults-keeper": "4.
|
|
68
|
-
"@opengeni/vaults-onepassword": "4.
|
|
69
|
-
"@opengeni/vaults-protonpass": "4.
|
|
70
|
-
"@opengeni/xai-subscription": "4.
|
|
48
|
+
"@opengeni/agent-proto": "4.5.0",
|
|
49
|
+
"@opengeni/artifact-tool": "4.5.0",
|
|
50
|
+
"@opengeni/capabilities": "4.5.0",
|
|
51
|
+
"@opengeni/codemode": "4.5.0",
|
|
52
|
+
"@opengeni/codex": "4.5.0",
|
|
53
|
+
"@opengeni/config": "4.5.0",
|
|
54
|
+
"@opengeni/contracts": "4.5.0",
|
|
55
|
+
"@opengeni/core": "4.5.0",
|
|
56
|
+
"@opengeni/db": "4.5.0",
|
|
57
|
+
"@opengeni/documents": "4.5.0",
|
|
58
|
+
"@opengeni/events": "4.5.0",
|
|
59
|
+
"@opengeni/github": "4.5.0",
|
|
60
|
+
"@opengeni/interaction": "4.5.0",
|
|
61
|
+
"@opengeni/network": "4.5.0",
|
|
62
|
+
"@opengeni/observability": "4.5.0",
|
|
63
|
+
"@opengeni/runtime": "4.5.0",
|
|
64
|
+
"@opengeni/storage": "4.5.0",
|
|
65
|
+
"@opengeni/tool-gateway": "4.5.0",
|
|
66
|
+
"@opengeni/vaults": "4.5.0",
|
|
67
|
+
"@opengeni/vaults-keeper": "4.5.0",
|
|
68
|
+
"@opengeni/vaults-onepassword": "4.5.0",
|
|
69
|
+
"@opengeni/vaults-protonpass": "4.5.0",
|
|
70
|
+
"@opengeni/xai-subscription": "4.5.0",
|
|
71
71
|
"@temporalio/client": "^1.17.0",
|
|
72
72
|
"ajv": "^8.20.0",
|
|
73
73
|
"better-auth": "^1.6.14",
|
package/src/app.ts
CHANGED
|
@@ -193,6 +193,7 @@ import {
|
|
|
193
193
|
registerProductUsageMetricBaselines,
|
|
194
194
|
stampPersonalApiKeyRequest,
|
|
195
195
|
verifiedDelegatedHumanAuthorizationForRequest,
|
|
196
|
+
browserExtension,
|
|
196
197
|
} from "@opengeni/core";
|
|
197
198
|
import { createManagedAuthNewSignupsGate } from "./auth/new-signups-gate";
|
|
198
199
|
import {
|
|
@@ -1516,6 +1517,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
1516
1517
|
deploymentRevision: deps.settings.deploymentRevision,
|
|
1517
1518
|
claudeSubscriptionEnabled: deps.settings.claudeSubscriptionEnabled,
|
|
1518
1519
|
connectedMachines: deps.settings.sandboxSelfhostedEnabled,
|
|
1520
|
+
browserExtension: browserExtension(deps.settings),
|
|
1519
1521
|
apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
|
|
1520
1522
|
...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
|
|
1521
1523
|
defaultModel,
|
|
@@ -6,14 +6,22 @@ export type SessionListHint = {
|
|
|
6
6
|
available: boolean;
|
|
7
7
|
/** null means reconcile all loaded scopes, including formerly visible rows. */
|
|
8
8
|
sessionIds: string[] | null;
|
|
9
|
+
/**
|
|
10
|
+
* The viewer's changed Site conversations, which ordinary lists omit. Only
|
|
11
|
+
* a surface that shows them (the rail's Site rows) needs to act on these.
|
|
12
|
+
*/
|
|
13
|
+
siteSessionIds?: string[];
|
|
9
14
|
};
|
|
10
15
|
|
|
16
|
+
/** Authorized hint IDs; a plain array has no Site conversations. */
|
|
17
|
+
export type SessionListHintIds = string[] | { sessionIds: string[]; siteSessionIds: string[] };
|
|
18
|
+
|
|
11
19
|
/** Transient commit hints; only the subsequent authorized list read is truth. */
|
|
12
20
|
export function subscribeSessionListHints(options: {
|
|
13
21
|
source: SessionListChangeSource;
|
|
14
22
|
workspaceId: string;
|
|
15
23
|
subjectId: string;
|
|
16
|
-
authorizeIds: (ids: string[]) => Promise<
|
|
24
|
+
authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
|
|
17
25
|
write: (hint: SessionListHint) => Promise<unknown>;
|
|
18
26
|
fail: (error: unknown) => void;
|
|
19
27
|
intervalMs: number;
|
|
@@ -24,12 +32,13 @@ export function subscribeSessionListHints(options: {
|
|
|
24
32
|
let all = false;
|
|
25
33
|
const ids = new Set<string>();
|
|
26
34
|
let tail = Promise.resolve();
|
|
27
|
-
const emit = (available: boolean, sessionIds: string[] | null) =>
|
|
35
|
+
const emit = (available: boolean, sessionIds: string[] | null, siteSessionIds: string[] = []) =>
|
|
28
36
|
options.write({
|
|
29
37
|
type: "workspace.sessions.changed",
|
|
30
38
|
workspaceId: options.workspaceId,
|
|
31
39
|
available,
|
|
32
40
|
sessionIds,
|
|
41
|
+
...(siteSessionIds.length ? { siteSessionIds } : {}),
|
|
33
42
|
});
|
|
34
43
|
const enqueue = (action: () => Promise<unknown>) => {
|
|
35
44
|
tail = tail
|
|
@@ -73,10 +82,14 @@ export function subscribeSessionListHints(options: {
|
|
|
73
82
|
ids.clear();
|
|
74
83
|
enqueue(async () => {
|
|
75
84
|
if (observedGeneration !== generation || !connected) return;
|
|
76
|
-
const
|
|
85
|
+
const authorized = reconcileAll ? null : await options.authorizeIds(pendingIds);
|
|
77
86
|
if (observedGeneration !== generation || !connected) return;
|
|
78
|
-
if (
|
|
79
|
-
|
|
87
|
+
if (authorized === null) return await emit(true, null);
|
|
88
|
+
const visible = Array.isArray(authorized)
|
|
89
|
+
? { sessionIds: authorized, siteSessionIds: [] }
|
|
90
|
+
: authorized;
|
|
91
|
+
if (!visible.sessionIds.length && !visible.siteSessionIds.length) return;
|
|
92
|
+
await emit(true, visible.sessionIds, visible.siteSessionIds);
|
|
80
93
|
});
|
|
81
94
|
}, options.intervalMs);
|
|
82
95
|
return () => {
|
package/src/http/sse.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "@opengeni/events";
|
|
23
23
|
import type { Observability } from "@opengeni/observability";
|
|
24
24
|
import { MANAGED_AUTH_ACTOR_EPOCH_HEADER } from "@opengeni/core/managed-auth-session-sets";
|
|
25
|
-
import { subscribeSessionListHints } from "./session-list-hints";
|
|
25
|
+
import { subscribeSessionListHints, type SessionListHintIds } from "./session-list-hints";
|
|
26
26
|
|
|
27
27
|
const SESSION_REPLAY_PAGE_SIZE = 100;
|
|
28
28
|
const SESSION_REPLAY_PAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -744,7 +744,7 @@ export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
|
|
|
744
744
|
| {
|
|
745
745
|
source: SessionListChangeSource;
|
|
746
746
|
subjectId: string;
|
|
747
|
-
authorizeIds: (ids: string[]) => Promise<
|
|
747
|
+
authorizeIds: (ids: string[]) => Promise<SessionListHintIds>;
|
|
748
748
|
}
|
|
749
749
|
| undefined;
|
|
750
750
|
};
|
|
@@ -6407,6 +6407,15 @@ export const ACTION_CATALOG: readonly ActionCatalogEntry[] = [
|
|
|
6407
6407
|
"PersonalGitHubOAuthStartResponse"
|
|
6408
6408
|
]
|
|
6409
6409
|
},
|
|
6410
|
+
{
|
|
6411
|
+
"id": "recordSiteOpened",
|
|
6412
|
+
"method": "POST",
|
|
6413
|
+
"path": "/v1/workspaces/:workspaceId/published-artifacts/:artifactId/opened",
|
|
6414
|
+
"request": [],
|
|
6415
|
+
"response": [
|
|
6416
|
+
"SiteOpenedResponse"
|
|
6417
|
+
]
|
|
6418
|
+
},
|
|
6410
6419
|
{
|
|
6411
6420
|
"id": "recoverSandbox",
|
|
6412
6421
|
"method": "POST",
|
package/src/mcp/server.ts
CHANGED
|
@@ -210,9 +210,9 @@ import {
|
|
|
210
210
|
buildCapabilityCatalog,
|
|
211
211
|
CONNECTED_MACHINE_CAPABILITY_ID,
|
|
212
212
|
EXCEL_ADDIN_CAPABILITY_ID,
|
|
213
|
+
browserExtension,
|
|
213
214
|
nativeConnectionCapabilityRecommendations,
|
|
214
215
|
userSetupCardPayload,
|
|
215
|
-
OPENGENI_BROWSER_EXTENSION_URL,
|
|
216
216
|
requireLiveAgentAttemptAuthorization,
|
|
217
217
|
requireSessionAuthorization,
|
|
218
218
|
requireSessionAuthorizationListScope,
|
|
@@ -2297,7 +2297,7 @@ function registerSandboxFileArtifactTool(
|
|
|
2297
2297
|
"sandbox_file_publish",
|
|
2298
2298
|
{
|
|
2299
2299
|
description:
|
|
2300
|
-
"Publish one
|
|
2300
|
+
"Publish one file from this session's workspace so the person can open it. Use it for every file you hand over: document, spreadsheet, PDF, image, video, archive or data. Link the result as [Descriptive label](artifact:<artifactId>), or  to preview an image, video, audio or PDF inline; never give a sandbox: link instead. The artifactId is also the file's workspace file ID, which tools such as a browser upload accept. A new file gets this chat's visibility unless you pass visibility. To change who can open an existing file, such as a generated image, pass its artifactId and visibility instead of path. Check access.whoCanOpen in the result before sharing the link with anyone else.",
|
|
2301
2301
|
inputSchema: {
|
|
2302
2302
|
path: z4.string().min(1).max(4_096).optional(),
|
|
2303
2303
|
artifactId: z4.string().uuid().optional(),
|
|
@@ -4471,9 +4471,10 @@ function registerFleetTools(
|
|
|
4471
4471
|
// Same live-attempt authority the other fleet tools resolve, without
|
|
4472
4472
|
// requiring a session box: a chat with no sandbox can still connect one.
|
|
4473
4473
|
await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
4474
|
-
const item = nativeConnectionCapabilityRecommendations({
|
|
4475
|
-
|
|
4476
|
-
|
|
4474
|
+
const item = nativeConnectionCapabilityRecommendations({
|
|
4475
|
+
connectedMachines: true,
|
|
4476
|
+
browserExtension: browserExtension(deps.settings),
|
|
4477
|
+
}).find((candidate) => candidate.id === CONNECTED_MACHINE_CAPABILITY_ID)!;
|
|
4477
4478
|
const setup = await capabilitySetupProjection(
|
|
4478
4479
|
deps,
|
|
4479
4480
|
grant.workspaceId,
|
|
@@ -4501,7 +4502,7 @@ function registerFleetTools(
|
|
|
4501
4502
|
setup.status === "ready"
|
|
4502
4503
|
? `${setup.detail} The Connected Machine card lists them, so the person can choose one for this chat or connect another.`
|
|
4503
4504
|
: "The Connected Machine card was posted. It shows the person what to do, so do not describe its buttons or steps; nothing is connected yet."
|
|
4504
|
-
} Check sandboxes_list after they confirm. Chrome needs the browser extension (${
|
|
4505
|
+
} Check sandboxes_list after they confirm. Chrome needs the browser extension (${browserExtension(deps.settings).installUrl}) on the connected machine.`,
|
|
4505
4506
|
});
|
|
4506
4507
|
}
|
|
4507
4508
|
return json(
|
|
@@ -4531,7 +4532,7 @@ function registerConnectedMachineTools(
|
|
|
4531
4532
|
{
|
|
4532
4533
|
description:
|
|
4533
4534
|
"Create a single-use Connected Machine enrollment token for this workspace (expires in one hour) using existing enrollments:manage authority. Returns the token, expiry and Unix/PowerShell install commands. Use it when you can run commands on the target machine yourself (run_on, SSH, a VM you control): run the matching command there, then verify the machine with sandboxes_list. One token connects one machine; mint another for each additional machine (the same machine may rerun its command). When the person must run it on their own computer, post the Connected Machine card instead (capability_authorization_request with api:connected-machine), which mints the command in their browser; do not paste a token into the chat. Screen control is optional and defaults off; to turn it on for a machine that is already connected, use connected_machine_enable_screen_control instead of reconnecting. The token grants whole-machine access on enrollment; do not put it in public code or unrelated logs. Existing machine connections are preserved. For Chrome, the person installs the Opengeni Browser extension (" +
|
|
4534
|
-
|
|
4535
|
+
browserExtension(deps.settings).installUrl +
|
|
4535
4536
|
") after the machine is connected.",
|
|
4536
4537
|
inputSchema: { allowScreenControl: z4.boolean().optional() },
|
|
4537
4538
|
},
|
|
@@ -4563,7 +4564,7 @@ function registerConnectedMachineTools(
|
|
|
4563
4564
|
{
|
|
4564
4565
|
description:
|
|
4565
4566
|
"Turn screen control on for a machine that is already connected, in place, so agents can see and use its screen (computer and attached-Chrome tools). `target` is a sandboxes_list id or an enrollment id. Needs no human click, token, terminal or reconnect: the platform records the consent on that machine and its agent refreshes its own credentials over the live connection. status=active means it is on now. status=pending means it is allowed and turns on by itself: reason=offline when the machine next connects; reason=agent_update_required after its agent is updated (update it with the machine update action, POST /v1/workspaces/{workspaceId}/machines/{enrollmentId}/update); reason=renewal_failed: call again shortly; reason=reconnect_required: the machine's connection predates in-place changes, so rerun the connect command on it (connected_machine_enroll_token with allowScreenControl=true). On a Mac the OS must also allow Screen Recording, Accessibility and Input Monitoring for the app that runs the agent (the name macOS lists in those panes): open each pane on the Mac with run_on exec `open 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'` (Privacy_Accessibility, Privacy_ListenEvent; also the privacy-settings machine action) and ask the person to switch that app on there, choosing Quit & Reopen if macOS asks; the machine list reports runtime.macPermissions. Chrome additionally needs the Opengeni Browser extension (" +
|
|
4566
|
-
|
|
4567
|
+
browserExtension(deps.settings).installUrl +
|
|
4567
4568
|
") on that machine.",
|
|
4568
4569
|
inputSchema: { target: z4.string().min(1) },
|
|
4569
4570
|
},
|
|
@@ -6720,6 +6721,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
6720
6721
|
...current.items,
|
|
6721
6722
|
...nativeConnectionCapabilityRecommendations({
|
|
6722
6723
|
connectedMachines: deps.settings.sandboxSelfhostedEnabled,
|
|
6724
|
+
browserExtension: browserExtension(deps.settings),
|
|
6723
6725
|
}),
|
|
6724
6726
|
];
|
|
6725
6727
|
const ranked = searchCapabilityCatalogItems(candidates, query, limit ?? 8);
|
|
@@ -6793,6 +6795,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
6793
6795
|
...current.items,
|
|
6794
6796
|
...nativeConnectionCapabilityRecommendations({
|
|
6795
6797
|
connectedMachines: deps.settings.sandboxSelfhostedEnabled,
|
|
6798
|
+
browserExtension: browserExtension(deps.settings),
|
|
6796
6799
|
}),
|
|
6797
6800
|
].find((candidate) => candidate.id === capabilityId);
|
|
6798
6801
|
if (!item || !capabilityCatalogItemIsTrustedForExposure(item)) {
|
|
@@ -6832,7 +6835,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
6832
6835
|
capabilityId: item.id,
|
|
6833
6836
|
status: "ready",
|
|
6834
6837
|
eventId,
|
|
6835
|
-
message: `${setup.detail} The Connected Machine card is in this chat: the person can choose a machine for this chat or connect another. Chrome needs the browser extension (${
|
|
6838
|
+
message: `${setup.detail} The Connected Machine card is in this chat: the person can choose a machine for this chat or connect another. Chrome needs the browser extension (${browserExtension(deps.settings).installUrl}) on a connected machine.`,
|
|
6836
6839
|
});
|
|
6837
6840
|
}
|
|
6838
6841
|
if (githubCardWhenReady) {
|
|
@@ -6126,6 +6126,26 @@ export function parseBrowserScreenshotOptions(
|
|
|
6126
6126
|
export function browserTabDiagnosisError(error: unknown): unknown {
|
|
6127
6127
|
if (!(error instanceof BrowserControlRequestError)) return error;
|
|
6128
6128
|
const code = error.error.code;
|
|
6129
|
+
// A controller that says a resource is gone for good (for example, the
|
|
6130
|
+
// person's Chrome profile reconnected and this session no longer controls
|
|
6131
|
+
// it) answers 503 but not retryable. As a 503 it would reach the agent as
|
|
6132
|
+
// "temporarily unavailable, retry", and every retry fails the same way, so
|
|
6133
|
+
// keep the controller's guidance and present it as the conflict it is.
|
|
6134
|
+
if (
|
|
6135
|
+
code === "resource_unavailable" &&
|
|
6136
|
+
error.status >= 500 &&
|
|
6137
|
+
!error.retryable &&
|
|
6138
|
+
!legacyInteractionCapacityFailure(error, "browser")
|
|
6139
|
+
) {
|
|
6140
|
+
const failure = new ApiHttpError(409, {
|
|
6141
|
+
code: "conflict",
|
|
6142
|
+
message: error.error.message,
|
|
6143
|
+
retryable: false,
|
|
6144
|
+
outcomeUnknown: false,
|
|
6145
|
+
});
|
|
6146
|
+
failure.cause = error;
|
|
6147
|
+
return failure;
|
|
6148
|
+
}
|
|
6129
6149
|
if (code !== "tab_not_responding" && code !== "tab_crashed") return error;
|
|
6130
6150
|
const failure = new ApiHttpError(409, {
|
|
6131
6151
|
code,
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
listInteractionInterventions,
|
|
41
41
|
listNetworkRoutes,
|
|
42
42
|
listSiteAuthConnections,
|
|
43
|
-
|
|
43
|
+
listVisibleSessionInvalidationHints,
|
|
44
44
|
sessionListChangesFor,
|
|
45
45
|
resolveInteractionIntervention,
|
|
46
46
|
updateNetworkRoute,
|
|
@@ -113,7 +113,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
|
|
|
113
113
|
freshGrant,
|
|
114
114
|
"http",
|
|
115
115
|
);
|
|
116
|
-
return await
|
|
116
|
+
return await listVisibleSessionInvalidationHints(deps.db, workspaceId, ids, {
|
|
117
117
|
subjectId: freshGrant.subjectId,
|
|
118
118
|
...(authorizationScope ? { authorizationScope } : {}),
|
|
119
119
|
personalWorkspaceOwnerException:
|
package/src/routes/sessions.ts
CHANGED
|
@@ -1042,6 +1042,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1042
1042
|
...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
|
|
1043
1043
|
...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
|
|
1044
1044
|
...(query.siteAccessId ? { siteAccessId: query.siteAccessId } : {}),
|
|
1045
|
+
...(query.siteConversations ? { siteConversations: query.siteConversations } : {}),
|
|
1045
1046
|
...(query.createdBy ? { createdBy: query.createdBy } : {}),
|
|
1046
1047
|
...(query.updatedFrom ? { updatedFrom: query.updatedFrom } : {}),
|
|
1047
1048
|
...(query.updatedBefore ? { updatedBefore: query.updatedBefore } : {}),
|
|
@@ -5891,6 +5892,7 @@ export function sessionListQuery(
|
|
|
5891
5892
|
): {
|
|
5892
5893
|
originSiteId: string | undefined;
|
|
5893
5894
|
siteAccessId: string | undefined;
|
|
5895
|
+
siteConversations: "include" | "group" | undefined;
|
|
5894
5896
|
limit: string | undefined;
|
|
5895
5897
|
parentSessionId: string | null | undefined;
|
|
5896
5898
|
cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
|
|
@@ -5925,6 +5927,33 @@ export function sessionListQuery(
|
|
|
5925
5927
|
});
|
|
5926
5928
|
if (originSiteId !== undefined && !z.string().uuid().safeParse(originSiteId).success)
|
|
5927
5929
|
throw new HTTPException(400, { message: "originSiteId must be a Site id" });
|
|
5930
|
+
const siteConversations = query.siteConversations;
|
|
5931
|
+
if (siteConversations !== undefined) {
|
|
5932
|
+
if (siteConversations !== "include" && siteConversations !== "group")
|
|
5933
|
+
throw new HTTPException(400, { message: 'siteConversations must be "include" or "group"' });
|
|
5934
|
+
if (!allowCursor)
|
|
5935
|
+
throw new HTTPException(400, { message: 'siteConversations requires view="page"' });
|
|
5936
|
+
// A Site's own history is already explicit; Site frames never reach this.
|
|
5937
|
+
if (originSiteId !== undefined || siteAccessId !== undefined)
|
|
5938
|
+
throw new HTTPException(400, {
|
|
5939
|
+
message: "siteConversations cannot be combined with a Site filter",
|
|
5940
|
+
});
|
|
5941
|
+
if (
|
|
5942
|
+
siteConversations === "group" &&
|
|
5943
|
+
(query.parentSessionId !== "null" || query.pinsOnly === "true")
|
|
5944
|
+
)
|
|
5945
|
+
throw new HTTPException(400, {
|
|
5946
|
+
message: 'siteConversations="group" requires a root page (parentSessionId=null)',
|
|
5947
|
+
});
|
|
5948
|
+
// Archived is a flat list: its Site conversations are ordinary rows.
|
|
5949
|
+
if (
|
|
5950
|
+
siteConversations === "group" &&
|
|
5951
|
+
(query.archiveStatus === "archived" || query.archivedOnly === "true")
|
|
5952
|
+
)
|
|
5953
|
+
throw new HTTPException(400, {
|
|
5954
|
+
message: 'siteConversations="group" cannot list archived sessions; use "include"',
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5928
5957
|
// "null" = roots only; a uuid = children of that session; anything else is
|
|
5929
5958
|
// a client error (an unvalidated value would surface as a Postgres uuid cast
|
|
5930
5959
|
// failure -> 500 rather than an honest 400).
|
|
@@ -6092,6 +6121,7 @@ export function sessionListQuery(
|
|
|
6092
6121
|
limit: query.limit,
|
|
6093
6122
|
originSiteId,
|
|
6094
6123
|
siteAccessId,
|
|
6124
|
+
siteConversations,
|
|
6095
6125
|
parentSessionId:
|
|
6096
6126
|
parentSessionId === undefined
|
|
6097
6127
|
? undefined
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
PublishWorkspaceArtifactVersionRequest,
|
|
6
6
|
RollbackWorkspaceArtifactRequest,
|
|
7
7
|
SetWorkspaceArtifactStatusRequest,
|
|
8
|
+
SiteOpenedResponse,
|
|
8
9
|
UpdateWorkspaceArtifactVisibilityRequest,
|
|
9
10
|
UpdateWorkspaceArtifactVisibilityResponse,
|
|
10
11
|
WORKSPACE_ARTIFACT_TITLE_MAX_CHARS,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
resolveSiteAudienceChange,
|
|
25
26
|
SessionAuthorizationDeniedError,
|
|
26
27
|
SessionAuthorizationUnavailableError,
|
|
28
|
+
currentSiteSessionOrigin,
|
|
27
29
|
siteViewerForGrant,
|
|
28
30
|
type ApiRouteDeps,
|
|
29
31
|
} from "@opengeni/core";
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
getWorkspaceArtifact,
|
|
34
36
|
getWorkspaceArtifactContentRef,
|
|
35
37
|
listWorkspaceArtifacts,
|
|
38
|
+
recordSiteOpened,
|
|
36
39
|
nestedPostgresSqlState,
|
|
37
40
|
publishWorkspaceArtifactVersion,
|
|
38
41
|
rollbackWorkspaceArtifact,
|
|
@@ -232,6 +235,33 @@ export function registerWorkspaceArtifactRoutes(app: Hono, deps: ApiRouteDeps):
|
|
|
232
235
|
}
|
|
233
236
|
});
|
|
234
237
|
|
|
238
|
+
// Opening a Site counts as this person's activity on its sidebar row. It is
|
|
239
|
+
// personal, needs only read access, and a Site page cannot record it.
|
|
240
|
+
app.post(`${base}/:artifactId/opened`, async (context) => {
|
|
241
|
+
const workspaceId = context.req.param("workspaceId");
|
|
242
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
|
|
243
|
+
if (currentSiteSessionOrigin())
|
|
244
|
+
throw new HTTPException(403, { message: "A Site page cannot record opening a Site" });
|
|
245
|
+
const siteId = artifactId(context);
|
|
246
|
+
const viewer = await siteViewerForGrant(deps.db, grant);
|
|
247
|
+
if (!viewer.subjectId)
|
|
248
|
+
throw new HTTPException(403, { message: "A signed-in person is required" });
|
|
249
|
+
try {
|
|
250
|
+
return context.json(
|
|
251
|
+
SiteOpenedResponse.parse(
|
|
252
|
+
await recordSiteOpened(deps.db, {
|
|
253
|
+
workspaceId,
|
|
254
|
+
subjectId: viewer.subjectId,
|
|
255
|
+
viewer,
|
|
256
|
+
siteId,
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return workspaceArtifactErrorResponse(context, error);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
235
265
|
app.get(base, async (context) => {
|
|
236
266
|
const workspaceId = context.req.param("workspaceId");
|
|
237
267
|
const grant = await requireAccessGrant(context, deps, workspaceId, "artifacts:read");
|