@opengeni/api-router 4.3.3-canary.39786771332001 → 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-S5VJ3GVR.js → chunk-LXUAR3HS.js} +122 -69
- package/dist/{chunk-S5VJ3GVR.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/mcp/user-setup-card.d.ts +21 -0
- 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 +37 -47
- package/src/mcp/user-setup-card.ts +42 -0
- 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
- package/src/sandbox-file-artifacts.ts +3 -1
- package/dist/mcp/machine-permission-card.d.ts +0 -33
- package/src/mcp/machine-permission-card.ts +0 -78
|
@@ -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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type UserSetupBlocker } from "@opengeni/contracts";
|
|
2
|
+
export { sessionChatUrl } from "@opengeni/contracts";
|
|
3
|
+
type ToolRefusal = {
|
|
4
|
+
code?: string | undefined;
|
|
5
|
+
reason?: string | undefined;
|
|
6
|
+
};
|
|
7
|
+
export type UserSetupCardPost = (blocker: UserSetupBlocker) => Promise<{
|
|
8
|
+
eventId: string | null;
|
|
9
|
+
chatUrl: string | null;
|
|
10
|
+
} | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Post the setup card when a tool was refused for something the person can fix
|
|
13
|
+
* themselves (USER_SETUP_BLOCKERS), and tell the agent it is posted. Every
|
|
14
|
+
* other result is returned unchanged. Posting is best effort: if it cannot be
|
|
15
|
+
* posted, the original refusal still stands.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withUserSetupCard<T extends ToolRefusal>(result: T, post: UserSetupCardPost): Promise<T & {
|
|
18
|
+
card?: "shown";
|
|
19
|
+
cardEventId?: string | null;
|
|
20
|
+
chatUrl?: string;
|
|
21
|
+
}>;
|
|
@@ -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
|
@@ -18,11 +18,7 @@ import { enableMachineScreenControl, enrollmentAccess } from "../sandbox/screen-
|
|
|
18
18
|
import { registerNotificationTools } from "./notification-tools";
|
|
19
19
|
import { excelAddinSetup, registerExcelTools } from "./excel-tools";
|
|
20
20
|
import { capabilityAccountReadiness } from "./capability-account-readiness";
|
|
21
|
-
import {
|
|
22
|
-
sessionChatUrl,
|
|
23
|
-
withMachinePermissionCard,
|
|
24
|
-
type MachinePermissionCardPost,
|
|
25
|
-
} from "./machine-permission-card";
|
|
21
|
+
import { sessionChatUrl, withUserSetupCard, type UserSetupCardPost } from "./user-setup-card";
|
|
26
22
|
import {
|
|
27
23
|
prepareWorkspaceArtifactUpload,
|
|
28
24
|
prepareWorkspaceArtifactPublication,
|
|
@@ -214,8 +210,9 @@ import {
|
|
|
214
210
|
buildCapabilityCatalog,
|
|
215
211
|
CONNECTED_MACHINE_CAPABILITY_ID,
|
|
216
212
|
EXCEL_ADDIN_CAPABILITY_ID,
|
|
213
|
+
browserExtension,
|
|
217
214
|
nativeConnectionCapabilityRecommendations,
|
|
218
|
-
|
|
215
|
+
userSetupCardPayload,
|
|
219
216
|
requireLiveAgentAttemptAuthorization,
|
|
220
217
|
requireSessionAuthorization,
|
|
221
218
|
requireSessionAuthorizationListScope,
|
|
@@ -2300,7 +2297,7 @@ function registerSandboxFileArtifactTool(
|
|
|
2300
2297
|
"sandbox_file_publish",
|
|
2301
2298
|
{
|
|
2302
2299
|
description:
|
|
2303
|
-
"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.",
|
|
2304
2301
|
inputSchema: {
|
|
2305
2302
|
path: z4.string().min(1).max(4_096).optional(),
|
|
2306
2303
|
artifactId: z4.string().uuid().optional(),
|
|
@@ -4323,23 +4320,23 @@ function registerFleetTools(
|
|
|
4323
4320
|
};
|
|
4324
4321
|
};
|
|
4325
4322
|
|
|
4326
|
-
// A refusal
|
|
4327
|
-
//
|
|
4328
|
-
//
|
|
4329
|
-
//
|
|
4330
|
-
//
|
|
4331
|
-
//
|
|
4332
|
-
//
|
|
4333
|
-
//
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4323
|
+
// A refusal the person can fix themselves (USER_SETUP_BLOCKERS, for example
|
|
4324
|
+
// the owner not having allowed this chat to use their machine yet) posts the
|
|
4325
|
+
// setup card right away, in the agent's own turn, so the person sees the fix
|
|
4326
|
+
// without the agent having to find and call the card tool. Only exact agent
|
|
4327
|
+
// attempts post it. A machine card names the machine the agent asked for, so
|
|
4328
|
+
// it can lead with allowing that machine. A repeated refusal for the same
|
|
4329
|
+
// machine in the same turn reuses that turn's card; the lookup and append run
|
|
4330
|
+
// atomically under the attempt fence so parallel refusals cannot stack
|
|
4331
|
+
// copies. Cards for every other capability are never limited.
|
|
4332
|
+
const userSetupCard =
|
|
4333
|
+
(toolName: string, target: string): UserSetupCardPost =>
|
|
4334
|
+
async (blocker) => {
|
|
4337
4335
|
const requestedSandboxId = UUID_TARGET.test(target) ? target.toLowerCase() : null;
|
|
4338
4336
|
if (!exactAgentAttemptClaims(grant)) return null;
|
|
4339
4337
|
const claims = exactAgentCommandContext(grant, sessionId);
|
|
4340
|
-
const
|
|
4341
|
-
|
|
4342
|
-
)!;
|
|
4338
|
+
const payload = userSetupCardPayload(blocker, { toolName, requestedSandboxId });
|
|
4339
|
+
if (!payload) return null;
|
|
4343
4340
|
const appended = await appendTurnCapabilityCardOnce(
|
|
4344
4341
|
deps.db,
|
|
4345
4342
|
grant.workspaceId,
|
|
@@ -4347,23 +4344,13 @@ function registerFleetTools(
|
|
|
4347
4344
|
claims.callerTurnId,
|
|
4348
4345
|
claims.callerExecutionGeneration,
|
|
4349
4346
|
claims.callerAttemptId,
|
|
4350
|
-
|
|
4351
|
-
{
|
|
4352
|
-
type: "tool.auth_needed",
|
|
4353
|
-
payload: {
|
|
4354
|
-
...capabilityCardPayload(item, {
|
|
4355
|
-
action: "connect",
|
|
4356
|
-
rationale: "Allow this chat to use your computer.",
|
|
4357
|
-
toolName,
|
|
4358
|
-
}),
|
|
4359
|
-
...(requestedSandboxId ? { requestedSandboxId } : {}),
|
|
4360
|
-
},
|
|
4361
|
-
},
|
|
4347
|
+
blocker.capabilityId,
|
|
4348
|
+
{ type: "tool.auth_needed", payload },
|
|
4362
4349
|
requestedSandboxId,
|
|
4363
4350
|
);
|
|
4364
4351
|
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, appended.events);
|
|
4365
4352
|
if (!appended.accepted) {
|
|
4366
|
-
throw new Error("The calling turn was replaced before the
|
|
4353
|
+
throw new Error("The calling turn was replaced before the setup card committed.");
|
|
4367
4354
|
}
|
|
4368
4355
|
return {
|
|
4369
4356
|
eventId: appended.existingEventId ?? appended.events[0]?.id ?? null,
|
|
@@ -4398,9 +4385,9 @@ function registerFleetTools(
|
|
|
4398
4385
|
},
|
|
4399
4386
|
async ({ target }) =>
|
|
4400
4387
|
json(
|
|
4401
|
-
await
|
|
4388
|
+
await withUserSetupCard(
|
|
4402
4389
|
await swapActiveSandbox(services, await fleetContext(), target),
|
|
4403
|
-
|
|
4390
|
+
userSetupCard("sandbox_attach", target),
|
|
4404
4391
|
),
|
|
4405
4392
|
),
|
|
4406
4393
|
);
|
|
@@ -4414,9 +4401,9 @@ function registerFleetTools(
|
|
|
4414
4401
|
},
|
|
4415
4402
|
async ({ target }) =>
|
|
4416
4403
|
json(
|
|
4417
|
-
await
|
|
4404
|
+
await withUserSetupCard(
|
|
4418
4405
|
await swapActiveSandbox(services, await fleetContext(), target),
|
|
4419
|
-
|
|
4406
|
+
userSetupCard("sandbox_swap", target),
|
|
4420
4407
|
),
|
|
4421
4408
|
),
|
|
4422
4409
|
);
|
|
@@ -4458,12 +4445,12 @@ function registerFleetTools(
|
|
|
4458
4445
|
)
|
|
4459
4446
|
: null;
|
|
4460
4447
|
return json(
|
|
4461
|
-
await
|
|
4448
|
+
await withUserSetupCard(
|
|
4462
4449
|
await runOnSandbox(services, await fleetContext(), target, typedOp, {
|
|
4463
4450
|
...(transientExecEnvironment ? { transientExecEnvironment } : {}),
|
|
4464
4451
|
...(secretResolver ? { secretResolver } : {}),
|
|
4465
4452
|
}),
|
|
4466
|
-
|
|
4453
|
+
userSetupCard("run_on", target),
|
|
4467
4454
|
),
|
|
4468
4455
|
);
|
|
4469
4456
|
},
|
|
@@ -4484,9 +4471,10 @@ function registerFleetTools(
|
|
|
4484
4471
|
// Same live-attempt authority the other fleet tools resolve, without
|
|
4485
4472
|
// requiring a session box: a chat with no sandbox can still connect one.
|
|
4486
4473
|
await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
|
|
4487
|
-
const item = nativeConnectionCapabilityRecommendations({
|
|
4488
|
-
|
|
4489
|
-
|
|
4474
|
+
const item = nativeConnectionCapabilityRecommendations({
|
|
4475
|
+
connectedMachines: true,
|
|
4476
|
+
browserExtension: browserExtension(deps.settings),
|
|
4477
|
+
}).find((candidate) => candidate.id === CONNECTED_MACHINE_CAPABILITY_ID)!;
|
|
4490
4478
|
const setup = await capabilitySetupProjection(
|
|
4491
4479
|
deps,
|
|
4492
4480
|
grant.workspaceId,
|
|
@@ -4514,7 +4502,7 @@ function registerFleetTools(
|
|
|
4514
4502
|
setup.status === "ready"
|
|
4515
4503
|
? `${setup.detail} The Connected Machine card lists them, so the person can choose one for this chat or connect another.`
|
|
4516
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."
|
|
4517
|
-
} 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.`,
|
|
4518
4506
|
});
|
|
4519
4507
|
}
|
|
4520
4508
|
return json(
|
|
@@ -4544,7 +4532,7 @@ function registerConnectedMachineTools(
|
|
|
4544
4532
|
{
|
|
4545
4533
|
description:
|
|
4546
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 (" +
|
|
4547
|
-
|
|
4535
|
+
browserExtension(deps.settings).installUrl +
|
|
4548
4536
|
") after the machine is connected.",
|
|
4549
4537
|
inputSchema: { allowScreenControl: z4.boolean().optional() },
|
|
4550
4538
|
},
|
|
@@ -4576,7 +4564,7 @@ function registerConnectedMachineTools(
|
|
|
4576
4564
|
{
|
|
4577
4565
|
description:
|
|
4578
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 (" +
|
|
4579
|
-
|
|
4567
|
+
browserExtension(deps.settings).installUrl +
|
|
4580
4568
|
") on that machine.",
|
|
4581
4569
|
inputSchema: { target: z4.string().min(1) },
|
|
4582
4570
|
},
|
|
@@ -6733,6 +6721,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
6733
6721
|
...current.items,
|
|
6734
6722
|
...nativeConnectionCapabilityRecommendations({
|
|
6735
6723
|
connectedMachines: deps.settings.sandboxSelfhostedEnabled,
|
|
6724
|
+
browserExtension: browserExtension(deps.settings),
|
|
6736
6725
|
}),
|
|
6737
6726
|
];
|
|
6738
6727
|
const ranked = searchCapabilityCatalogItems(candidates, query, limit ?? 8);
|
|
@@ -6806,6 +6795,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
6806
6795
|
...current.items,
|
|
6807
6796
|
...nativeConnectionCapabilityRecommendations({
|
|
6808
6797
|
connectedMachines: deps.settings.sandboxSelfhostedEnabled,
|
|
6798
|
+
browserExtension: browserExtension(deps.settings),
|
|
6809
6799
|
}),
|
|
6810
6800
|
].find((candidate) => candidate.id === capabilityId);
|
|
6811
6801
|
if (!item || !capabilityCatalogItemIsTrustedForExposure(item)) {
|
|
@@ -6845,7 +6835,7 @@ function registerCapabilityDiscoveryTools(
|
|
|
6845
6835
|
capabilityId: item.id,
|
|
6846
6836
|
status: "ready",
|
|
6847
6837
|
eventId,
|
|
6848
|
-
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.`,
|
|
6849
6839
|
});
|
|
6850
6840
|
}
|
|
6851
6841
|
if (githubCardWhenReady) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type UserSetupBlocker,
|
|
3
|
+
userSetupBlocker,
|
|
4
|
+
userSetupCardShownMessage,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
6
|
+
|
|
7
|
+
export { sessionChatUrl } from "@opengeni/contracts";
|
|
8
|
+
|
|
9
|
+
type ToolRefusal = { code?: string | undefined; reason?: string | undefined };
|
|
10
|
+
|
|
11
|
+
export type UserSetupCardPost = (blocker: UserSetupBlocker) => Promise<{
|
|
12
|
+
eventId: string | null;
|
|
13
|
+
chatUrl: string | null;
|
|
14
|
+
} | null>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Post the setup card when a tool was refused for something the person can fix
|
|
18
|
+
* themselves (USER_SETUP_BLOCKERS), and tell the agent it is posted. Every
|
|
19
|
+
* other result is returned unchanged. Posting is best effort: if it cannot be
|
|
20
|
+
* posted, the original refusal still stands.
|
|
21
|
+
*/
|
|
22
|
+
export async function withUserSetupCard<T extends ToolRefusal>(
|
|
23
|
+
result: T,
|
|
24
|
+
post: UserSetupCardPost,
|
|
25
|
+
): Promise<T & { card?: "shown"; cardEventId?: string | null; chatUrl?: string }> {
|
|
26
|
+
const blocker = userSetupBlocker(result.code);
|
|
27
|
+
if (!blocker) return result;
|
|
28
|
+
let posted: Awaited<ReturnType<UserSetupCardPost>>;
|
|
29
|
+
try {
|
|
30
|
+
posted = await post(blocker);
|
|
31
|
+
} catch {
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
if (!posted) return result;
|
|
35
|
+
return {
|
|
36
|
+
...result,
|
|
37
|
+
reason: userSetupCardShownMessage(blocker, posted.chatUrl),
|
|
38
|
+
card: "shown",
|
|
39
|
+
cardEventId: posted.eventId,
|
|
40
|
+
...(posted.chatUrl ? { chatUrl: posted.chatUrl } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -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
|