Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-browse-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roomote/web': minor
---

Add a `browse` tool to Fast Sessions that gives each conversation a private cloud browser without a sandbox. Configure `R_FAST_BROWSER_PROVIDER=browseruse` and `R_BROWSER_USE_API_KEY` to enable it. Screenshots and recordings are saved as Session artifacts, shown in the web transcript, and attached to chat replies when the agent asks for delivery.
7 changes: 7 additions & 0 deletions .docker/app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,16 @@
ENV S3_ENDPOINT=http://minio:9000
ENV S3_PRESIGN_ENDPOINT=http://minio:9000
ENV S3_REGION=us-east-1
ENV S3_ACCESS_KEY_ID=roomote

Check warning on line 118 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "S3_ACCESS_KEY_ID") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV S3_SECRET_ACCESS_KEY=roomote-local-artifacts-password

Check warning on line 119 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "S3_SECRET_ACCESS_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV S3_BUCKET_ARTIFACTS=roomote-artifacts
ENV JOB_AUTH_PRIVATE_KEY=local-self-host-build-job-auth-private-key

Check warning on line 121 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "JOB_AUTH_PRIVATE_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV JOB_AUTH_PUBLIC_KEY=local-self-host-build-job-auth-public-key
ENV PREVIEW_AUTH_PRIVATE_KEY=local-self-host-build-preview-auth-private-key

Check warning on line 123 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "PREVIEW_AUTH_PRIVATE_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV PREVIEW_AUTH_PUBLIC_KEY=local-self-host-build-preview-auth-public-key
ENV DASHBOARD_PASSWORD=roomote-local-admin

Check warning on line 125 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DASHBOARD_PASSWORD") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV ENCRYPTION_KEY=local-roomote-encryption-key-0001

Check warning on line 126 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "ENCRYPTION_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV ARTIFACT_SIGNING_KEY=local-roomote-artifact-signing-key-1

Check warning on line 127 in .docker/app/Dockerfile

View workflow job for this annotation

GitHub Actions / Docker Build (app, amd64)

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "ARTIFACT_SIGNING_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ENV PREVIEW_PROXY_BASE_URL=http://localhost:18081
ENV PREVIEW_DOMAINS=localhost,127.0.0.1,roomotepreview.localhost

Expand Down Expand Up @@ -323,6 +323,13 @@
apt -qq install -y git ffmpeg util-linux && \
rm -rf /var/lib/apt/lists/*

# The Fast `browse` tool drives a cloud browser through the agent-browser CLI
# from the api process. Only the CLI ships here; no Chrome is installed, so the
# tool stays off until R_FAST_BROWSER_PROVIDER names a provider.
ARG AGENT_BROWSER_VERSION=0.37.1
RUN npm install -g "agent-browser@${AGENT_BROWSER_VERSION}" && \
npm cache clean --force

# The install and version check run as root; with the image's HOME=/tmp they
# would bake root-owned dotdirs (npm cache, OpenCode's data/config/cache
# dirs) into the layer. The runtime user shares that HOME and must be able to
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/handlers/discord/fast-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@roomote/communication';
import {
admitFastAgentHumanFollowUp,
buildFastAgentMediaArtifactCreator,
createFastAgentConversationArtifact,
persistFastAgentInlineHumanTurn,
recordFastAgentConversationMessageBestEffort,
Expand Down Expand Up @@ -395,6 +396,7 @@ export async function processDiscordFastAgentMessage(
fastConversationId: session.id,
...artifact,
}),
createMediaArtifact: buildFastAgentMediaArtifactCreator(session.id),
...(durableTurnForResume
? {
requestDurableResume: () =>
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/handlers/slack/events/fast-agent-reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@roomote/communication';
import {
buildFastAgentArtifactCreator,
buildFastAgentMediaArtifactCreator,
findFastAgentSessionForProviderMessage,
persistFastAgentInlineHumanTurn,
recordFastAgentConversationMessageBestEffort,
Expand Down Expand Up @@ -164,6 +165,7 @@ async function processFastAgentReaction(params: {
}
: {}),
createArtifact: buildFastAgentArtifactCreator(session.id),
createMediaArtifact: buildFastAgentMediaArtifactCreator(session.id),
activity: createFastAgentSlackSessionActivity({
slack: context.slack,
workspaceId: context.teamId,
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/handlers/slack/events/fast-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { appendAttachmentTextsToPromptText } from '@roomote/cloud-agents';
import {
admitFastAgentHumanFollowUp,
buildFastAgentMediaArtifactCreator,
createFastAgentConversationArtifact,
persistFastAgentInlineHumanTurn,
wakeFastAgentParentEventAt,
Expand Down Expand Up @@ -308,6 +309,7 @@ export async function processFastAgentMessage(params: {
fastConversationId: session.id,
...artifact,
}),
createMediaArtifact: buildFastAgentMediaArtifactCreator(session.id),
...(durableTurn
? {
requestDurableResume: () =>
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ as per-task auth tokens or workspace paths.
| `R_ALLOWED_EMAILS` | Optional | Comma-separated email allowlist for deployments that restrict sign-in by email. |
| `R_ELEVENLABS_API_KEY` | Optional | ElevenLabs API key for narrated feature-demo videos. The key stays on the control plane; sandboxes reach text-to-speech only through an authenticated Roomote endpoint. A key scoped to text-to-speech only is sufficient and recommended. |
| `R_ELEVENLABS_VOICE_ID` | Optional | ElevenLabs voice ID used for feature-demo narration. Required alongside the API key for narration to be available. |
| `R_FAST_BROWSER_PROVIDER` | Optional | Enables the Fast `browse` tool, which gives every Session a private browser without a sandbox. Set to `browseruse` to use Browser Use cloud browsers (requires `R_BROWSER_USE_API_KEY`). Unset disables the tool. |
| `R_BROWSER_USE_API_KEY` | Optional | Browser Use API key for the Fast `browse` tool. The key stays on the control plane; the model only sees the browser command output. |

During Microsoft Teams setup, Roomote uses the Microsoft Entra app values for
the Teams bot by default. Use **Show advanced config** after the Directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,13 @@ function getVisibleToolInput(
? 'message'
: toolName === 'inspect_images'
? 'question'
: toolName === 'post_to_channel'
? 'text'
: toolName === 'send_chat_reaction_emoji'
? 'name'
: null;
: toolName === 'browse'
? 'command'
: toolName === 'post_to_channel'
? 'text'
: toolName === 'send_chat_reaction_emoji'
? 'name'
: null;
if (!visibleField && toolName !== 'receive_task_report') {
return null;
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FolderIcon,
GalleryVerticalEnd,
GitPullRequest,
Globe,
HardDriveUpload,
List,
ListChecks,
Expand Down Expand Up @@ -40,6 +41,7 @@ export function toolIconForKey(key: ToolIconKey): LucideIcon {
if (key === 'message') return MessageSquareText;
if (key === 'memory') return BookOpenText;
if (key === 'artifact') return HardDriveUpload;
if (key === 'globe') return Globe;
if (key === 'widget') return GalleryVerticalEnd;
if (key === 'roomote') return RoomoteR;
if (key === 'video') return Video;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface ResolvedToolPolicy {
}

const CONSEQUENTIAL_RECEIPTS = new Set([
'browse',
'launch_task',
'review_pull_request',
'cancel_task',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type ToolIconKey =
| 'environment'
| 'alert'
| 'messages'
| 'globe'
| 'tool';

type ToolPresentationPhase = 'running' | 'completed' | 'failed';
Expand Down Expand Up @@ -106,6 +107,7 @@ const COMMUNICATION_TOOL_NAMES = new Set([
'ignore_event',
]);
const TOOL_ICON_OVERRIDES: Readonly<Partial<Record<string, ToolIconKey>>> = {
browse: 'globe',
manage_custom_automations: 'task',
manage_wakeups: 'task',
get_about_me: 'roomote',
Expand Down Expand Up @@ -426,6 +428,16 @@ function resolveReceiptLanguage(
verb: byPhase('Inspecting', 'Inspected', 'Failed to Inspect'),
object: 'Images',
};
if (toolName === 'browse') {
const command = stringArgument(args, 'command');
const summary = command
? command.split(/\s+/u).slice(0, 2).join(' ')
: null;
return {
verb: byPhase('Browsing', 'Browsed', 'Failed to Browse'),
object: summary ?? 'browser',
};
}
if (nativeToolName === 'skill' || nativeToolName === 'load_skill') {
const name = stringArgument(args, 'name');
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { isSubagentToolPayload } from './subagent-tool';
import type { AcpToolCallUiMessage, AcpToolResultUiMessage } from './types';

const MANAGE_ARTIFACTS_TOOL_NAME = 'manage_artifacts';
const BROWSE_TOOL_NAME = 'browse';

type VisualProofUploadExtraction = {
artifactId: string;
artifactType: 'visual-proof';
viewUrl: string;
rawUrl?: string;
/** Set by the Fast `browse` tool so a recording's rawUrl renders as video. */
contentType?: string;
};

export type VisualProofDisplayMedia =
Expand Down Expand Up @@ -103,6 +106,7 @@ function parseVisualProofSuccessPayload(
const viewUrl = asNonEmptyString(record.viewUrl);
const artifactType = asNonEmptyString(record.artifactType);
const rawUrl = asNonEmptyString(record.rawUrl) ?? undefined;
const contentType = asNonEmptyString(record.contentType) ?? undefined;

if (!artifactId || !viewUrl || artifactType !== 'visual-proof') {
return null;
Expand All @@ -113,6 +117,7 @@ function parseVisualProofSuccessPayload(
artifactType: 'visual-proof',
viewUrl,
...(rawUrl ? { rawUrl } : {}),
...(contentType ? { contentType } : {}),
};
}

Expand All @@ -134,14 +139,22 @@ function extractVisualProofUploadFromToolMessage(
return null;
}

if (msg.data.isMcp !== true) {
return null;
}

const toolName = getMcpToolName(msg.data);
// The Fast `browse` native tool saves its screenshots and recordings the
// same way task sandboxes upload visual proof.
const isBrowseCapture =
msg.data.isMcp === false &&
msg.data.isRoomoteNativeTool === true &&
toolName === BROWSE_TOOL_NAME;

if (!isBrowseCapture) {
if (msg.data.isMcp !== true) {
return null;
}

if (toolName !== MANAGE_ARTIFACTS_TOOL_NAME) {
return null;
if (toolName !== MANAGE_ARTIFACTS_TOOL_NAME) {
return null;
}
}

const output = asNonEmptyString(msg.data.output);
Expand Down Expand Up @@ -228,7 +241,20 @@ function resolveVisualProofDisplayMedia(
}
}

// Upload contract: rawUrl is only set for images.
// The Fast browse tool signs rawUrl for recordings too; the raw route
// serves WebM, so play it inline rather than treating it as an image.
if (extraction.rawUrl && extraction.contentType?.startsWith('video/')) {
return {
kind: 'video',
src: extraction.rawUrl,
viewUrl: extraction.viewUrl,
artifactId: extraction.artifactId,
path,
version,
};
}

// Upload contract: otherwise rawUrl is only set for images.
if (extraction.rawUrl) {
return {
kind: 'image',
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/trpc/commands/fast-sessions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
} from '@roomote/cloud-agents/server';
import {
buildFastAgentArtifactCreator,
buildFastAgentMediaArtifactCreator,
buildFastAgentSurfaceReplyDelivery,
createFastAgentSessionArtifact,
createFastAgentSessionMediaArtifact,
persistFastAgentInlineHumanTurn,
resolveUserMcpServerConfigs,
wakeFastAgentParentEventAt,
Expand Down Expand Up @@ -454,6 +456,11 @@ export async function startFastSessionCommand(
...artifact,
});
},
createMediaArtifact: (artifact) =>
createFastAgentSessionMediaArtifact({
sessionId: unifiedSession.id,
...artifact,
}),
launchTask,
postReply: async () => {},
},
Expand Down Expand Up @@ -550,6 +557,7 @@ export async function startSetupFastSessionCommand(
conversation,
adapter: {
createArtifact: buildFastAgentArtifactCreator(session.id),
createMediaArtifact: buildFastAgentMediaArtifactCreator(session.id),
launchTask: createFastAgentWebTaskLauncher({
userId: auth.userId,
}),
Expand Down Expand Up @@ -852,6 +860,7 @@ export async function submitFastSessionUserInputCommand(
conversation,
adapter: {
createArtifact: buildFastAgentArtifactCreator(session.id),
createMediaArtifact: buildFastAgentMediaArtifactCreator(session.id),
launchTask: createFastAgentWebTaskLauncher({
userId: auth.userId,
}),
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/trpc/commands/setup/setup-session.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createHash } from 'node:crypto';

import { type FastAgentTurnAdapter } from '@roomote/cloud-agents/server';
import { buildFastAgentArtifactCreator } from '@roomote/sdk/server';
import {
buildFastAgentArtifactCreator,
buildFastAgentMediaArtifactCreator,
} from '@roomote/sdk/server';
import {
and,
db,
Expand Down Expand Up @@ -380,6 +383,9 @@ async function buildSetupPlatformEventTurn(
createArtifact: buildFastAgentArtifactCreator(
conversation.fastConversationId,
),
createMediaArtifact: buildFastAgentMediaArtifactCreator(
conversation.fastConversationId,
),
launchTask: (
await import('@roomote/cloud-agents/server')
).createFastAgentWebTaskLauncher({
Expand Down
Loading
Loading