@jini-ai/integrations 0.1.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/LICENSE +201 -0
- package/README.md +51 -0
- package/dist/composio/bounded-data.d.ts +36 -0
- package/dist/composio/bounded-data.d.ts.map +1 -0
- package/dist/composio/bounded-data.js +181 -0
- package/dist/composio/bounded-data.js.map +1 -0
- package/dist/composio/catalog.d.ts +147 -0
- package/dist/composio/catalog.d.ts.map +1 -0
- package/dist/composio/catalog.js +126 -0
- package/dist/composio/catalog.js.map +1 -0
- package/dist/composio/composio-config.d.ts +42 -0
- package/dist/composio/composio-config.d.ts.map +1 -0
- package/dist/composio/composio-config.js +145 -0
- package/dist/composio/composio-config.js.map +1 -0
- package/dist/composio/composio-descriptions.d.ts +16 -0
- package/dist/composio/composio-descriptions.d.ts.map +1 -0
- package/dist/composio/composio-descriptions.js +766 -0
- package/dist/composio/composio-descriptions.js.map +1 -0
- package/dist/composio/composio.d.ts +364 -0
- package/dist/composio/composio.d.ts.map +1 -0
- package/dist/composio/composio.js +1902 -0
- package/dist/composio/composio.js.map +1 -0
- package/dist/composio/errors.d.ts +15 -0
- package/dist/composio/errors.d.ts.map +1 -0
- package/dist/composio/errors.js +16 -0
- package/dist/composio/errors.js.map +1 -0
- package/dist/composio/file-lock.d.ts +13 -0
- package/dist/composio/file-lock.d.ts.map +1 -0
- package/dist/composio/file-lock.js +92 -0
- package/dist/composio/file-lock.js.map +1 -0
- package/dist/composio/index.d.ts +18 -0
- package/dist/composio/index.d.ts.map +1 -0
- package/dist/composio/index.js +7 -0
- package/dist/composio/index.js.map +1 -0
- package/dist/composio/json-schema.d.ts +93 -0
- package/dist/composio/json-schema.d.ts.map +1 -0
- package/dist/composio/json-schema.js +498 -0
- package/dist/composio/json-schema.js.map +1 -0
- package/dist/composio/output-protection.d.ts +32 -0
- package/dist/composio/output-protection.d.ts.map +1 -0
- package/dist/composio/output-protection.js +105 -0
- package/dist/composio/output-protection.js.map +1 -0
- package/dist/composio/service.d.ts +181 -0
- package/dist/composio/service.d.ts.map +1 -0
- package/dist/composio/service.js +758 -0
- package/dist/composio/service.js.map +1 -0
- package/dist/media-providers/capability-registry.d.ts +28 -0
- package/dist/media-providers/capability-registry.d.ts.map +1 -0
- package/dist/media-providers/capability-registry.js +27 -0
- package/dist/media-providers/capability-registry.js.map +1 -0
- package/dist/media-providers/catalog.d.ts +20 -0
- package/dist/media-providers/catalog.d.ts.map +1 -0
- package/dist/media-providers/catalog.js +20 -0
- package/dist/media-providers/catalog.js.map +1 -0
- package/dist/media-providers/dispatch/context.d.ts +21 -0
- package/dist/media-providers/dispatch/context.d.ts.map +1 -0
- package/dist/media-providers/dispatch/context.js +36 -0
- package/dist/media-providers/dispatch/context.js.map +1 -0
- package/dist/media-providers/dispatch/credentials.d.ts +4 -0
- package/dist/media-providers/dispatch/credentials.d.ts.map +1 -0
- package/dist/media-providers/dispatch/credentials.js +25 -0
- package/dist/media-providers/dispatch/credentials.js.map +1 -0
- package/dist/media-providers/dispatch/engine.d.ts +30 -0
- package/dist/media-providers/dispatch/engine.d.ts.map +1 -0
- package/dist/media-providers/dispatch/engine.js +208 -0
- package/dist/media-providers/dispatch/engine.js.map +1 -0
- package/dist/media-providers/dispatch/index.d.ts +25 -0
- package/dist/media-providers/dispatch/index.d.ts.map +1 -0
- package/dist/media-providers/dispatch/index.js +24 -0
- package/dist/media-providers/dispatch/index.js.map +1 -0
- package/dist/media-providers/dispatch/openai-compatible.d.ts +44 -0
- package/dist/media-providers/dispatch/openai-compatible.d.ts.map +1 -0
- package/dist/media-providers/dispatch/openai-compatible.js +214 -0
- package/dist/media-providers/dispatch/openai-compatible.js.map +1 -0
- package/dist/media-providers/dispatch/providers/aihubmix-shared.d.ts +72 -0
- package/dist/media-providers/dispatch/providers/aihubmix-shared.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/aihubmix-shared.js +131 -0
- package/dist/media-providers/dispatch/providers/aihubmix-shared.js.map +1 -0
- package/dist/media-providers/dispatch/providers/aihubmix.d.ts +4 -0
- package/dist/media-providers/dispatch/providers/aihubmix.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/aihubmix.js +205 -0
- package/dist/media-providers/dispatch/providers/aihubmix.js.map +1 -0
- package/dist/media-providers/dispatch/providers/custom-image.d.ts +6 -0
- package/dist/media-providers/dispatch/providers/custom-image.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/custom-image.js +94 -0
- package/dist/media-providers/dispatch/providers/custom-image.js.map +1 -0
- package/dist/media-providers/dispatch/providers/elevenlabs.d.ts +4 -0
- package/dist/media-providers/dispatch/providers/elevenlabs.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/elevenlabs.js +163 -0
- package/dist/media-providers/dispatch/providers/elevenlabs.js.map +1 -0
- package/dist/media-providers/dispatch/providers/fishaudio.d.ts +3 -0
- package/dist/media-providers/dispatch/providers/fishaudio.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/fishaudio.js +83 -0
- package/dist/media-providers/dispatch/providers/fishaudio.js.map +1 -0
- package/dist/media-providers/dispatch/providers/grok.d.ts +15 -0
- package/dist/media-providers/dispatch/providers/grok.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/grok.js +150 -0
- package/dist/media-providers/dispatch/providers/grok.js.map +1 -0
- package/dist/media-providers/dispatch/providers/imagerouter.d.ts +6 -0
- package/dist/media-providers/dispatch/providers/imagerouter.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/imagerouter.js +131 -0
- package/dist/media-providers/dispatch/providers/imagerouter.js.map +1 -0
- package/dist/media-providers/dispatch/providers/minimax.d.ts +3 -0
- package/dist/media-providers/dispatch/providers/minimax.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/minimax.js +96 -0
- package/dist/media-providers/dispatch/providers/minimax.js.map +1 -0
- package/dist/media-providers/dispatch/providers/nanobanana.d.ts +3 -0
- package/dist/media-providers/dispatch/providers/nanobanana.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/nanobanana.js +142 -0
- package/dist/media-providers/dispatch/providers/nanobanana.js.map +1 -0
- package/dist/media-providers/dispatch/providers/openai.d.ts +4 -0
- package/dist/media-providers/dispatch/providers/openai.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/openai.js +231 -0
- package/dist/media-providers/dispatch/providers/openai.js.map +1 -0
- package/dist/media-providers/dispatch/providers/openrouter.d.ts +13 -0
- package/dist/media-providers/dispatch/providers/openrouter.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/openrouter.js +152 -0
- package/dist/media-providers/dispatch/providers/openrouter.js.map +1 -0
- package/dist/media-providers/dispatch/providers/senseaudio.d.ts +4 -0
- package/dist/media-providers/dispatch/providers/senseaudio.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/senseaudio.js +190 -0
- package/dist/media-providers/dispatch/providers/senseaudio.js.map +1 -0
- package/dist/media-providers/dispatch/providers/volcengine.d.ts +3 -0
- package/dist/media-providers/dispatch/providers/volcengine.d.ts.map +1 -0
- package/dist/media-providers/dispatch/providers/volcengine.js +95 -0
- package/dist/media-providers/dispatch/providers/volcengine.js.map +1 -0
- package/dist/media-providers/dispatch/response-parsers.d.ts +44 -0
- package/dist/media-providers/dispatch/response-parsers.d.ts.map +1 -0
- package/dist/media-providers/dispatch/response-parsers.js +103 -0
- package/dist/media-providers/dispatch/response-parsers.js.map +1 -0
- package/dist/media-providers/dispatch/ssrf-guard.d.ts +51 -0
- package/dist/media-providers/dispatch/ssrf-guard.d.ts.map +1 -0
- package/dist/media-providers/dispatch/ssrf-guard.js +215 -0
- package/dist/media-providers/dispatch/ssrf-guard.js.map +1 -0
- package/dist/media-providers/dispatch/stub.d.ts +5 -0
- package/dist/media-providers/dispatch/stub.d.ts.map +1 -0
- package/dist/media-providers/dispatch/stub.js +98 -0
- package/dist/media-providers/dispatch/stub.js.map +1 -0
- package/dist/media-providers/dispatch/types.d.ts +133 -0
- package/dist/media-providers/dispatch/types.d.ts.map +1 -0
- package/dist/media-providers/dispatch/types.js +2 -0
- package/dist/media-providers/dispatch/types.js.map +1 -0
- package/dist/media-providers/dispatch/vendor-adapter.d.ts +54 -0
- package/dist/media-providers/dispatch/vendor-adapter.d.ts.map +1 -0
- package/dist/media-providers/dispatch/vendor-adapter.js +68 -0
- package/dist/media-providers/dispatch/vendor-adapter.js.map +1 -0
- package/dist/media-providers/dispatch/vendor-registry.d.ts +36 -0
- package/dist/media-providers/dispatch/vendor-registry.d.ts.map +1 -0
- package/dist/media-providers/dispatch/vendor-registry.js +46 -0
- package/dist/media-providers/dispatch/vendor-registry.js.map +1 -0
- package/dist/media-providers/index.d.ts +22 -0
- package/dist/media-providers/index.d.ts.map +1 -0
- package/dist/media-providers/index.js +22 -0
- package/dist/media-providers/index.js.map +1 -0
- package/dist/media-providers/policy.d.ts +49 -0
- package/dist/media-providers/policy.d.ts.map +1 -0
- package/dist/media-providers/policy.js +39 -0
- package/dist/media-providers/policy.js.map +1 -0
- package/dist/media-providers/providers.d.ts +33 -0
- package/dist/media-providers/providers.d.ts.map +1 -0
- package/dist/media-providers/providers.js +200 -0
- package/dist/media-providers/providers.js.map +1 -0
- package/dist/media-providers/seed.d.ts +14 -0
- package/dist/media-providers/seed.d.ts.map +1 -0
- package/dist/media-providers/seed.js +109 -0
- package/dist/media-providers/seed.js.map +1 -0
- package/dist/media-providers/sqlite-task-store.d.ts +29 -0
- package/dist/media-providers/sqlite-task-store.d.ts.map +1 -0
- package/dist/media-providers/sqlite-task-store.js +234 -0
- package/dist/media-providers/sqlite-task-store.js.map +1 -0
- package/dist/media-providers/staging.d.ts +29 -0
- package/dist/media-providers/staging.d.ts.map +1 -0
- package/dist/media-providers/staging.js +141 -0
- package/dist/media-providers/staging.js.map +1 -0
- package/dist/media-providers/task-store.d.ts +94 -0
- package/dist/media-providers/task-store.d.ts.map +1 -0
- package/dist/media-providers/task-store.js +160 -0
- package/dist/media-providers/task-store.js.map +1 -0
- package/dist/media-providers/tokens.d.ts +7 -0
- package/dist/media-providers/tokens.d.ts.map +1 -0
- package/dist/media-providers/tokens.js +13 -0
- package/dist/media-providers/tokens.js.map +1 -0
- package/dist/media-providers/types.d.ts +123 -0
- package/dist/media-providers/types.d.ts.map +1 -0
- package/dist/media-providers/types.js +12 -0
- package/dist/media-providers/types.js.map +1 -0
- package/dist/media-providers/video-request.d.ts +62 -0
- package/dist/media-providers/video-request.d.ts.map +1 -0
- package/dist/media-providers/video-request.js +219 -0
- package/dist/media-providers/video-request.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { classifyConnectorToolSafety, connectorDefinitionToDetail, } from './catalog.js';
|
|
5
|
+
import { COMPOSIO_MAX_SECRET_STORE_BYTES, readPrivateBoundedUtf8File, toStructurallyBoundedJsonValue, } from './bounded-data.js';
|
|
6
|
+
import { ComposioConnectorProvider, isComposioCredentialMaterial, } from './composio.js';
|
|
7
|
+
import { ConnectorServiceError } from './errors.js';
|
|
8
|
+
import { assertConnectorInputMatchesSchema } from './json-schema.js';
|
|
9
|
+
import { withExclusiveFileLock } from './file-lock.js';
|
|
10
|
+
import { CONNECTOR_MAX_OUTPUT_BYTES, protectConnectorOutput, } from './output-protection.js';
|
|
11
|
+
function publicComposioAuthStart(auth) {
|
|
12
|
+
return {
|
|
13
|
+
kind: auth.kind,
|
|
14
|
+
...(auth.redirectUrl === undefined ? {} : { redirectUrl: auth.redirectUrl }),
|
|
15
|
+
...(auth.providerConnectionId === undefined ? {} : { providerConnectionId: auth.providerConnectionId }),
|
|
16
|
+
...(auth.expiresAt === undefined ? {} : { expiresAt: auth.expiresAt }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function isConnectorAuthStaleError(error, request) {
|
|
20
|
+
if (!(error instanceof ConnectorServiceError) || error.code !== 'CONNECTOR_EXECUTION_FAILED')
|
|
21
|
+
return false;
|
|
22
|
+
const details = error.details;
|
|
23
|
+
return details?.connectorId === request.connectorId
|
|
24
|
+
&& details.toolName === request.toolName
|
|
25
|
+
&& details.authStale === true;
|
|
26
|
+
}
|
|
27
|
+
function connectorAuthExpiredMessage(definition) {
|
|
28
|
+
return `${definition.name} authorization expired. Reconnect ${definition.name}.`;
|
|
29
|
+
}
|
|
30
|
+
export { ConnectorServiceError } from './errors.js';
|
|
31
|
+
export { CONNECTOR_MAX_OUTPUT_BYTES, protectConnectorOutput, } from './output-protection.js';
|
|
32
|
+
function nowIso(now) {
|
|
33
|
+
return new Date(now()).toISOString();
|
|
34
|
+
}
|
|
35
|
+
function cloneCredentialMaterial(credentials) {
|
|
36
|
+
const cloned = toStructurallyBoundedJsonValue(credentials);
|
|
37
|
+
if (cloned === null || typeof cloned !== 'object' || Array.isArray(cloned)) {
|
|
38
|
+
throw new TypeError('connector credentials must be a bounded JSON object');
|
|
39
|
+
}
|
|
40
|
+
return cloned;
|
|
41
|
+
}
|
|
42
|
+
export class InMemoryConnectorCredentialStore {
|
|
43
|
+
records = new Map();
|
|
44
|
+
get(connectorId) {
|
|
45
|
+
const record = this.records.get(connectorId);
|
|
46
|
+
return record === undefined ? undefined : { ...record, credentials: cloneCredentialMaterial(record.credentials) };
|
|
47
|
+
}
|
|
48
|
+
set(record) {
|
|
49
|
+
this.records.set(record.connectorId, { ...record, credentials: cloneCredentialMaterial(record.credentials) });
|
|
50
|
+
}
|
|
51
|
+
delete(connectorId) {
|
|
52
|
+
this.records.delete(connectorId);
|
|
53
|
+
}
|
|
54
|
+
deleteByProvider(provider) {
|
|
55
|
+
for (const [connectorId, record] of this.records.entries()) {
|
|
56
|
+
if (record.credentials.provider === provider)
|
|
57
|
+
this.records.delete(connectorId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class FileConnectorCredentialStore {
|
|
62
|
+
filePath;
|
|
63
|
+
constructor({ filePath }) {
|
|
64
|
+
if (!filePath.trim())
|
|
65
|
+
throw new TypeError('Composio credential filePath must not be empty.');
|
|
66
|
+
this.filePath = path.resolve(filePath);
|
|
67
|
+
}
|
|
68
|
+
get(connectorId) {
|
|
69
|
+
return this.readRecords()[connectorId];
|
|
70
|
+
}
|
|
71
|
+
set(record) {
|
|
72
|
+
withExclusiveFileLock(this.filePath, () => {
|
|
73
|
+
const records = this.readRecords();
|
|
74
|
+
records[record.connectorId] = { ...record, credentials: cloneCredentialMaterial(record.credentials) };
|
|
75
|
+
this.writeRecords(records);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
delete(connectorId) {
|
|
79
|
+
withExclusiveFileLock(this.filePath, () => {
|
|
80
|
+
const records = this.readRecords();
|
|
81
|
+
if (records[connectorId] === undefined)
|
|
82
|
+
return;
|
|
83
|
+
delete records[connectorId];
|
|
84
|
+
this.writeRecords(records);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
deleteByProvider(provider) {
|
|
88
|
+
withExclusiveFileLock(this.filePath, () => {
|
|
89
|
+
const records = this.readRecords();
|
|
90
|
+
let changed = false;
|
|
91
|
+
for (const [connectorId, record] of Object.entries(records)) {
|
|
92
|
+
if (record.credentials.provider === provider) {
|
|
93
|
+
delete records[connectorId];
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (changed)
|
|
98
|
+
this.writeRecords(records);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
readRecords() {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(readPrivateBoundedUtf8File(this.filePath, COMPOSIO_MAX_SECRET_STORE_BYTES));
|
|
104
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
105
|
+
return {};
|
|
106
|
+
const records = {};
|
|
107
|
+
for (const [connectorId, value] of Object.entries(parsed)) {
|
|
108
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
109
|
+
continue;
|
|
110
|
+
const raw = value;
|
|
111
|
+
if (raw.schemaVersion !== 1 || raw.connectorId !== connectorId || typeof raw.accountLabel !== 'string' || typeof raw.updatedAt !== 'string')
|
|
112
|
+
continue;
|
|
113
|
+
if (!raw.credentials || typeof raw.credentials !== 'object' || Array.isArray(raw.credentials))
|
|
114
|
+
continue;
|
|
115
|
+
if (raw.credentials.provider === 'composio' && !isComposioCredentialMaterial(raw.credentials))
|
|
116
|
+
continue;
|
|
117
|
+
records[connectorId] = {
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
connectorId,
|
|
120
|
+
accountLabel: raw.accountLabel,
|
|
121
|
+
credentials: cloneCredentialMaterial(raw.credentials),
|
|
122
|
+
updatedAt: raw.updatedAt,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return records;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
129
|
+
return {};
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
writeRecords(records) {
|
|
134
|
+
const dir = path.dirname(this.filePath);
|
|
135
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
136
|
+
fs.chmodSync(dir, 0o700);
|
|
137
|
+
const tempPath = `${this.filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
138
|
+
const serialized = `${JSON.stringify(records, null, 2)}\n`;
|
|
139
|
+
if (Buffer.byteLength(serialized, 'utf8') > COMPOSIO_MAX_SECRET_STORE_BYTES) {
|
|
140
|
+
throw new RangeError(`Composio credentials exceed the ${COMPOSIO_MAX_SECRET_STORE_BYTES}-byte limit.`);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(tempPath, serialized, { encoding: 'utf8', mode: 0o600 });
|
|
144
|
+
fs.renameSync(tempPath, this.filePath);
|
|
145
|
+
fs.chmodSync(this.filePath, 0o600);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
try {
|
|
149
|
+
fs.unlinkSync(tempPath);
|
|
150
|
+
}
|
|
151
|
+
catch (cleanupError) {
|
|
152
|
+
if (!isErrno(cleanupError, 'ENOENT'))
|
|
153
|
+
throw cleanupError;
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function cloneStatus(status) {
|
|
160
|
+
return {
|
|
161
|
+
status: status.status,
|
|
162
|
+
...(status.accountLabel === undefined ? {} : { accountLabel: status.accountLabel }),
|
|
163
|
+
...(status.lastError === undefined ? {} : { lastError: status.lastError }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function isAutoConnectedConnector(definition) {
|
|
167
|
+
return (definition.authentication === 'local' || definition.authentication === 'none')
|
|
168
|
+
&& definition.tools.every((tool) => tool.requiredScopes.length === 0);
|
|
169
|
+
}
|
|
170
|
+
function approvalRank(approval) {
|
|
171
|
+
switch (approval) {
|
|
172
|
+
case 'auto':
|
|
173
|
+
return 0;
|
|
174
|
+
case 'confirm':
|
|
175
|
+
return 1;
|
|
176
|
+
case 'disabled':
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function stricterApproval(...approvals) {
|
|
181
|
+
return approvals.reduce((left, right) => (approvalRank(left) >= approvalRank(right) ? left : right));
|
|
182
|
+
}
|
|
183
|
+
function runtimeSafetyForTool(tool) {
|
|
184
|
+
const classified = classifyConnectorToolSafety(tool);
|
|
185
|
+
if (classified.sideEffect !== 'read' || classified.approval !== 'auto')
|
|
186
|
+
return classified;
|
|
187
|
+
return tool.safety;
|
|
188
|
+
}
|
|
189
|
+
function defaultConnectedAccountLabel(definition) {
|
|
190
|
+
return definition.name;
|
|
191
|
+
}
|
|
192
|
+
/** In-memory connection state backed optionally by durable credentials. */
|
|
193
|
+
export class ConnectorStatusService {
|
|
194
|
+
statuses = new Map();
|
|
195
|
+
now;
|
|
196
|
+
credentialStore;
|
|
197
|
+
constructor(options = {}) {
|
|
198
|
+
this.now = options.now ?? Date.now;
|
|
199
|
+
this.credentialStore = options.credentialStore;
|
|
200
|
+
for (const [connectorId, status] of Object.entries(options.initialStatuses ?? {})) {
|
|
201
|
+
this.statuses.set(connectorId, { ...cloneStatus(status), updatedAt: nowIso(this.now) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
setCredentialStore(credentialStore) {
|
|
205
|
+
this.credentialStore = credentialStore;
|
|
206
|
+
}
|
|
207
|
+
deleteCredentialsByProvider(provider) {
|
|
208
|
+
for (const [connectorId, status] of this.statuses.entries()) {
|
|
209
|
+
if (status.status !== 'connected')
|
|
210
|
+
continue;
|
|
211
|
+
const credential = this.getCredential(connectorId);
|
|
212
|
+
if (credential?.credentials.provider === provider)
|
|
213
|
+
this.statuses.delete(connectorId);
|
|
214
|
+
}
|
|
215
|
+
this.credentialStore?.deleteByProvider(provider);
|
|
216
|
+
}
|
|
217
|
+
getStatus(definition) {
|
|
218
|
+
if (definition.disabled)
|
|
219
|
+
return { status: 'disabled' };
|
|
220
|
+
const stored = this.statuses.get(definition.id);
|
|
221
|
+
if (stored)
|
|
222
|
+
return cloneStatus(stored);
|
|
223
|
+
const credentialRecord = this.getCredential(definition.id);
|
|
224
|
+
if (credentialRecord !== undefined) {
|
|
225
|
+
return { status: 'connected', accountLabel: credentialRecord.accountLabel };
|
|
226
|
+
}
|
|
227
|
+
if (isAutoConnectedConnector(definition)) {
|
|
228
|
+
return { status: 'connected', accountLabel: defaultConnectedAccountLabel(definition) };
|
|
229
|
+
}
|
|
230
|
+
return { status: 'available' };
|
|
231
|
+
}
|
|
232
|
+
listStatuses() {
|
|
233
|
+
return Object.fromEntries(Array.from(this.statuses.entries()).map(([connectorId, status]) => [connectorId, cloneStatus(status)]));
|
|
234
|
+
}
|
|
235
|
+
connect(definition, accountLabel, credentials) {
|
|
236
|
+
if (definition.disabled)
|
|
237
|
+
return { status: 'disabled' };
|
|
238
|
+
if (credentials !== undefined) {
|
|
239
|
+
this.credentialStore?.set({
|
|
240
|
+
schemaVersion: 1,
|
|
241
|
+
connectorId: definition.id,
|
|
242
|
+
accountLabel: accountLabel ?? defaultConnectedAccountLabel(definition),
|
|
243
|
+
credentials,
|
|
244
|
+
updatedAt: nowIso(this.now),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const next = {
|
|
248
|
+
status: 'connected',
|
|
249
|
+
accountLabel: accountLabel ?? defaultConnectedAccountLabel(definition),
|
|
250
|
+
updatedAt: nowIso(this.now),
|
|
251
|
+
};
|
|
252
|
+
this.statuses.set(definition.id, next);
|
|
253
|
+
return cloneStatus(next);
|
|
254
|
+
}
|
|
255
|
+
getCredential(connectorId) {
|
|
256
|
+
return this.credentialStore?.get(connectorId);
|
|
257
|
+
}
|
|
258
|
+
disconnect(definition) {
|
|
259
|
+
if (definition.disabled)
|
|
260
|
+
return { status: 'disabled' };
|
|
261
|
+
this.credentialStore?.delete(definition.id);
|
|
262
|
+
if (isAutoConnectedConnector(definition)) {
|
|
263
|
+
this.statuses.delete(definition.id);
|
|
264
|
+
return this.getStatus(definition);
|
|
265
|
+
}
|
|
266
|
+
const next = { status: 'available', updatedAt: nowIso(this.now) };
|
|
267
|
+
this.statuses.set(definition.id, next);
|
|
268
|
+
return cloneStatus(next);
|
|
269
|
+
}
|
|
270
|
+
setError(definition, lastError, accountLabel) {
|
|
271
|
+
if (definition.disabled)
|
|
272
|
+
return { status: 'disabled' };
|
|
273
|
+
const next = {
|
|
274
|
+
status: 'error',
|
|
275
|
+
...(accountLabel === undefined ? {} : { accountLabel }),
|
|
276
|
+
lastError,
|
|
277
|
+
updatedAt: nowIso(this.now),
|
|
278
|
+
};
|
|
279
|
+
this.statuses.set(definition.id, next);
|
|
280
|
+
return cloneStatus(next);
|
|
281
|
+
}
|
|
282
|
+
markAuthenticationExpired(definition, lastError, accountLabel) {
|
|
283
|
+
this.credentialStore?.delete(definition.id);
|
|
284
|
+
return this.setError(definition, lastError, accountLabel);
|
|
285
|
+
}
|
|
286
|
+
clear(connectorId) {
|
|
287
|
+
this.statuses.delete(connectorId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export const CONNECTOR_RUN_RATE_LIMIT_CALLS = 10;
|
|
291
|
+
export const CONNECTOR_RUN_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
292
|
+
export const CONNECTOR_RUN_LIMIT_TTL_MS = 15 * 60_000;
|
|
293
|
+
export const CONNECTOR_RUN_TOTAL_CALL_LIMIT = 60;
|
|
294
|
+
export const COMPOSIO_AUTH_CONFIG_PREPARE_LIMIT = 100;
|
|
295
|
+
const CONNECTOR_RUN_LIMIT_MAX_BUCKETS = 10_000;
|
|
296
|
+
const CONNECTOR_RUN_IDENTIFIER_MAX_LENGTH = 256;
|
|
297
|
+
const CONNECTOR_EXECUTE_IDENTIFIER_MAX_LENGTH = 256;
|
|
298
|
+
const CONNECTOR_EXECUTE_INPUT_MAX_BYTES = 1024 * 1024;
|
|
299
|
+
function canonicalRunIdentifier(value, label) {
|
|
300
|
+
const canonical = value.normalize('NFKC').trim().toLowerCase();
|
|
301
|
+
if (canonical.length === 0
|
|
302
|
+
|| canonical.length > CONNECTOR_RUN_IDENTIFIER_MAX_LENGTH
|
|
303
|
+
|| !/^[a-z0-9][a-z0-9._:@/-]*$/.test(canonical)) {
|
|
304
|
+
throw new ConnectorServiceError('CONNECTOR_RATE_LIMITED', `${label} must be a non-empty canonical identifier`, 400, { field: label, maxLength: CONNECTOR_RUN_IDENTIFIER_MAX_LENGTH });
|
|
305
|
+
}
|
|
306
|
+
return canonical;
|
|
307
|
+
}
|
|
308
|
+
function connectorRunLimitKey(context) {
|
|
309
|
+
const scopeId = canonicalRunIdentifier(context.scopeId, 'scopeId');
|
|
310
|
+
const sessionId = canonicalRunIdentifier(context.sessionId ?? `${context.purpose ?? 'agent_preview'}:no-session-id`, 'sessionId');
|
|
311
|
+
return `${scopeId}\0${sessionId}`;
|
|
312
|
+
}
|
|
313
|
+
function normalizeExecuteIdentifier(value, field) {
|
|
314
|
+
if (typeof value !== 'string') {
|
|
315
|
+
throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', `${field} must be a string`, 400, { field, maxLength: CONNECTOR_EXECUTE_IDENTIFIER_MAX_LENGTH });
|
|
316
|
+
}
|
|
317
|
+
const normalized = value.normalize('NFKC').trim();
|
|
318
|
+
if (normalized.length === 0
|
|
319
|
+
|| normalized.length > CONNECTOR_EXECUTE_IDENTIFIER_MAX_LENGTH
|
|
320
|
+
|| /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
321
|
+
throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', `${field} must be a bounded identifier`, 400, { field, maxLength: CONNECTOR_EXECUTE_IDENTIFIER_MAX_LENGTH });
|
|
322
|
+
}
|
|
323
|
+
return normalized;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Detaches and bounds an execute request before any provider or account I/O.
|
|
327
|
+
*
|
|
328
|
+
* @returns A normalized request whose input is a finite, acyclic JSON object.
|
|
329
|
+
* @throws {ConnectorServiceError} With a stable 400 contract for malformed or
|
|
330
|
+
* oversized identifiers, roots, structures, strings, or serialized payloads.
|
|
331
|
+
*
|
|
332
|
+
* @complexity Time: O(n), where n is the bounded input graph and serialized
|
|
333
|
+
* byte length. Space: O(n) for the detached JSON value and serialization.
|
|
334
|
+
* @overallScore 100/100
|
|
335
|
+
*/
|
|
336
|
+
function normalizeConnectorExecuteRequest(request) {
|
|
337
|
+
const connectorId = normalizeExecuteIdentifier(request.connectorId, 'connectorId');
|
|
338
|
+
const toolName = normalizeExecuteIdentifier(request.toolName, 'toolName');
|
|
339
|
+
let boundedInput;
|
|
340
|
+
try {
|
|
341
|
+
boundedInput = toStructurallyBoundedJsonValue(request.input);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', 'connector input exceeds structural safety limits', 400, { connectorId, toolName });
|
|
345
|
+
}
|
|
346
|
+
if (boundedInput === null || typeof boundedInput !== 'object' || Array.isArray(boundedInput)) {
|
|
347
|
+
throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', 'connector input must be a JSON object', 400, { connectorId, toolName });
|
|
348
|
+
}
|
|
349
|
+
const serializedBytes = Buffer.byteLength(JSON.stringify(boundedInput), 'utf8');
|
|
350
|
+
if (serializedBytes > CONNECTOR_EXECUTE_INPUT_MAX_BYTES) {
|
|
351
|
+
throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', 'connector input exceeds the serialized byte limit', 400, {
|
|
352
|
+
connectorId,
|
|
353
|
+
toolName,
|
|
354
|
+
maxSerializedBytes: CONNECTOR_EXECUTE_INPUT_MAX_BYTES,
|
|
355
|
+
serializedBytes,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
connectorId,
|
|
360
|
+
toolName,
|
|
361
|
+
input: boundedInput,
|
|
362
|
+
...(request.expectedAccountLabel === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { expectedAccountLabel: request.expectedAccountLabel }),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function aggregateCatalogHydrationError() {
|
|
368
|
+
return new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'aggregate Composio catalog hydration is unsupported; hydrate one connector at a time', 400);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Application service that coordinates the Composio provider, connection
|
|
372
|
+
* status, durable credentials, safety policy, and execution limits.
|
|
373
|
+
*/
|
|
374
|
+
export class ComposioConnectorService {
|
|
375
|
+
runLimits = new Map();
|
|
376
|
+
provider;
|
|
377
|
+
statusService;
|
|
378
|
+
now;
|
|
379
|
+
constructor(options) {
|
|
380
|
+
this.provider = options.provider;
|
|
381
|
+
this.statusService = options.statusService ?? new ConnectorStatusService(options.now === undefined ? {} : { now: options.now });
|
|
382
|
+
this.now = options.now ?? Date.now;
|
|
383
|
+
}
|
|
384
|
+
setCredentialStore(credentialStore) {
|
|
385
|
+
this.statusService.setCredentialStore(credentialStore);
|
|
386
|
+
}
|
|
387
|
+
deleteCredentialsByProvider(provider) {
|
|
388
|
+
this.statusService.deleteCredentialsByProvider(provider);
|
|
389
|
+
}
|
|
390
|
+
async listDefinitions(signal) {
|
|
391
|
+
return this.provider.listDefinitions(signal);
|
|
392
|
+
}
|
|
393
|
+
async listHydratedDefinitions(signal) {
|
|
394
|
+
void signal;
|
|
395
|
+
throw aggregateCatalogHydrationError();
|
|
396
|
+
}
|
|
397
|
+
listFastDefinitions() {
|
|
398
|
+
return this.provider.getFastDefinitions();
|
|
399
|
+
}
|
|
400
|
+
getFastDefinition(connectorId) {
|
|
401
|
+
return this.listFastDefinitions().find((definition) => definition.id === connectorId);
|
|
402
|
+
}
|
|
403
|
+
async getDefinition(connectorId, signal) {
|
|
404
|
+
return this.provider.getDefinition(connectorId, signal);
|
|
405
|
+
}
|
|
406
|
+
async getHydratedDefinition(connectorId, signal) {
|
|
407
|
+
return this.provider.getHydratedDefinition(connectorId, signal, {
|
|
408
|
+
requireCurrentTools: true,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
async getPreviewDefinition(connectorId, options) {
|
|
412
|
+
return this.provider.getPreviewDefinition(connectorId, options);
|
|
413
|
+
}
|
|
414
|
+
getStatus(definition) {
|
|
415
|
+
const status = this.statusService.getStatus(definition);
|
|
416
|
+
if (definition.authentication === 'composio'
|
|
417
|
+
&& status.status === 'connected'
|
|
418
|
+
&& this.getCredential(definition.id) === undefined) {
|
|
419
|
+
return { status: 'available' };
|
|
420
|
+
}
|
|
421
|
+
return status;
|
|
422
|
+
}
|
|
423
|
+
getCredential(connectorId) {
|
|
424
|
+
const credential = this.statusService.getCredential(connectorId);
|
|
425
|
+
const definition = this.getFastDefinition(connectorId);
|
|
426
|
+
if (definition?.authentication === 'composio'
|
|
427
|
+
&& !this.provider.credentialMatchesDefinition(definition, credential?.credentials)) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
return credential;
|
|
431
|
+
}
|
|
432
|
+
async listConnectors(signal) {
|
|
433
|
+
return this.listFastDefinitions().map((definition) => this.toDetail(definition));
|
|
434
|
+
}
|
|
435
|
+
listConnectorStatuses() {
|
|
436
|
+
return {
|
|
437
|
+
...this.statusService.listStatuses(),
|
|
438
|
+
...Object.fromEntries(this.listFastDefinitions().map((definition) => [definition.id, this.getStatus(definition)])),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async listConnectorDiscovery(options = {}) {
|
|
442
|
+
if (options.refresh)
|
|
443
|
+
this.provider.clearDiscoveryCache();
|
|
444
|
+
let definitions;
|
|
445
|
+
if (options.hydrateTools) {
|
|
446
|
+
throw aggregateCatalogHydrationError();
|
|
447
|
+
}
|
|
448
|
+
else if (options.refresh) {
|
|
449
|
+
definitions = await this.provider.refreshCatalog(options.signal);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
definitions = await this.listDefinitions(options.signal);
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
connectors: definitions.map((definition) => this.toDetail(definition)),
|
|
456
|
+
meta: {
|
|
457
|
+
provider: 'composio',
|
|
458
|
+
...(options.refresh ? { refreshRequested: true } : {}),
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
async getConnector(connectorId, signal) {
|
|
463
|
+
const definition = await this.getDefinition(connectorId, signal);
|
|
464
|
+
if (!definition) {
|
|
465
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
466
|
+
}
|
|
467
|
+
return this.toDetail(definition);
|
|
468
|
+
}
|
|
469
|
+
async getHydratedConnector(connectorId, signal) {
|
|
470
|
+
const definition = await this.getHydratedDefinition(connectorId, signal);
|
|
471
|
+
if (!definition) {
|
|
472
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
473
|
+
}
|
|
474
|
+
return this.toDetail(definition);
|
|
475
|
+
}
|
|
476
|
+
async getPreviewConnector(connectorId, options) {
|
|
477
|
+
const definition = await this.getPreviewDefinition(connectorId, options);
|
|
478
|
+
if (!definition) {
|
|
479
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
480
|
+
}
|
|
481
|
+
return this.toDetail(definition);
|
|
482
|
+
}
|
|
483
|
+
async prepareAuthConfigs(connectorIds, signal) {
|
|
484
|
+
const results = {};
|
|
485
|
+
const uniqueConnectorIds = [...new Set(connectorIds.map((id) => id.trim()).filter(Boolean))];
|
|
486
|
+
if (uniqueConnectorIds.length > COMPOSIO_AUTH_CONFIG_PREPARE_LIMIT) {
|
|
487
|
+
throw new ConnectorServiceError('CONNECTOR_RATE_LIMITED', 'too many connector auth configs requested in one batch', 413, {
|
|
488
|
+
requested: uniqueConnectorIds.length,
|
|
489
|
+
maximum: COMPOSIO_AUTH_CONFIG_PREPARE_LIMIT,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
await mapWithConcurrency(uniqueConnectorIds, 8, async (connectorId) => {
|
|
493
|
+
const definition = this.getFastDefinition(connectorId) ?? await this.getDefinition(connectorId, signal);
|
|
494
|
+
if (!definition) {
|
|
495
|
+
results[connectorId] = { status: 'error', message: 'connector not found' };
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (definition.authentication !== 'composio') {
|
|
499
|
+
results[connectorId] = { status: 'error', message: 'connector is not backed by Composio' };
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
results[connectorId] = await this.provider.prepareAuthConfig(definition, signal);
|
|
503
|
+
});
|
|
504
|
+
return { results };
|
|
505
|
+
}
|
|
506
|
+
async connect(connectorId, options = {}) {
|
|
507
|
+
const definition = this.getFastDefinition(connectorId) ?? await this.getDefinition(connectorId, options.signal);
|
|
508
|
+
if (!definition) {
|
|
509
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
510
|
+
}
|
|
511
|
+
let auth;
|
|
512
|
+
let detailDefinition = definition;
|
|
513
|
+
if (definition.authentication === 'composio' && options.credentials !== undefined) {
|
|
514
|
+
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio credentials cannot be supplied directly; complete the validated OAuth flow', 400, { connectorId });
|
|
515
|
+
}
|
|
516
|
+
if (definition.authentication === 'composio') {
|
|
517
|
+
if (!options.callbackUrl) {
|
|
518
|
+
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'callbackUrl is required for Composio connectors', 400, { connectorId });
|
|
519
|
+
}
|
|
520
|
+
auth = await this.provider.connect(definition, options.callbackUrl, options.signal);
|
|
521
|
+
if (auth.kind === 'redirect_required' || auth.kind === 'pending') {
|
|
522
|
+
return { connector: this.toDetail(detailDefinition), auth: publicComposioAuthStart(auth) };
|
|
523
|
+
}
|
|
524
|
+
if (auth.credentials === undefined
|
|
525
|
+
|| !this.provider.credentialMatchesDefinition(definition, auth.credentials)) {
|
|
526
|
+
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'Composio reported a connected account without validated credential evidence', 502, { connectorId });
|
|
527
|
+
}
|
|
528
|
+
if (auth.credentials !== undefined) {
|
|
529
|
+
options = { ...options, ...(auth.accountLabel === undefined ? {} : { accountLabel: auth.accountLabel }), credentials: auth.credentials };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const status = this.statusService.connect(detailDefinition, options.accountLabel, options.credentials);
|
|
533
|
+
if (status.status === 'disabled') {
|
|
534
|
+
throw new ConnectorServiceError('CONNECTOR_DISABLED', 'connector is disabled', 403);
|
|
535
|
+
}
|
|
536
|
+
return { connector: this.toDetail(detailDefinition), ...(auth === undefined ? {} : { auth: publicComposioAuthStart(auth) }) };
|
|
537
|
+
}
|
|
538
|
+
async disconnect(connectorId, signal) {
|
|
539
|
+
const definition = this.getFastDefinition(connectorId) ?? await this.getDefinition(connectorId, signal);
|
|
540
|
+
if (!definition) {
|
|
541
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
542
|
+
}
|
|
543
|
+
if (definition.authentication === 'composio') {
|
|
544
|
+
await this.provider.disconnect(definition, this.getCredential(connectorId)?.credentials, signal);
|
|
545
|
+
}
|
|
546
|
+
this.statusService.disconnect(definition);
|
|
547
|
+
return this.toDetail(definition);
|
|
548
|
+
}
|
|
549
|
+
async cancelPendingAuthorization(connectorId) {
|
|
550
|
+
const definition = this.getFastDefinition(connectorId) ?? await this.getDefinition(connectorId);
|
|
551
|
+
if (!definition) {
|
|
552
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
553
|
+
}
|
|
554
|
+
if (definition.authentication === 'composio') {
|
|
555
|
+
this.provider.cancelPendingConnections(connectorId);
|
|
556
|
+
}
|
|
557
|
+
return this.toDetail(definition);
|
|
558
|
+
}
|
|
559
|
+
async completeComposioConnection(input) {
|
|
560
|
+
const definition = await this.getDefinition(input.connectorId, input.signal);
|
|
561
|
+
if (!definition) {
|
|
562
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
563
|
+
}
|
|
564
|
+
if (definition.authentication !== 'composio') {
|
|
565
|
+
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'connector is not backed by Composio', 400, { connectorId: input.connectorId });
|
|
566
|
+
}
|
|
567
|
+
const completed = await this.provider.completeConnection({ definition, state: input.state, ...(input.providerConnectionId === undefined ? {} : { providerConnectionId: input.providerConnectionId }), ...(input.status === undefined ? {} : { status: input.status }), ...(input.signal === undefined ? {} : { signal: input.signal }) });
|
|
568
|
+
this.statusService.connect(definition, completed.accountLabel, completed.credentials);
|
|
569
|
+
return this.toDetail(definition);
|
|
570
|
+
}
|
|
571
|
+
async execute(request, context) {
|
|
572
|
+
const normalizedRequest = normalizeConnectorExecuteRequest(request);
|
|
573
|
+
// Context identifiers are also public request material. Validate their
|
|
574
|
+
// canonical shape before current-schema hydration performs provider I/O.
|
|
575
|
+
connectorRunLimitKey(context);
|
|
576
|
+
// Execution always resolves a fresh hydrated definition. Persisted catalog
|
|
577
|
+
// data is a display/startup cache and is never an authority for side effects.
|
|
578
|
+
const definition = await this.getHydratedDefinition(normalizedRequest.connectorId, context.signal);
|
|
579
|
+
if (!definition) {
|
|
580
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_FOUND', 'connector not found', 404);
|
|
581
|
+
}
|
|
582
|
+
const connector = this.toDetail(definition);
|
|
583
|
+
if (connector.status === 'disabled') {
|
|
584
|
+
throw new ConnectorServiceError('CONNECTOR_DISABLED', 'connector is disabled', 403);
|
|
585
|
+
}
|
|
586
|
+
if (connector.status !== 'connected') {
|
|
587
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_CONNECTED', 'connector is not connected', 403, {
|
|
588
|
+
connectorId: normalizedRequest.connectorId,
|
|
589
|
+
status: connector.status,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (normalizedRequest.expectedAccountLabel !== undefined
|
|
593
|
+
&& connector.accountLabel !== normalizedRequest.expectedAccountLabel) {
|
|
594
|
+
throw new ConnectorServiceError('CONNECTOR_NOT_CONNECTED', 'connector account changed since refresh approval', 409, {
|
|
595
|
+
connectorId: normalizedRequest.connectorId,
|
|
596
|
+
expectedAccountLabel: normalizedRequest.expectedAccountLabel,
|
|
597
|
+
currentAccountLabel: connector.accountLabel ?? null,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const tool = definition.allowedToolNames.includes(normalizedRequest.toolName)
|
|
601
|
+
? definition.tools.find((candidate) => candidate.name === normalizedRequest.toolName)
|
|
602
|
+
: undefined;
|
|
603
|
+
if (!tool) {
|
|
604
|
+
throw new ConnectorServiceError('CONNECTOR_TOOL_NOT_FOUND', 'connector tool is not allowed', 404, {
|
|
605
|
+
connectorId: normalizedRequest.connectorId,
|
|
606
|
+
toolName: normalizedRequest.toolName,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const runtimeSafety = runtimeSafetyForTool(tool);
|
|
610
|
+
const effectiveApproval = stricterApproval(
|
|
611
|
+
// An absent host policy is not evidence of auto-approval. This matches
|
|
612
|
+
// the provider's independent fail-closed execution boundary.
|
|
613
|
+
definition.minimumApproval ?? 'disabled', tool.safety.approval, runtimeSafety.approval);
|
|
614
|
+
if (effectiveApproval !== 'auto' || runtimeSafety.sideEffect !== 'read') {
|
|
615
|
+
throw new ConnectorServiceError('CONNECTOR_SAFETY_DENIED', 'connector tool is not auto-approved read-only by current safety policy', 403, {
|
|
616
|
+
connectorId: normalizedRequest.connectorId,
|
|
617
|
+
toolName: normalizedRequest.toolName,
|
|
618
|
+
approvalPolicy: effectiveApproval,
|
|
619
|
+
safety: { ...runtimeSafety },
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
if (tool.inputSchemaUnsupportedReason !== undefined)
|
|
624
|
+
throw new Error(tool.inputSchemaUnsupportedReason);
|
|
625
|
+
assertConnectorInputMatchesSchema(normalizedRequest.input, tool.inputSchemaJson);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
throw new ConnectorServiceError('CONNECTOR_INPUT_SCHEMA_MISMATCH', error.message, 400, {
|
|
629
|
+
connectorId: normalizedRequest.connectorId,
|
|
630
|
+
toolName: normalizedRequest.toolName,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
this.enforceRunLimits(context);
|
|
634
|
+
let providerOutput;
|
|
635
|
+
try {
|
|
636
|
+
providerOutput = await this.executeConnectorProviderTool(normalizedRequest, context, definition, tool);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (isConnectorAuthStaleError(error, normalizedRequest)) {
|
|
640
|
+
this.statusService.markAuthenticationExpired(definition, connectorAuthExpiredMessage(definition), connector.accountLabel);
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
const protectedOutput = protectConnectorOutput(providerOutput);
|
|
645
|
+
const output = protectedOutput.output;
|
|
646
|
+
const outputSummary = summarizeConnectorOutput(output);
|
|
647
|
+
return {
|
|
648
|
+
ok: true,
|
|
649
|
+
connectorId: normalizedRequest.connectorId,
|
|
650
|
+
...(connector.accountLabel === undefined ? {} : { accountLabel: connector.accountLabel }),
|
|
651
|
+
toolName: normalizedRequest.toolName,
|
|
652
|
+
safety: { ...runtimeSafety },
|
|
653
|
+
output,
|
|
654
|
+
...(outputSummary === undefined ? {} : { outputSummary }),
|
|
655
|
+
metadata: {
|
|
656
|
+
connectorId: normalizedRequest.connectorId,
|
|
657
|
+
toolName: normalizedRequest.toolName,
|
|
658
|
+
purpose: context.purpose ?? 'agent_preview',
|
|
659
|
+
outputSerializedBytes: protectedOutput.serializedBytes,
|
|
660
|
+
...(protectedOutput.redacted ? { redacted: true } : {}),
|
|
661
|
+
...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async executeConnectorProviderTool(request, context, resolvedDefinition, resolvedTool) {
|
|
666
|
+
const definition = resolvedDefinition ?? await this.getHydratedDefinition(request.connectorId, context.signal);
|
|
667
|
+
const tool = resolvedTool ?? definition?.tools.find((candidate) => candidate.name === request.toolName);
|
|
668
|
+
if (definition?.authentication === 'composio' && tool) {
|
|
669
|
+
return this.provider.execute(definition, tool, request.input, this.getCredential(request.connectorId)?.credentials, context.signal);
|
|
670
|
+
}
|
|
671
|
+
throw new ConnectorServiceError('CONNECTOR_EXECUTION_FAILED', 'connector provider is not implemented', 501, {
|
|
672
|
+
connectorId: request.connectorId,
|
|
673
|
+
toolName: request.toolName,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
enforceRunLimits(context) {
|
|
677
|
+
const now = this.now();
|
|
678
|
+
this.pruneRunLimits(now);
|
|
679
|
+
const key = connectorRunLimitKey(context);
|
|
680
|
+
const current = this.runLimits.get(key);
|
|
681
|
+
if (current === undefined && this.runLimits.size >= CONNECTOR_RUN_LIMIT_MAX_BUCKETS) {
|
|
682
|
+
throw new ConnectorServiceError('CONNECTOR_RATE_LIMITED', 'connector run-limit bucket capacity exceeded', 429, {
|
|
683
|
+
bucketLimit: CONNECTOR_RUN_LIMIT_MAX_BUCKETS,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
const state = current === undefined || now - current.windowStartedAt >= CONNECTOR_RUN_RATE_LIMIT_WINDOW_MS
|
|
687
|
+
? { windowStartedAt: now, lastSeenAt: now, windowCalls: 0, totalCalls: current?.totalCalls ?? 0 }
|
|
688
|
+
: current;
|
|
689
|
+
if (state.totalCalls >= CONNECTOR_RUN_TOTAL_CALL_LIMIT) {
|
|
690
|
+
throw new ConnectorServiceError('CONNECTOR_RATE_LIMITED', 'connector tool run call limit exceeded', 429, {
|
|
691
|
+
sessionId: context.sessionId ?? null,
|
|
692
|
+
totalCallLimit: CONNECTOR_RUN_TOTAL_CALL_LIMIT,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (state.windowCalls >= CONNECTOR_RUN_RATE_LIMIT_CALLS) {
|
|
696
|
+
throw new ConnectorServiceError('CONNECTOR_RATE_LIMITED', 'connector tool rate limit exceeded', 429, {
|
|
697
|
+
sessionId: context.sessionId ?? null,
|
|
698
|
+
rateLimit: CONNECTOR_RUN_RATE_LIMIT_CALLS,
|
|
699
|
+
windowMs: CONNECTOR_RUN_RATE_LIMIT_WINDOW_MS,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
state.windowCalls += 1;
|
|
703
|
+
state.totalCalls += 1;
|
|
704
|
+
state.lastSeenAt = now;
|
|
705
|
+
this.runLimits.set(key, state);
|
|
706
|
+
}
|
|
707
|
+
pruneRunLimits(now = this.now()) {
|
|
708
|
+
for (const [key, state] of this.runLimits.entries()) {
|
|
709
|
+
if (now - state.lastSeenAt >= CONNECTOR_RUN_LIMIT_TTL_MS)
|
|
710
|
+
this.runLimits.delete(key);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
toDetail(definition) {
|
|
714
|
+
const detail = connectorDefinitionToDetail(definition);
|
|
715
|
+
const status = this.getStatus(definition);
|
|
716
|
+
return {
|
|
717
|
+
...detail,
|
|
718
|
+
status: status.status,
|
|
719
|
+
...(status.accountLabel === undefined ? {} : { accountLabel: status.accountLabel }),
|
|
720
|
+
...(status.lastError === undefined ? {} : { lastError: status.lastError }),
|
|
721
|
+
auth: {
|
|
722
|
+
...detail.auth,
|
|
723
|
+
configured: detail.auth.configured || (definition.authentication === 'composio' && this.provider.isConfigured(definition)),
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function summarizeConnectorOutput(output) {
|
|
729
|
+
const maybeToolName = output.toolName;
|
|
730
|
+
if (typeof maybeToolName === 'string') {
|
|
731
|
+
const data = output.data !== null && typeof output.data === 'object' && !Array.isArray(output.data)
|
|
732
|
+
? output.data
|
|
733
|
+
: output;
|
|
734
|
+
if (typeof data.count === 'number')
|
|
735
|
+
return `${maybeToolName}: ${data.count} result${data.count === 1 ? '' : 's'}`;
|
|
736
|
+
if (typeof data.path === 'string')
|
|
737
|
+
return `${maybeToolName}: ${data.path}`;
|
|
738
|
+
if (typeof data.isRepository === 'boolean')
|
|
739
|
+
return `${maybeToolName}: ${data.isRepository ? 'repository found' : 'not a repository'}`;
|
|
740
|
+
return maybeToolName;
|
|
741
|
+
}
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
745
|
+
let nextIndex = 0;
|
|
746
|
+
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
747
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
748
|
+
while (nextIndex < items.length) {
|
|
749
|
+
const index = nextIndex;
|
|
750
|
+
nextIndex += 1;
|
|
751
|
+
await mapper(items[index]);
|
|
752
|
+
}
|
|
753
|
+
}));
|
|
754
|
+
}
|
|
755
|
+
function isErrno(error, code) {
|
|
756
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
|
|
757
|
+
}
|
|
758
|
+
//# sourceMappingURL=service.js.map
|