diff --git a/.env.example b/.env.example
index 702dc04e..24e72d68 100644
--- a/.env.example
+++ b/.env.example
@@ -40,6 +40,9 @@ BROWSER_PORT=4311
# Use the same random 24+ character secret in app/browser. All redirects are blocked.
# Supply canonical public URLs. Use the separate browser container for isolation.
+# Optional connected apps through Composio; see docs/SETUP.md#composio.
+COMPOSIO_API_KEY=
+
# Optional realtime speech; compute uses the same configured Intelligence thread.
VOICE_API_KEY=
VOICE_MODEL=
diff --git a/README.md b/README.md
index 8d00e323..59d17193 100644
--- a/README.md
+++ b/README.md
@@ -185,7 +185,7 @@ See [Setup](docs/SETUP.md) for configuration, Slack, calls, the browser service,
Conversation messages, tool calls, and run events are persisted in the configured Intelligence deployment. The hosted setup sends them to CopilotKit's cloud; the local evaluation setup uses Intelligence on your machine. OpenDots has no standalone SQLite conversation store. SQLite stores pages, workspace metadata, and thread bindings separately.
-The configured model provider receives conversation context, including authorized page content and tool results. Local Intelligence does not make a remote model local: configure the app's model and the Automatic Learning model separately. Public-web research sends queries and selected URLs to Parallel by default; set `WEB_SEARCH_PROVIDER=disabled` to disable those tools. Speech and messaging integrations send data to their configured providers when used.
+The configured model provider receives conversation context, including authorized page content and tool results. Local Intelligence does not make a remote model local: configure the app's model and the Automatic Learning model separately. Public-web research sends queries and selected URLs to Parallel by default; set `WEB_SEARCH_PROVIDER=disabled` to disable those tools. When connected apps are enabled, tool arguments and results pass through Composio and the app provider. Speech and messaging integrations send data to their configured providers when used.
CopilotKit SDK telemetry collects usage metadata separately from conversation persistence. OpenDots also records bounded browser setup stages and first successful assistant activation with a random installation ID; the same opt-out flags disable this tracking and purge pending setup events. See [browser setup telemetry](docs/SETUP-TELEMETRY.md). Runtime events are tagged `OpenDots`; see [signup and usage tracking](docs/TELEMETRY.md). Set `COPILOTKIT_TELEMETRY_DISABLED=true` or `DO_NOT_TRACK=1` to opt out; see [CopilotKit telemetry](https://docs.copilotkit.ai/telemetry). Review [CopilotKit's privacy policy](https://www.copilotkit.ai/privacy-policy) and the policies and retention settings of each service you configure. Installing or renewing local Intelligence still requires CopilotKit sign-in and internet access.
diff --git a/docs/SETUP.md b/docs/SETUP.md
index 274b3151..80b0c40d 100644
--- a/docs/SETUP.md
+++ b/docs/SETUP.md
@@ -110,6 +110,18 @@ npm run browser
Use the same secret on the app and browser processes. Browser navigation is read-only with JavaScript disabled. Private addresses, redirects, and authenticated pages are unsupported; provide a canonical public URL. This is a bounded research tool, not a general desktop or shell.
+## Composio
+
+Set `COMPOSIO_API_KEY` to let Dots use the owner’s connected apps. Restart OpenDots, enable Connected apps in the global settings and for a Dot, then optionally restrict that Dot to comma-separated toolkit slugs such as `gmail` or `google_calendar`. Leaving the toolkit list blank lets the Dot search every toolkit available to the Composio project.
+
+Connected apps remain disabled when you add the API key. Composio tools can read and change data in real accounts, and a blank toolkit list permits every toolkit available to the Composio project. The global and per-Dot switches require the owner to grant that access explicitly.
+
+OpenDots uses the stable `OWNER_ID` as the Composio user identity. Do not change it after connecting accounts. When a tool needs authentication, open the HTTPS Connect link shown in chat and finish the provider’s sign-in flow. Disabling either permission prevents new app calls and stops active work. Without `COMPOSIO_API_KEY`, no Composio client or tools are created and the controls stay hidden.
+
+To test the integration, configure a working model, limit a Dot to one toolkit, and start with a read-only request such as listing recent messages or calendar events. Complete the Connect flow when prompted, then repeat the request. Test write actions only with an account where an accidental change is safe.
+
+Tool arguments and results pass through Composio and the connected app provider. Keep credentials in the server environment and review [Composio’s documentation](https://docs.composio.dev) and each provider’s privacy and retention terms.
+
## Persistent Dot computers
For a separate browser, persistent files, and optional shell for each specialist, follow [Computer setup](COMPUTERS.md). This uses pinned OpenBot computer/supervisor services and per-Dot permissions. Parallel research tools remain available alongside configured computer tools. With the browser provider selected, Dots use their computer tools in place of the read-only public-page tool; enable each Dot's required capabilities before use.
diff --git a/package-lock.json b/package-lock.json
index 05d28509..eb6ca3d9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@ag-ui/client": "^0.0.59",
"@ag-ui/core": "^0.0.59",
+ "@composio/core": "^0.22.0",
"@copilotkit/channels": "^0.11.0",
"@copilotkit/core": "^1.75.0",
"@copilotkit/react-core": "^1.75.0",
@@ -730,6 +731,67 @@
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
"license": "Apache-2.0"
},
+ "node_modules/@composio/client": {
+ "version": "2.0.0-rc.8",
+ "resolved": "https://registry.npmjs.org/@composio/client/-/client-2.0.0-rc.8.tgz",
+ "integrity": "sha512-0C0Flmblx0Y3i6iGUY/MvST4aNXn3xEmPEA4wqZd3acm9x6u61/0i1LXCgBzFTTZEveK8YBFRu8Lpo/SMZwAFQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@composio/core": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/@composio/core/-/core-0.22.0.tgz",
+ "integrity": "sha512-Inc7/yqhRQTfvU3km/VwSpewwJ9O/5mnC2FjSPDp56MfEdUfx61rwoi6+pancTUhHsHic5qe0lB4C2rHd7iioA==",
+ "license": "ISC",
+ "dependencies": {
+ "@composio/client": "2.0.0-rc.8",
+ "@composio/json-schema-to-zod": "0.3.3",
+ "@types/json-schema": "^7.0.15",
+ "is-fs-case-sensitive": "^2.0.0",
+ "openai": "^7.21.0",
+ "picocolors": "^1.1.1",
+ "pusher-js": "^8.6.0",
+ "semver": "^7.8.5",
+ "undici": "^7.29.1",
+ "zod-to-json-schema": "^3.25.2"
+ },
+ "engines": {
+ "node": ">=22.22.3"
+ },
+ "peerDependencies": {
+ "zod": ">=3.25.76 <5"
+ }
+ },
+ "node_modules/@composio/core/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@composio/json-schema-to-zod": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@composio/json-schema-to-zod/-/json-schema-to-zod-0.3.3.tgz",
+ "integrity": "sha512-QmgYo4IeK5/34Iqo1jSx6fSnLEyL++07FlW5Y6SfGFOhyb5htnY+vZPjs/tL8puJYhiEU7FV7kXdu5EAQoGrSA==",
+ "license": "ISC",
+ "dependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "dequal": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=22.22.3"
+ },
+ "peerDependencies": {
+ "zod": ">=3.25.76 <5"
+ }
+ },
"node_modules/@copilotkit/a2ui-renderer": {
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/@copilotkit/a2ui-renderer/-/a2ui-renderer-1.75.0.tgz",
@@ -2246,7 +2308,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2263,7 +2324,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2280,7 +2340,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2297,7 +2356,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2314,7 +2372,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2331,7 +2388,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2348,7 +2404,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2365,7 +2420,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2382,7 +2436,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2399,7 +2452,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2416,7 +2468,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2433,7 +2484,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2450,7 +2500,6 @@
"cpu": [
"mips64el"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2467,7 +2516,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2484,7 +2532,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2501,7 +2548,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2518,7 +2564,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2535,7 +2580,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2552,7 +2596,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2569,7 +2612,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2586,7 +2628,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2603,7 +2644,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2620,7 +2660,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2637,7 +2676,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2654,7 +2692,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2671,7 +2708,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4121,7 +4157,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4138,7 +4173,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4155,7 +4189,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4172,7 +4205,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4189,7 +4221,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4206,7 +4237,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4223,7 +4253,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4240,7 +4269,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4257,7 +4285,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4274,7 +4301,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4291,7 +4317,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4308,7 +4333,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4325,7 +4349,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4342,7 +4365,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4359,7 +4381,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -8151,7 +8172,7 @@
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
- "dev": true,
+ "devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -8825,7 +8846,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -9719,6 +9739,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fs-case-sensitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fs-case-sensitive/-/is-fs-case-sensitive-2.0.0.tgz",
+ "integrity": "sha512-JoCsyGITdYPM+pUbeMQ4IiEuQ4wjPdeWORlG7n644isewbcxiQIr+9gmF5k7UabTP/jLBRkbgydEamR7JZBKHA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/is-fs-case-sensitive?sponsor=1"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -10093,7 +10122,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10114,7 +10142,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10135,7 +10162,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10156,7 +10182,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10177,7 +10202,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10198,7 +10222,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10219,7 +10242,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10240,7 +10262,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10261,7 +10282,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10282,7 +10302,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -10303,7 +10322,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12175,7 +12193,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "devOptional": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -12606,6 +12623,15 @@
"node": ">=6"
}
},
+ "node_modules/pusher-js": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.6.0.tgz",
+ "integrity": "sha512-wShJPfCS/kYkCBVzVW67wa9cnQIgHTszEK2XHNrFkOgGruuGw081aERAxfRjfdFU+WcIt8x6dvbwkTW4iZuQ8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "tweetnacl": "^1.0.3"
+ }
+ },
"node_modules/qified": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
@@ -13809,7 +13835,7 @@
"version": "4.23.15",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
"integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
@@ -13833,6 +13859,12 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
+ "node_modules/tweetnacl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
+ "license": "Unlicense"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index 308d87c7..e642f46c 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"dependencies": {
"@ag-ui/client": "^0.0.59",
"@ag-ui/core": "^0.0.59",
+ "@composio/core": "^0.22.0",
"@copilotkit/channels": "^0.11.0",
"@copilotkit/core": "^1.75.0",
"@copilotkit/react-core": "^1.75.0",
diff --git a/src/client/Chat.tsx b/src/client/Chat.tsx
index 8ec3ce3d..f21cb6a7 100644
--- a/src/client/Chat.tsx
+++ b/src/client/Chat.tsx
@@ -137,7 +137,7 @@ export function Chat({
setError('');
setRunning(true);
agent.addMessage({
- id: crypto.randomUUID(),
+ id: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random()}`,
role: 'user',
content: contextualMessage(text, pageContext),
});
@@ -237,6 +237,7 @@ export function Chat({
message.toolCalls?.some(
(call) =>
call.function.name.startsWith('computer_') ||
+ call.function.name === 'COMPOSIO_MANAGE_CONNECTIONS' ||
call.function.name === pageReviewTool.name ||
call.function.name === connectionActionTool.name,
))),
diff --git a/src/client/WorkspaceDialog.tsx b/src/client/WorkspaceDialog.tsx
index deee5899..ece3361b 100644
--- a/src/client/WorkspaceDialog.tsx
+++ b/src/client/WorkspaceDialog.tsx
@@ -46,6 +46,16 @@ export function WorkspaceDialog({
? (dialog.dot?.memoryAllowed ?? true)
: state.settings.memoryAllowed,
);
+ const [apps, setApps] = useState(
+ dialog.type === 'dot'
+ ? (dialog.dot?.appsAllowed ?? false)
+ : state.settings.appsAllowed,
+ );
+ const [composioToolkits, setComposioToolkits] = useState(
+ dialog.type === 'dot'
+ ? (dialog.dot?.composioToolkits ?? []).join(', ')
+ : '',
+ );
const [spaceIds, setSpaceIds] = useState(
dialog.type === 'dot' ? (dialog.dot?.spaceIds ?? [dialog.spaceId]) : [],
);
@@ -147,6 +157,11 @@ export function WorkspaceDialog({
instructions: text,
researchAllowed: research,
memoryAllowed: memory,
+ appsAllowed: apps,
+ composioToolkits: composioToolkits
+ .split(',')
+ .map((slug) => slug.trim())
+ .filter(Boolean),
learningContainerId: learningContainer.trim() || null,
skillDeliveryEnabled: skillDelivery,
};
@@ -154,7 +169,11 @@ export function WorkspaceDialog({
if (dialog.type === 'settings') {
path = '/settings';
method = 'PATCH';
- body = { researchAllowed: research, memoryAllowed: memory };
+ body = {
+ researchAllowed: research,
+ memoryAllowed: memory,
+ appsAllowed: apps,
+ };
}
if (dialog.type === 'memory') {
path = dialog.memory
@@ -292,8 +311,44 @@ export function WorkspaceDialog({
+ {workspace.setup.composio && (
+
+ )}
>
)}
+ {dialog.type === 'dot' && workspace.setup.composio && (
+
+ )}
{dialog.type === 'dot' && (