@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.4
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/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/session-snapshot.ts +260 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -1
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/notifications/__tests__/decision-engine.test.ts +175 -0
- package/src/notifications/decision-engine.ts +43 -8
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-routes.test.ts +19 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-routes.ts +17 -161
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/acp/status.test.ts +276 -21
- package/src/tools/acp/status.ts +98 -32
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -24,7 +24,13 @@ import {
|
|
|
24
24
|
AcpResumeError,
|
|
25
25
|
AcpSessionNotFoundError,
|
|
26
26
|
} from "../../acp/session-manager.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type AcpSessionSnapshot,
|
|
29
|
+
listAcpSessionSnapshots,
|
|
30
|
+
snapshotHistoryRow,
|
|
31
|
+
withCurrentAuthMarkers,
|
|
32
|
+
} from "../../acp/session-snapshot.js";
|
|
33
|
+
import { isLiveAcpStatus } from "../../acp/types.js";
|
|
28
34
|
import {
|
|
29
35
|
AcpSessionModelUpdateEventSchema,
|
|
30
36
|
type AssistantEvent,
|
|
@@ -103,11 +109,14 @@ type SessionEntry = z.infer<typeof sessionEntrySchema>;
|
|
|
103
109
|
* client has no use for it and no way to resolve the other side of the
|
|
104
110
|
* comparison, so serving it would only widen what leaves the daemon.
|
|
105
111
|
*/
|
|
106
|
-
type MergedSession =
|
|
112
|
+
type MergedSession = AcpSessionSnapshot;
|
|
107
113
|
|
|
108
|
-
/** Drop
|
|
114
|
+
/** Drop internal snapshot metadata before the session goes out on the wire. */
|
|
109
115
|
function stripMarkerCredential({
|
|
110
|
-
authErrorCredential:
|
|
116
|
+
authErrorCredential: _credential,
|
|
117
|
+
source: _source,
|
|
118
|
+
resumable: _resumable,
|
|
119
|
+
cwd: _cwd,
|
|
111
120
|
...session
|
|
112
121
|
}: MergedSession): SessionEntry {
|
|
113
122
|
return session;
|
|
@@ -511,7 +520,7 @@ async function listSessions({ queryParams }: RouteHandlerArgs) {
|
|
|
511
520
|
return resolvedByAgent.get(agentId);
|
|
512
521
|
};
|
|
513
522
|
|
|
514
|
-
const { sessions: merged, sawEveryHistoryRow } =
|
|
523
|
+
const { sessions: merged, sawEveryHistoryRow } = listAcpSessionSnapshots({
|
|
515
524
|
limit,
|
|
516
525
|
conversationId,
|
|
517
526
|
});
|
|
@@ -552,43 +561,12 @@ async function listSessions({ queryParams }: RouteHandlerArgs) {
|
|
|
552
561
|
return { sessions: [...page, stripMarkerCredential(marker)] };
|
|
553
562
|
}
|
|
554
563
|
|
|
555
|
-
/**
|
|
556
|
-
* Blank the failure code on any session whose marker no longer describes the
|
|
557
|
-
* credential its agent would resolve.
|
|
558
|
-
*
|
|
559
|
-
* This comparison is what retires a Connect card: not a sweep that has to run
|
|
560
|
-
* at the right moment, but the marker no longer describing the credential in
|
|
561
|
-
* use. Applied after merging rather than inside the query, so live sessions
|
|
562
|
-
* and history rows are judged by exactly the same rule.
|
|
563
|
-
*
|
|
564
|
-
* Resolved per agent, because precedence is per agent: one alias can carry a
|
|
565
|
-
* configured token while another falls through to the vault. Memoised across
|
|
566
|
-
* the request, since a conversation's marked runs are nearly always one agent
|
|
567
|
-
* and each resolution costs a vault read.
|
|
568
|
-
*/
|
|
569
564
|
async function withCurrentMarkersOnly(
|
|
570
565
|
sessions: MergedSession[],
|
|
571
566
|
resolvedFor: (agentId: string) => Promise<string | undefined>,
|
|
572
567
|
): Promise<SessionEntry[]> {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
return sessions.map(strip);
|
|
576
|
-
}
|
|
577
|
-
const judged: SessionEntry[] = [];
|
|
578
|
-
for (const session of sessions) {
|
|
579
|
-
if (session.authErrorCode === undefined) {
|
|
580
|
-
judged.push(strip(session));
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
const current = acpAuthMarkerStillCurrent(
|
|
584
|
-
session.authErrorCredential,
|
|
585
|
-
await resolvedFor(session.agentId),
|
|
586
|
-
);
|
|
587
|
-
judged.push(
|
|
588
|
-
strip(current ? session : { ...session, authErrorCode: undefined }),
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
return judged;
|
|
568
|
+
const judged = await withCurrentAuthMarkers(sessions, resolvedFor);
|
|
569
|
+
return judged.map(stripMarkerCredential);
|
|
592
570
|
}
|
|
593
571
|
|
|
594
572
|
function bulkDeleteSessions({ queryParams }: RouteHandlerArgs) {
|
|
@@ -879,128 +857,6 @@ function parseLimit(raw: string | null | undefined): number {
|
|
|
879
857
|
return Math.min(Math.floor(n), MAX_SESSION_LIMIT);
|
|
880
858
|
}
|
|
881
859
|
|
|
882
|
-
function listMergedSessions(opts: { limit: number; conversationId?: string }): {
|
|
883
|
-
sessions: MergedSession[];
|
|
884
|
-
/**
|
|
885
|
-
* Whether the history query reached the end of this conversation's rows.
|
|
886
|
-
*
|
|
887
|
-
* A short read means there is nothing beyond what was returned, which is the
|
|
888
|
-
* proof that no marker is hiding outside the page.
|
|
889
|
-
*/
|
|
890
|
-
sawEveryHistoryRow: boolean;
|
|
891
|
-
} {
|
|
892
|
-
const manager = getAcpSessionManager();
|
|
893
|
-
const inMemory = manager.getStatus() as AcpSessionState[];
|
|
894
|
-
|
|
895
|
-
const merged = new Map<string, MergedSession>();
|
|
896
|
-
for (const s of inMemory) {
|
|
897
|
-
if (opts.conversationId && s.parentConversationId !== opts.conversationId) {
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
merged.set(s.id, {
|
|
901
|
-
id: s.id,
|
|
902
|
-
agentId: s.agentId,
|
|
903
|
-
acpSessionId: s.acpSessionId,
|
|
904
|
-
parentConversationId: s.parentConversationId,
|
|
905
|
-
status: s.status,
|
|
906
|
-
startedAt: s.startedAt,
|
|
907
|
-
completedAt: s.completedAt ?? null,
|
|
908
|
-
error: s.error ?? null,
|
|
909
|
-
stopReason: s.stopReason ?? null,
|
|
910
|
-
task: s.task,
|
|
911
|
-
parentToolUseId: s.parentToolUseId,
|
|
912
|
-
authErrorCode: s.authErrorCode,
|
|
913
|
-
authErrorCredential: s.authErrorCredential,
|
|
914
|
-
model: s.model,
|
|
915
|
-
availableModels: s.availableModels,
|
|
916
|
-
modelRevisionEpoch: s.modelRevisionEpoch,
|
|
917
|
-
modelRevision: s.modelRevision,
|
|
918
|
-
usedTokens: s.latestUsage?.usedTokens,
|
|
919
|
-
contextSize: s.latestUsage?.contextSize,
|
|
920
|
-
costAmount: s.latestUsage?.costAmount,
|
|
921
|
-
costCurrency: s.latestUsage?.costCurrency,
|
|
922
|
-
inputTokens: s.latestUsage?.inputTokens,
|
|
923
|
-
outputTokens: s.latestUsage?.outputTokens,
|
|
924
|
-
eventLog: manager.getBufferedUpdates(s.id),
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const db = getDb();
|
|
929
|
-
const baseQuery = db.select().from(acpSessionHistory);
|
|
930
|
-
const filtered = opts.conversationId
|
|
931
|
-
? baseQuery.where(
|
|
932
|
-
eq(acpSessionHistory.parentConversationId, opts.conversationId),
|
|
933
|
-
)
|
|
934
|
-
: baseQuery;
|
|
935
|
-
// Fetch only enough rows to fill the requested page after merging with
|
|
936
|
-
// in-memory sessions. In-memory entries take precedence on id collision,
|
|
937
|
-
// so we pad by the count that survived the conversation filter to
|
|
938
|
-
// guarantee we still surface `limit` distinct rows even when every
|
|
939
|
-
// in-memory session shadows a DB row — without over-fetching when many
|
|
940
|
-
// unrelated sessions are in memory.
|
|
941
|
-
const historyLimit = opts.limit + merged.size;
|
|
942
|
-
const historyRows = filtered
|
|
943
|
-
.orderBy(desc(acpSessionHistory.startedAt))
|
|
944
|
-
.limit(historyLimit)
|
|
945
|
-
.all();
|
|
946
|
-
|
|
947
|
-
for (const row of historyRows) {
|
|
948
|
-
if (merged.has(row.id)) {
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
merged.set(row.id, toMergedSession(row));
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
return {
|
|
955
|
-
sessions: Array.from(merged.values()).sort(
|
|
956
|
-
(a, b) => b.startedAt - a.startedAt,
|
|
957
|
-
),
|
|
958
|
-
sawEveryHistoryRow: historyRows.length < historyLimit,
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/** Shape a history row for the response, parsing its stored event log. */
|
|
963
|
-
function toMergedSession(
|
|
964
|
-
row: typeof acpSessionHistory.$inferSelect,
|
|
965
|
-
): MergedSession {
|
|
966
|
-
let eventLog: unknown[] = [];
|
|
967
|
-
try {
|
|
968
|
-
const parsed = JSON.parse(row.eventLogJson) as unknown;
|
|
969
|
-
if (Array.isArray(parsed)) {
|
|
970
|
-
eventLog = parsed;
|
|
971
|
-
}
|
|
972
|
-
} catch (err) {
|
|
973
|
-
log.warn(
|
|
974
|
-
{ id: row.id, err },
|
|
975
|
-
"Failed to parse event_log_json for ACP session history row",
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
// Rows predating the usage migration carry NULLs for these columns and
|
|
979
|
-
// degrade to undefined.
|
|
980
|
-
return {
|
|
981
|
-
id: row.id,
|
|
982
|
-
agentId: row.agentId,
|
|
983
|
-
acpSessionId: row.acpSessionId,
|
|
984
|
-
parentConversationId: row.parentConversationId,
|
|
985
|
-
status: row.status,
|
|
986
|
-
startedAt: row.startedAt,
|
|
987
|
-
completedAt: row.completedAt,
|
|
988
|
-
error: row.error,
|
|
989
|
-
stopReason: row.stopReason,
|
|
990
|
-
task: row.task ?? undefined,
|
|
991
|
-
parentToolUseId: row.parentToolUseId ?? undefined,
|
|
992
|
-
authErrorCode: row.authErrorCode ?? undefined,
|
|
993
|
-
authErrorCredential: row.authErrorCredential ?? undefined,
|
|
994
|
-
usedTokens: row.usedTokens ?? undefined,
|
|
995
|
-
contextSize: row.contextSize ?? undefined,
|
|
996
|
-
costAmount: row.costAmount ?? undefined,
|
|
997
|
-
costCurrency: row.costCurrency ?? undefined,
|
|
998
|
-
inputTokens: row.inputTokens ?? undefined,
|
|
999
|
-
outputTokens: row.outputTokens ?? undefined,
|
|
1000
|
-
eventLog,
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
860
|
/**
|
|
1005
861
|
* Reach past the page for the one marked run a client would restore the card
|
|
1006
862
|
* from, when the page itself holds none.
|
|
@@ -1061,7 +917,7 @@ async function findRecoveryMarker(
|
|
|
1061
917
|
.from(acpSessionHistory)
|
|
1062
918
|
.where(eq(acpSessionHistory.id, marker.id))
|
|
1063
919
|
.get();
|
|
1064
|
-
return row ?
|
|
920
|
+
return row ? snapshotHistoryRow(row) : undefined;
|
|
1065
921
|
}
|
|
1066
922
|
}
|
|
1067
923
|
return undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveTurnClientOs } from "../../daemon/conversation-client-surface.js";
|
|
2
|
+
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
3
|
+
import type { ToolContext } from "../../tools/types.js";
|
|
4
|
+
import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
|
|
5
|
+
|
|
6
|
+
export function browserCliConversationKey(sessionId: string): string {
|
|
7
|
+
return `browser-cli:${sessionId}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function resolveBrowserContext(
|
|
11
|
+
conversationId: string | undefined,
|
|
12
|
+
sessionId: string,
|
|
13
|
+
headers: Record<string, string>,
|
|
14
|
+
abortSignal?: AbortSignal,
|
|
15
|
+
): Promise<ToolContext> {
|
|
16
|
+
const conversation = conversationId
|
|
17
|
+
? findConversation(conversationId)
|
|
18
|
+
: undefined;
|
|
19
|
+
const actor = await resolveActorPrincipalIdForLocalGuardian(
|
|
20
|
+
conversation?.getTurnActorPrincipalId() ??
|
|
21
|
+
headers["x-vellum-actor-principal-id"]?.trim(),
|
|
22
|
+
);
|
|
23
|
+
const signals = [abortSignal, conversation?.abortController?.signal].filter(
|
|
24
|
+
(signal): signal is AbortSignal => signal !== undefined,
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
workingDir: process.cwd(),
|
|
28
|
+
conversationId: conversation
|
|
29
|
+
? conversationId!
|
|
30
|
+
: browserCliConversationKey(sessionId),
|
|
31
|
+
trustClass: conversation?.trustContext?.trustClass ?? "unknown",
|
|
32
|
+
transportInterface: conversation?.transportInterface,
|
|
33
|
+
clientOs: conversation
|
|
34
|
+
? resolveTurnClientOs(conversation).clientOs
|
|
35
|
+
: undefined,
|
|
36
|
+
sourceActorPrincipalId: actor,
|
|
37
|
+
signal: signals.length ? AbortSignal.any(signals) : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
BROWSER_OPERATIONS,
|
|
17
17
|
type BrowserOperation,
|
|
18
18
|
} from "../../browser/types.js";
|
|
19
|
-
import {
|
|
19
|
+
import { shouldUseVirtualDesktopBrowser } from "../../browser/virtual-desktop-target.js";
|
|
20
|
+
import { executeDesktopBrowserOperation } from "../../desktop/desktop-browser-operations.js";
|
|
20
21
|
import type { ContentBlock } from "../../providers/types.js";
|
|
21
22
|
import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
|
|
22
|
-
import {
|
|
23
|
+
import { resolveBrowserContext } from "./browser-context.js";
|
|
24
|
+
export { browserCliConversationKey } from "./browser-context.js";
|
|
23
25
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
24
26
|
|
|
25
27
|
// ── Param validation ─────────────────────────────────────────────────
|
|
@@ -29,18 +31,9 @@ const BrowserExecuteParams = z.object({
|
|
|
29
31
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
30
32
|
sessionId: z.string().min(1).default("default"),
|
|
31
33
|
conversationId: z.string().min(1).optional(),
|
|
34
|
+
desktop: z.boolean().optional(),
|
|
32
35
|
});
|
|
33
36
|
|
|
34
|
-
// ── Conversation key ─────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Build a deterministic conversation key from a session ID.
|
|
38
|
-
* All CLI browser calls with the same session share browser state.
|
|
39
|
-
*/
|
|
40
|
-
export function browserCliConversationKey(sessionId: string): string {
|
|
41
|
-
return `browser-cli:${sessionId}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
37
|
// ── Screenshot extraction ────────────────────────────────────────────
|
|
45
38
|
|
|
46
39
|
/**
|
|
@@ -70,45 +63,21 @@ function extractScreenshots(
|
|
|
70
63
|
async function handleBrowserExecute({
|
|
71
64
|
body = {},
|
|
72
65
|
headers = {},
|
|
66
|
+
abortSignal,
|
|
73
67
|
}: RouteHandlerArgs) {
|
|
74
|
-
const { operation, input, sessionId, conversationId } =
|
|
68
|
+
const { operation, input, sessionId, conversationId, desktop } =
|
|
75
69
|
BrowserExecuteParams.parse(body);
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
: undefined;
|
|
83
|
-
|
|
84
|
-
const resolvedConversationId = conversation
|
|
85
|
-
? conversationId!
|
|
86
|
-
: browserCliConversationKey(sessionId);
|
|
87
|
-
|
|
88
|
-
// Actor principal lets host-browser routing enforce same-actor ownership.
|
|
89
|
-
// A live conversation's turn actor owns the request (e.g. a nested-bash
|
|
90
|
-
// browser call inherits the conversation's actor), so it wins over the
|
|
91
|
-
// request header — which over IPC may carry a synthetic local-guardian id
|
|
92
|
-
// injected for header-less local callers. Resolve through the local-guardian
|
|
93
|
-
// translation so the value matches the actorPrincipalId host_browser clients
|
|
94
|
-
// register with (dev-bypass otherwise mismatches).
|
|
95
|
-
const headerActor =
|
|
96
|
-
headers["x-vellum-actor-principal-id"]?.trim() || undefined;
|
|
97
|
-
const sourceActorPrincipalId = await resolveActorPrincipalIdForLocalGuardian(
|
|
98
|
-
conversation?.getTurnActorPrincipalId() ?? headerActor,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const result = await executeBrowserOperation(
|
|
102
|
-
operation as BrowserOperation,
|
|
103
|
-
input,
|
|
104
|
-
{
|
|
105
|
-
workingDir: process.cwd(),
|
|
106
|
-
conversationId: resolvedConversationId,
|
|
107
|
-
trustClass: conversation?.trustContext?.trustClass ?? "unknown",
|
|
108
|
-
transportInterface: conversation?.transportInterface,
|
|
109
|
-
sourceActorPrincipalId,
|
|
110
|
-
},
|
|
71
|
+
const context = await resolveBrowserContext(
|
|
72
|
+
conversationId,
|
|
73
|
+
sessionId,
|
|
74
|
+
headers,
|
|
75
|
+
abortSignal,
|
|
111
76
|
);
|
|
77
|
+
const execute = shouldUseVirtualDesktopBrowser(desktop, input, context)
|
|
78
|
+
? executeDesktopBrowserOperation
|
|
79
|
+
: executeBrowserOperation;
|
|
80
|
+
const result = await execute(operation as BrowserOperation, input, context);
|
|
112
81
|
|
|
113
82
|
const screenshots = extractScreenshots(result.contentBlocks);
|
|
114
83
|
|
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
* Routes for browser tab management commands.
|
|
3
3
|
*
|
|
4
4
|
* Exposes `browser_tabs` so CLI commands can list, create, select, and
|
|
5
|
-
* close browser tabs
|
|
5
|
+
* close browser tabs in the Chrome extension or streamed desktop browser.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { shouldUseVirtualDesktopBrowser } from "../../browser/virtual-desktop-target.js";
|
|
11
11
|
import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
|
|
12
|
+
import { executeDesktopBrowserTabs } from "../../desktop/desktop-browser-operations.js";
|
|
13
|
+
import { normalizeBrowserMode } from "../../tools/browser/browser-mode.js";
|
|
12
14
|
import { getCdpClient } from "../../tools/browser/cdp-client/factory.js";
|
|
13
15
|
import {
|
|
14
16
|
clearPinnedTab,
|
|
15
17
|
clearPinnedTabByTabId,
|
|
16
18
|
setPinnedTab,
|
|
17
19
|
} from "../../tools/browser/pinned-tabs.js";
|
|
18
|
-
import type { ToolContext } from "../../tools/types.js";
|
|
19
20
|
import { LOCAL_PRINCIPALS } from "../auth/route-policy.js";
|
|
20
|
-
import {
|
|
21
|
+
import { resolveBrowserContext } from "./browser-context.js";
|
|
21
22
|
import { BadRequestError } from "./errors.js";
|
|
22
23
|
import { parseBody } from "./parse-body.js";
|
|
23
24
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
@@ -26,6 +27,8 @@ const BrowserTabsParams = z.object({
|
|
|
26
27
|
command: z.enum(["list", "select", "new", "close"]),
|
|
27
28
|
sessionId: z.string().min(1).default("default"),
|
|
28
29
|
conversationId: z.string().min(1).optional(),
|
|
30
|
+
desktop: z.boolean().optional(),
|
|
31
|
+
browserMode: z.string().optional(),
|
|
29
32
|
tabId: z.number().optional(),
|
|
30
33
|
url: z.string().optional(),
|
|
31
34
|
// Route tab operations to a specific extension client in multi-client
|
|
@@ -33,33 +36,73 @@ const BrowserTabsParams = z.object({
|
|
|
33
36
|
targetClientId: z.string().min(1).optional(),
|
|
34
37
|
});
|
|
35
38
|
|
|
36
|
-
async function handleBrowserTabs({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
transportInterface: conversation?.transportInterface,
|
|
52
|
-
} as unknown as ToolContext;
|
|
39
|
+
async function handleBrowserTabs({
|
|
40
|
+
body = {},
|
|
41
|
+
headers = {},
|
|
42
|
+
abortSignal,
|
|
43
|
+
}: RouteHandlerArgs) {
|
|
44
|
+
const {
|
|
45
|
+
command,
|
|
46
|
+
sessionId,
|
|
47
|
+
conversationId,
|
|
48
|
+
tabId,
|
|
49
|
+
url,
|
|
50
|
+
targetClientId,
|
|
51
|
+
desktop,
|
|
52
|
+
browserMode,
|
|
53
|
+
} = parseBody(BrowserTabsParams, body);
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
+
const context = await resolveBrowserContext(
|
|
56
|
+
conversationId,
|
|
57
|
+
sessionId,
|
|
58
|
+
headers,
|
|
59
|
+
abortSignal,
|
|
60
|
+
);
|
|
61
|
+
const resolvedConversationId = context.conversationId;
|
|
62
|
+
const mode = normalizeBrowserMode(browserMode);
|
|
63
|
+
if ("error" in mode) {
|
|
64
|
+
throw new BadRequestError(mode.error);
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
shouldUseVirtualDesktopBrowser(
|
|
68
|
+
desktop,
|
|
69
|
+
{ browser_mode: browserMode, target_client_id: targetClientId },
|
|
70
|
+
context,
|
|
71
|
+
)
|
|
72
|
+
) {
|
|
73
|
+
if (
|
|
74
|
+
targetClientId ||
|
|
75
|
+
(mode.mode !== "auto" && mode.mode !== "cdp-inspect")
|
|
76
|
+
) {
|
|
77
|
+
throw new BadRequestError(
|
|
78
|
+
"--virtual-desktop cannot target a personal browser client",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const result = await executeDesktopBrowserTabs(
|
|
82
|
+
{ command, tabId, url },
|
|
83
|
+
context,
|
|
84
|
+
);
|
|
85
|
+
if (result.isError) {
|
|
86
|
+
throw new BadRequestError(result.content);
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(result.content);
|
|
89
|
+
}
|
|
55
90
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
const cdpOptions = {
|
|
92
|
+
mode:
|
|
93
|
+
targetClientId || mode.mode === "auto"
|
|
94
|
+
? ("extension" as const)
|
|
95
|
+
: mode.mode,
|
|
61
96
|
targetClientId,
|
|
62
|
-
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Absorb brief reconnects before dispatching to the extension.
|
|
100
|
+
if (cdpOptions.mode === "extension") {
|
|
101
|
+
await HostBrowserProxy.instance.waitForExtensionClient(
|
|
102
|
+
context.sourceActorPrincipalId,
|
|
103
|
+
targetClientId,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
63
106
|
|
|
64
107
|
if (command === "list") {
|
|
65
108
|
const cdp = getCdpClient(context, cdpOptions);
|
|
@@ -185,7 +228,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
185
228
|
handler: handleBrowserTabs,
|
|
186
229
|
summary: "Manage browser tabs",
|
|
187
230
|
description:
|
|
188
|
-
"List, create, select, or close browser tabs
|
|
231
|
+
"List, create, select, or close browser tabs in the Chrome extension or streamed desktop browser.",
|
|
189
232
|
tags: ["browser"],
|
|
190
233
|
requestBody: BrowserTabsParams,
|
|
191
234
|
responseBody: z.object({
|
|
@@ -14,6 +14,7 @@ import { ROUTES } from "./desktop-setup-routes.js";
|
|
|
14
14
|
import { NotFoundError } from "./errors.js";
|
|
15
15
|
|
|
16
16
|
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
17
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
17
18
|
const status = spyOn(desktopDependencyInstaller, "getStatus");
|
|
18
19
|
const start = spyOn(desktopDependencyInstaller, "start");
|
|
19
20
|
|
|
@@ -24,6 +25,7 @@ afterAll(() => {
|
|
|
24
25
|
|
|
25
26
|
beforeEach(() => {
|
|
26
27
|
process.env.IS_CONTAINERIZED = "true";
|
|
28
|
+
process.env.IS_PLATFORM = "true";
|
|
27
29
|
status.mockReturnValue({ state: "required" });
|
|
28
30
|
start.mockReturnValue({ state: "installing", stage: "packages" });
|
|
29
31
|
});
|
|
@@ -34,6 +36,11 @@ afterEach(() => {
|
|
|
34
36
|
} else {
|
|
35
37
|
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
36
38
|
}
|
|
39
|
+
if (originalPlatform === undefined) {
|
|
40
|
+
delete process.env.IS_PLATFORM;
|
|
41
|
+
} else {
|
|
42
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
43
|
+
}
|
|
37
44
|
setOverridesForTesting({});
|
|
38
45
|
status.mockClear();
|
|
39
46
|
start.mockClear();
|
|
@@ -60,7 +67,15 @@ describe("desktop setup route feature gate", () => {
|
|
|
60
67
|
expect(start).not.toHaveBeenCalled();
|
|
61
68
|
});
|
|
62
69
|
|
|
63
|
-
test(`${route.method}
|
|
70
|
+
test(`${route.method} rejects self-hosted Docker assistants without inspecting or installing dependencies`, () => {
|
|
71
|
+
process.env.IS_PLATFORM = "false";
|
|
72
|
+
setOverridesForTesting({ "assistant-desktop": true });
|
|
73
|
+
expect(() => route.handler({})).toThrow(NotFoundError);
|
|
74
|
+
expect(status).not.toHaveBeenCalled();
|
|
75
|
+
expect(start).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test(`${route.method} serves setup only for enabled platform containers`, () => {
|
|
64
79
|
setOverridesForTesting({ "assistant-desktop": true });
|
|
65
80
|
expect(route.handler({})).toEqual(
|
|
66
81
|
route.method === "GET"
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
import { getConfig } from "../../config/loader.js";
|
|
4
4
|
import { desktopDependencyInstaller } from "../../desktop/desktop-dependencies.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isVirtualDesktopEnabled } from "../../desktop/virtual-desktop-feature.js";
|
|
6
6
|
import { GATEWAY_PRINCIPALS } from "../auth/route-policy.js";
|
|
7
7
|
import { NotFoundError } from "./errors.js";
|
|
8
8
|
import type { RouteDefinition } from "./types.js";
|
|
@@ -19,8 +19,10 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
|
|
|
19
19
|
method,
|
|
20
20
|
policy: { requiredScopes: [], allowedPrincipalTypes: GATEWAY_PRINCIPALS },
|
|
21
21
|
handler: () => {
|
|
22
|
-
if (!
|
|
23
|
-
throw new NotFoundError(
|
|
22
|
+
if (!isVirtualDesktopEnabled(getConfig())) {
|
|
23
|
+
throw new NotFoundError(
|
|
24
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
25
|
+
);
|
|
24
26
|
}
|
|
25
27
|
return method === "GET"
|
|
26
28
|
? desktopDependencyInstaller.getStatus()
|
|
@@ -28,8 +30,8 @@ export const ROUTES: RouteDefinition[] = ["GET", "POST"].map((method) => ({
|
|
|
28
30
|
},
|
|
29
31
|
summary:
|
|
30
32
|
method === "GET"
|
|
31
|
-
? "Get desktop setup status"
|
|
32
|
-
: "Install desktop components",
|
|
33
|
+
? "Get virtual desktop setup status"
|
|
34
|
+
: "Install virtual desktop components",
|
|
33
35
|
tags: ["desktop"],
|
|
34
36
|
responseBody: statusSchema,
|
|
35
37
|
}));
|