scripts/verify-composio-managed-gmail.mjs
Rama main · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { SqliteSettingsStore } from '../src/config/settings-store.ts';
import { SqliteConfigStore } from '../src/config/store.ts';
import { agentRoutineDeletion } from '../src/routines/store.ts';
import { MANAGED_CONNECTOR_CATALOG } from '../src/connections/catalog/index.ts';
import {
createManagedConnectionProviderRegistry,
invokeManagedConnectionCapability,
} from '../src/connections/managed.ts';
import { ComposioManagedConnectionProvider } from '../src/connections/providers/composio.ts';
import { ConnectionAccountService } from '../src/connections/store.ts';
import { SqliteUsageStore } from '../src/usage/store.ts';
import { MANAGED_CONNECTOR_CANARY_MANIFESTS } from '../src/connections/canary-manifests.ts';
const apiKey = requiredEnvironmentValue('COMPOSIO_API_KEY');
const accountRef = requiredEnvironmentValue('COMPOSIO_CONNECTED_ACCOUNT_ID');
const principalRef = requiredEnvironmentValue('COMPOSIO_USER_ID');
const connectorInput = process.env.COMPOSIO_CONNECTOR?.trim().toLowerCase() || 'gmail';
const accessLane = parseAccessLane(process.env.COMPOSIO_ACCESS_LANE);
const connector = MANAGED_CONNECTOR_CATALOG.list().find(
({ id, toolkit }) => id === connectorInput || toolkit === connectorInput,
);
if (!connector) throw new Error(`Unknown COMPOSIO_CONNECTOR ${JSON.stringify(connectorInput)}`);
const manifest = MANAGED_CONNECTOR_CANARY_MANIFESTS[connector.toolkit];
if (!manifest) throw new Error(`No safe canary manifest exists for ${connector.toolkit}`);
const laneCapabilities = connector.capabilities.filter(({ accessLane: capabilityLane }) =>
accessLane === 'read' ? capabilityLane === 'read' : capabilityLane === 'write');
const configuredCapability = process.env.COMPOSIO_CAPABILITY?.trim();
if (accessLane === 'write' && !configuredCapability) {
throw new Error('COMPOSIO_CAPABILITY is required for a write canary');
}
const capabilityId = configuredCapability || manifest.capability || laneCapabilities[0]?.id;
const capability = capabilityId && MANAGED_CONNECTOR_CATALOG.capability(capabilityId);
if (!capability || capability.connectorToolkit !== connector.toolkit ||
(accessLane === 'read' && capability.accessLane !== 'read')) {
throw new Error('COMPOSIO_CAPABILITY is not available for the selected connector/access lane');
}
const arguments_ = parseJsonObject('COMPOSIO_ARGUMENTS_JSON', manifest.arguments);
const resourceConstraints = parseJsonObject('COMPOSIO_RESOURCE_CONSTRAINTS_JSON', {});
if (!configuredCapability || capability.id === manifest.capability) {
requireManifestInputs(manifest, arguments_, resourceConstraints);
}
const dataModifying = capability.effect !== 'read';
if (dataModifying && process.env.COMPOSIO_ALLOW_WRITE_CANARY !== '1') {
throw new Error(
`Capability ${capability.id} can modify external state. `
+ 'Set COMPOSIO_ALLOW_WRITE_CANARY=1 only after reviewing COMPOSIO_ARGUMENTS_JSON.',
);
}
const workspaceId = 'T_COMPOSIO_VERIFY';
const agentId = 'agent_composio_verify';
const membershipId = 'membership_composio_verify';
const config = new SqliteConfigStore(':memory:', agentRoutineDeletion, { agents: [] });
const settings = new SqliteSettingsStore(':memory:');
const usage = new SqliteUsageStore(':memory:');
const provider = new ComposioManagedConnectionProvider({ apiKey });
const providers = createManagedConnectionProviderRegistry([provider]);
const service = new ConnectionAccountService({ config, settings, managedProviders: providers });
const principal = {
userId: 'user_composio_verify',
membershipId,
organizationId: 'organization_composio_verify',
role: 'owner',
authenticatorKind: 'test',
credentialId: 'credential_composio_verify',
correlationId: crypto.randomUUID(),
machine: false,
};
const identity = activeIdentity({ workspaceId, membershipId, principal });
try {
await config.createAgent({
id: agentId,
name: 'Composio verifier',
instructions: `Verify the managed ${connector.label} connection.`,
enabled: true,
creatorMembershipId: membershipId,
editPolicy: 'creator_and_admins',
skills: [],
mcpServers: [],
apiConnections: [],
repositories: [],
});
await config.ensureWorkspaceInstallation({
workspaceId,
transportMode: 'direct',
defaultAgentId: agentId,
});
const account = await service.create({
principal,
workspaceId,
ownerKind: 'member',
providerId: connector.providerId,
label: `Live Composio ${connector.label}`,
policy: {
kind: 'managed',
adapterId: 'composio',
toolkit: connector.toolkit,
principalRef,
accountRef,
allowedCapabilities: [capability.id],
resourceConstraints,
},
});
await service.attach({
principal,
agentId,
connectionAccountId: account.id,
allowedCapabilities: [capability.id],
resourceConstraints: Object.fromEntries(
Object.entries(resourceConstraints).map(([key, selections]) => [
key,
Array.isArray(selections) ? selections.map(({ handle }) => handle) : [],
]),
),
});
if (dataModifying) {
console.error(JSON.stringify({
event: 'chickpea.composio_live_verifier.data_modifying_canary',
connector: connector.id,
capability: capability.id,
effectClass: capability.effect,
}));
}
const operationId = `composio-canary:${crypto.randomUUID()}`;
const result = await invokeManagedConnectionCapability({
config,
identity,
providers,
workspaceId,
agentId,
actorMembershipId: membershipId,
connectionAccountId: account.id,
capability: capability.id,
arguments: arguments_,
usage,
correlation: { operationId },
});
const summary = await usage.summarizeConnectorUsage({
from: 0,
to: Date.now() + 1,
workspaceId,
toolkit: connector.toolkit,
});
assert.equal(account.lifecycle, 'ready');
assert.equal(summary.attemptCount, 1);
assert.equal(summary.groups[0]?.outcome, 'success');
console.log(JSON.stringify({
ok: true,
connector: connector.id,
toolkit: connector.toolkit,
accessLane,
capability: capability.id,
effectClass: capability.effect,
dataModifyingCanary: dataModifying,
accountLifecycle: account.lifecycle,
providerVersion: result.providerVersion ?? null,
providerLogId: result.logId ?? null,
resultKeys: Object.keys(result.data).sort(),
resultBytes: new TextEncoder().encode(result.serializedData).byteLength,
measurement: {
outcome: summary.groups[0]?.outcome ?? null,
providerToolCallCount: summary.providerToolCallCount,
estimatedCostMicros: summary.estimatedCostMicros,
},
}, null, 2));
} finally {
config.close();
settings.close();
usage.close();
}
function requiredEnvironmentValue(name) {
const value = process.env[name]?.trim();
if (!value) throw new Error(`${name} is required`);
return value;
}
function parseAccessLane(value) {
const lane = value?.trim().toLowerCase() || 'read';
if (lane !== 'read' && lane !== 'write') {
throw new Error('COMPOSIO_ACCESS_LANE must be read or write');
}
return lane;
}
function parseJsonObject(name, fallback) {
const input = process.env[name]?.trim();
if (!input) return fallback;
let parsed;
try {
parsed = JSON.parse(input);
} catch {
throw new Error(`${name} must contain valid JSON`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${name} must contain a JSON object`);
}
return parsed;
}
function requireManifestInputs(manifest, arguments_, resourceConstraints) {
const missingArguments = manifest.requiredArguments.filter((name) => {
const value = arguments_[name];
return typeof value !== 'string' || !value.trim();
});
const missingResources = manifest.requiredResourceConstraints.filter((name) => {
const value = resourceConstraints[name];
return !Array.isArray(value) || value.length === 0;
});
if (missingArguments.length || missingResources.length) {
throw new Error([
missingArguments.length
? `COMPOSIO_ARGUMENTS_JSON must include: ${missingArguments.join(', ')}`
: '',
missingResources.length
? `COMPOSIO_RESOURCE_CONSTRAINTS_JSON must include: ${missingResources.join(', ')}`
: '',
].filter(Boolean).join('; '));
}
}
function activeIdentity({ workspaceId: teamId, membershipId: memberId, principal: actor }) {
const user = {
id: actor.userId,
slackTeamId: teamId,
slackUserId: 'U_COMPOSIO_VERIFY',
displayName: 'Composio verifier',
contactEmail: null,
createdAt: 1,
updatedAt: 1,
};
const membership = {
id: memberId,
organizationId: actor.organizationId,
userId: user.id,
role: 'owner',
status: 'active',
createdAt: 1,
updatedAt: 1,
};
const binding = {
id: 'binding_composio_verify',
provider: 'slack',
slackTeamId: teamId,
slackUserId: user.slackUserId,
userId: user.id,
organizationId: membership.organizationId,
membershipId: membership.id,
betterAuthUserId: null,
betterAuthMembershipId: null,
revision: 1,
createdAt: 1,
updatedAt: 1,
};
return {
getOrganization: async () => ({
id: actor.organizationId,
displayName: 'Composio verification',
slackTeamId: teamId,
authMode: 'slack_active',
canonicalAdminOrigin: 'https://chickpea.invalid',
createdAt: 1,
updatedAt: 1,
}),
getMembership: async () => membership,
getMembershipAccessOverlay: async () => undefined,
getUser: async () => user,
resolveSlackIdentity: async () => ({ user, membership, binding }),
};
}