mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Its tool gateway applies company access rules and approval controls to connected apps. > - MCP aggregators expose many apps through one provider endpoint. > - Each aggregator needs its own credential, catalog, grants, and lifecycle in Paperclip. > - This pull request adds independent Zapier, Arcade, Composio Connect, and Executor setup with a common Access → Connect layout. > - A default-off MCP aggregators flag lets operators opt in while we complete provider acceptance tests. > - Agents use the normal Paperclip permissions, Test screen, and gateway after setup. ## Linked Issues or Issue Description **Subsystem affected** Apps, connection setup, shared contracts, and the remote MCP gateway. **Problem or motivation** Aggregator endpoints need clear provider setup and correct MCP sessions. Generic setup does not explain each provider's authentication or broad execution tools. Provider approval must preserve the original execution instead of replaying a write. **Proposed solution** Add four separate connectors behind Settings → Experimental → MCP aggregators. Start with human and agent access, then connect the endpoint and read its tools. Enable tools by default. Use the existing Permissions and Test screens after setup. Keep legacy Composio API-key and child connections intact. **Alternatives considered** A shared connection for all providers would mix credentials and access rules. Separate provider-specific permission and test screens would duplicate existing controls. Vercel Connect is outside this change. **Roadmap alignment** Extends the existing MCP Tool Gateway & Apps capability and the Connected Apps roadmap area. This work was requested and reviewed by the maintainer. Related work: #11894, #12630, #12632, #12634, and #12906 concern the legacy Composio broker. #13102 also covers remote MCP pagination. This change preserves the broker path and adds initialized sessions, response matching, and provider resume handling alongside pagination. ## What Changed - Add branded setup and interactive Storybooks for Zapier, Arcade, Composio Connect, and Executor. Use the existing access controls and normal action tests. Do not request a connection name or action choices during setup. - Add the default-off `enableMcpAggregators` flag to settings, managed feature metadata, the catalog, and setup guards. Hidden connections keep running. Legacy Composio connections remain unchanged. - Reuse the vault, grants, policy, and catalog models. Support OAuth discovery, bearer tokens, custom headers, and credential-bearing URLs. Add no database tables or migrations. - Initialize and retain Streamable HTTP sessions by connection and effective credentials. Read paginated catalogs and match streaming responses to request IDs. - Classify unfamiliar aggregator tools as writes despite upstream read-only hints; only exact reviewed read capabilities enter the read-only allowlist. Legacy Composio child behavior is preserved. - Preserve provider authorization links and execution IDs. Support Executor approve/resume, decline, and cancel without automatic replay of uncertain writes. - Preserve Off and Ask first choices during refresh and reconnect. Allow new tools and retire removed tools. Keep agent access updates atomic and preserve an empty agent selection. - Document connector UX rules, provider branding sources, and live acceptance results. - Stabilize the existing Sentry release fixture after its repeated CI failure by reusing one module mock; production Sentry behavior is unchanged. ## Verification - Final head `d11781970`: [CI run](https://github.com/paperclipai/paperclip/actions/runs/35633534900) passed, including broad typecheck, test shards, build, and E2E. All 54 checks pass; 2 optional checks are skipped. Greptile is 5/5, Security Scan passes, and all review threads are resolved. - Passed 27 focused connector Vitest checks and 18 connector-only Storybook browser checks before the flag change. All 85 stories rendered at desktop and narrow widths. - Passed 5 connector lifecycle/server checks and 7 selected flag checks after adding the flag. The latter cover settings, managed defaults, cached catalog visibility, and all four setup routes. - Review fixes passed 13 risk/handoff/lifecycle checks, dedicated session-expiration and transport regressions, 13 selected connector/gateway CI cases, and 10 selected setup/reconnect UI cases. A real Composio connection-list call also succeeded through the refreshed UI on `9ab115f71`. - UI and server TypeScript checks passed. UI build, Storybook build, token gates, and diff whitespace checks passed during implementation. - Real browser and real Paperclip agent tests passed for Arcade, Composio, and Executor. Tested action permissions, denied agent access, reconnect, disconnect, and isolation. Tested Arcade catalog additions/removal and Executor provider approve/resume, decline, and cancel. - Zapier live acceptance is incomplete. Its dedicated provider server is configured, but its credential-copy dialog returned an empty clipboard through browser automation. No live Zapier action is claimed. - The three isolated Sentry release cases pass after the CI fixture fix. - Local verification is deliberately narrow at the maintainer's request. The full local suite, recursive typecheck, and repository-wide build were not run. CI provides the broader checks. ## Risks - Shared MCP transport changes affect other remote MCP servers. Protocol fixtures cover initialized sessions, streaming response matching, pagination, and isolation. - Broad execution tools remain broad permissions. The provider governs actions inside those tools. - Provider handoff links are retained briefly in memory. After a server restart, a one-time link may require reopening the provider dashboard. Paperclip does not replay the original call. - Zapier remains unproven live. Custom-header imports and self-hosted endpoints have fixture coverage rather than a separate live account for every variant. - Turning the experimental flag off hides setup; it does not revoke existing credentials or stop existing connections. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, shell execution, and browser automation. The exact runtime model ID and context-window size are not exposed in this session. A separate Anthropic-backed Paperclip agent performed live gateway acceptance tasks. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
1052 lines
48 KiB
TypeScript
1052 lines
48 KiB
TypeScript
import { isRemoteMcpConnectorMethod } from "../remote-mcp-connectors.js";
|
|
import { z } from "zod";
|
|
import {
|
|
CONNECTION_TOKEN_ISSUANCE_PATHS,
|
|
SECRET_PROJECTION_CLASSES,
|
|
TOOL_ACTION_REQUEST_STATUSES,
|
|
TOOL_APPLICATION_STATUSES,
|
|
TOOL_APPLICATION_TYPES,
|
|
TOOL_AUDIT_EVENT_TYPES,
|
|
TOOL_AUDIT_OUTCOMES,
|
|
TOOL_CATALOG_ENTRY_KINDS,
|
|
TOOL_CATALOG_ENTRY_STATUSES,
|
|
TOOL_CONNECTION_HEALTH_STATUSES,
|
|
TOOL_CONNECTION_KINDS,
|
|
TOOL_INVOCATION_APPROVAL_STATES,
|
|
TOOL_INVOCATION_STATUSES,
|
|
TOOL_MCP_GATEWAY_CONTEXT_SCOPE_TYPES,
|
|
TOOL_MCP_GATEWAY_DEFAULT_PROFILE_MODES,
|
|
TOOL_MCP_GATEWAY_STATUSES,
|
|
TOOL_MCP_GATEWAY_TOKEN_ACTIONS,
|
|
TOOL_MCP_GATEWAY_TOKEN_SUBJECT_TYPES,
|
|
TOOL_POLICY_DECISIONS,
|
|
TOOL_POLICY_TYPES,
|
|
TOOL_PROFILE_BINDING_TARGET_TYPES,
|
|
TOOL_PROFILE_DEFAULT_ACTIONS,
|
|
TOOL_PROFILE_ENTRY_EFFECTS,
|
|
TOOL_PROFILE_ENTRY_SELECTOR_TYPES,
|
|
TOOL_PROFILE_STATUSES,
|
|
TOOL_RATE_LIMIT_WINDOW_KINDS,
|
|
TOOL_RISK_LEVELS,
|
|
TOOL_RUNTIME_KINDS,
|
|
TOOL_RUNTIME_SLOT_STATUSES,
|
|
} from "../constants.js";
|
|
import {
|
|
checkMcpRemoteHeaderName,
|
|
checkMcpRemoteHeaderValue,
|
|
mcpRemoteHeaderNameFromConfigPath,
|
|
mcpRemoteHeaderRejectionMessage,
|
|
} from "../mcp-remote-headers.js";
|
|
import { jsonSchemaSchema } from "./plugin.js";
|
|
import { objectWithoutDefaults } from "./partial.js";
|
|
|
|
export const toolApplicationTypeSchema = z.enum(TOOL_APPLICATION_TYPES);
|
|
export const toolApplicationStatusSchema = z.enum(TOOL_APPLICATION_STATUSES);
|
|
export const toolConnectionTransportSchema = z.enum(["mcp_remote", "rest_api", "local_stdio", "chat_sdk"]);
|
|
export const toolConnectionPurposeSchema = z.enum(["tool", "channel"]);
|
|
export const toolConnectionAuthKindSchema = z.enum(["oauth", "api_key", "none"]);
|
|
export const toolConnectionOwnershipSchema = z.enum(["platform_shared", "platform_provisioned", "customer", "dcr"]);
|
|
export const toolConnectionCredentialSourceSchema = z.enum(["paperclip_vault", "vercel_connect"]);
|
|
export const vercelConnectCredentialSummarySchema = z.object({
|
|
provider: z.literal("vercel_connect"),
|
|
connectorId: z.string().trim().min(1).max(255),
|
|
connectorUid: z.string().trim().min(1).max(255),
|
|
service: z.string().trim().min(1).max(255),
|
|
connectorType: z.string().trim().min(1).max(255),
|
|
principalMode: z.enum(["app", "user"]),
|
|
headerName: z.string().trim().min(1).max(160),
|
|
headerPrefix: z.string().max(120).nullable().optional(),
|
|
scopes: z.array(z.string().trim().min(1).max(500)).max(50),
|
|
}).strict();
|
|
export const vercelConnectGrantSummarySchema = z.object({
|
|
provider: z.literal("vercel_connect"),
|
|
subjectType: z.enum(["app", "user"]),
|
|
installationId: z.string().trim().min(1).max(255).optional(),
|
|
tenantId: z.string().trim().min(1).max(255).optional(),
|
|
tokenId: z.string().trim().min(1).max(255).optional(),
|
|
expiresAt: z.string().datetime({ offset: true }).optional(),
|
|
lastVerifiedAt: z.string().datetime({ offset: true }).optional(),
|
|
}).strict();
|
|
export const connectionGrantKindSchema = z.enum(["organization", "user", "agent"]);
|
|
export const connectionGrantStatusSchema = z.enum(["active", "revoked", "expired", "needs_reauthorization"]);
|
|
export const createConnectionGrantDelegationSchema = z.object({
|
|
agentId: z.string().guid(),
|
|
});
|
|
export type CreateConnectionGrantDelegation = z.infer<typeof createConnectionGrantDelegationSchema>;
|
|
export const toolConnectionCredentialPolicySchema = z.enum(["shared", "per_user", "per_user_with_fallback", "per_agent"]);
|
|
export const toolConnectionStatusSchema = z.enum(["draft", "active", "disabled", "archived"]);
|
|
export const toolConnectionInstallTargetTypeSchema = z.enum(["company", "agent"]);
|
|
export const toolCredentialPlacementSchema = z.enum(["header", "env", "url"]);
|
|
export const toolConnectionKindSchema = z.enum(TOOL_CONNECTION_KINDS);
|
|
export const toolConnectionHealthStatusSchema = z.enum(TOOL_CONNECTION_HEALTH_STATUSES);
|
|
export const toolCatalogEntryKindSchema = z.enum(TOOL_CATALOG_ENTRY_KINDS);
|
|
export const toolCatalogEntryStatusSchema = z.enum(TOOL_CATALOG_ENTRY_STATUSES);
|
|
export const toolRiskLevelSchema = z.enum(TOOL_RISK_LEVELS);
|
|
export const toolProfileStatusSchema = z.enum(TOOL_PROFILE_STATUSES);
|
|
export const toolProfileDefaultActionSchema = z.enum(TOOL_PROFILE_DEFAULT_ACTIONS);
|
|
export const toolProfileEntrySelectorTypeSchema = z.enum(TOOL_PROFILE_ENTRY_SELECTOR_TYPES);
|
|
export const toolProfileEntryEffectSchema = z.enum(TOOL_PROFILE_ENTRY_EFFECTS);
|
|
export const toolProfileBindingTargetTypeSchema = z.enum(TOOL_PROFILE_BINDING_TARGET_TYPES);
|
|
export const toolPolicyTypeSchema = z.enum(TOOL_POLICY_TYPES);
|
|
export const toolPolicyDecisionSchema = z.enum(TOOL_POLICY_DECISIONS);
|
|
export const toolInvocationStatusSchema = z.enum(TOOL_INVOCATION_STATUSES);
|
|
export const toolInvocationApprovalStateSchema = z.enum(TOOL_INVOCATION_APPROVAL_STATES);
|
|
export const toolMcpGatewayStatusSchema = z.enum(TOOL_MCP_GATEWAY_STATUSES);
|
|
export const toolMcpGatewayDefaultProfileModeSchema = z.enum(TOOL_MCP_GATEWAY_DEFAULT_PROFILE_MODES);
|
|
export const toolMcpGatewayContextScopeTypeSchema = z.enum(TOOL_MCP_GATEWAY_CONTEXT_SCOPE_TYPES);
|
|
export const toolMcpGatewayTokenSubjectTypeSchema = z.enum(TOOL_MCP_GATEWAY_TOKEN_SUBJECT_TYPES);
|
|
export const toolMcpGatewayTokenActionSchema = z.enum(TOOL_MCP_GATEWAY_TOKEN_ACTIONS);
|
|
export const toolActionRequestStatusSchema = z.enum(TOOL_ACTION_REQUEST_STATUSES);
|
|
export const toolAuditEventTypeSchema = z.enum(TOOL_AUDIT_EVENT_TYPES);
|
|
export const toolAuditOutcomeSchema = z.enum(TOOL_AUDIT_OUTCOMES);
|
|
export const toolRuntimeKindSchema = z.enum(TOOL_RUNTIME_KINDS);
|
|
export const toolRuntimeSlotStatusSchema = z.enum(TOOL_RUNTIME_SLOT_STATUSES);
|
|
export const toolRateLimitWindowKindSchema = z.enum(TOOL_RATE_LIMIT_WINDOW_KINDS);
|
|
|
|
const safeKeyPattern = /^[a-z0-9][a-z0-9._:-]*$/i;
|
|
const sensitiveConfigKeyPattern =
|
|
/^(access[-_]?key([-_]?id)?|api[-_]?key|authorization|bearer|client[-_]?secret|credential|credentials|jwt|password|passwd|private[-_]?key|refresh[-_]?token|secret|secret[-_]?access[-_]?key|secret[-_]?key|session[-_]?token|token)$/i;
|
|
|
|
function rejectSensitiveConfigKeys(value: unknown, ctx: z.RefinementCtx, path: Array<string | number> = []) {
|
|
if (!value || typeof value !== "object") return;
|
|
if (Array.isArray(value)) {
|
|
value.forEach((entry, index) => rejectSensitiveConfigKeys(entry, ctx, [...path, index]));
|
|
return;
|
|
}
|
|
for (const [key, nested] of Object.entries(value)) {
|
|
if (sensitiveConfigKeyPattern.test(key)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: [...path, key],
|
|
message: `Tool access config cannot persist sensitive field: ${key}. Use credentialSecretRefs instead.`,
|
|
});
|
|
}
|
|
rejectSensitiveConfigKeys(nested, ctx, [...path, key]);
|
|
}
|
|
}
|
|
|
|
export const toolCredentialSecretRefSchema = z.object({
|
|
secretId: z.string().guid(),
|
|
versionSelector: z.union([z.literal("latest"), z.number().int().positive()]).optional(),
|
|
configPath: z.string().trim().min(1).max(200),
|
|
required: z.boolean().optional(),
|
|
label: z.string().trim().max(120).optional().nullable(),
|
|
projectionClass: z.enum(SECRET_PROJECTION_CLASSES).optional(),
|
|
projectionAllowlistKey: z.string().trim().min(1).max(160).optional().nullable(),
|
|
keyScope: z.string().trim().min(1).max(160).optional(),
|
|
expiresAt: z.string().datetime({ offset: true }).optional(),
|
|
});
|
|
|
|
export const mcpConnectionCredentialRefSchema = z.object({
|
|
name: z.string().trim().min(1).max(120),
|
|
secretId: z.string().guid(),
|
|
version: z.union([z.literal("latest"), z.number().int().positive()]).optional(),
|
|
placement: toolCredentialPlacementSchema,
|
|
key: z.string().trim().min(1).max(160),
|
|
prefix: z.string().max(120).nullable().optional(),
|
|
});
|
|
|
|
export const toolTransportConfigSchema = z.record(z.string(), z.unknown()).superRefine(rejectSensitiveConfigKeys);
|
|
|
|
export const toolRedactedValueSummarySchema = z.object({
|
|
summary: z.string().max(4000),
|
|
sizeBytes: z.number().int().min(0).optional().nullable(),
|
|
sha256: z.string().trim().regex(/^[a-f0-9]{64}$/i).optional().nullable(),
|
|
redactedFields: z.array(z.string().trim().min(1).max(200)).default([]).optional(),
|
|
artifactId: z.string().guid().optional().nullable(),
|
|
});
|
|
|
|
export const createToolApplicationSchema = z.object({
|
|
applicationKey: z.string().trim().min(1).max(160).regex(safeKeyPattern).optional(),
|
|
name: z.string().trim().min(1).max(160),
|
|
description: z.string().max(4000).optional().nullable(),
|
|
type: toolApplicationTypeSchema,
|
|
status: toolApplicationStatusSchema.optional(),
|
|
pluginId: z.string().guid().optional().nullable(),
|
|
ownerAgentId: z.string().guid().optional().nullable(),
|
|
ownerUserId: z.string().optional().nullable(),
|
|
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolApplication = z.infer<typeof createToolApplicationSchema>;
|
|
|
|
export const updateToolApplicationSchema = createToolApplicationSchema.partial().refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "At least one tool application field is required" },
|
|
);
|
|
|
|
export type UpdateToolApplication = z.infer<typeof updateToolApplicationSchema>;
|
|
|
|
export const createToolConnectionSchema = z.object({
|
|
applicationId: z.string().guid().optional(),
|
|
applicationName: z.string().trim().min(1).max(160).optional(),
|
|
name: z.string().trim().min(1).max(160),
|
|
connectionPurpose: toolConnectionPurposeSchema.default("tool"),
|
|
transport: toolConnectionTransportSchema.optional(),
|
|
authKind: toolConnectionAuthKindSchema.default("none"),
|
|
credentialPolicy: toolConnectionCredentialPolicySchema.optional(),
|
|
ownership: toolConnectionOwnershipSchema.default("customer"),
|
|
status: toolConnectionStatusSchema.optional(),
|
|
connectionKind: toolConnectionKindSchema.default("managed"),
|
|
config: toolTransportConfigSchema.optional(),
|
|
transportConfig: toolTransportConfigSchema.default({}),
|
|
credentialRefs: z.array(mcpConnectionCredentialRefSchema).optional(),
|
|
credentialSecretRefs: z.array(toolCredentialSecretRefSchema).default([]),
|
|
enabled: z.boolean().optional(),
|
|
});
|
|
|
|
export type CreateToolConnection = z.infer<typeof createToolConnectionSchema>;
|
|
|
|
export const updateToolConnectionSchema = objectWithoutDefaults(createToolConnectionSchema.omit({ applicationId: true })).partial().refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "At least one tool connection field is required" },
|
|
);
|
|
|
|
export type UpdateToolConnection = z.infer<typeof updateToolConnectionSchema>;
|
|
|
|
export const connectionGrantSchema = z.object({
|
|
id: z.string().guid(),
|
|
companyId: z.string().guid(),
|
|
connectionId: z.string().guid(),
|
|
kind: connectionGrantKindSchema,
|
|
subjectUserId: z.string().nullable(),
|
|
subjectAgentId: z.string().guid().nullable().optional(),
|
|
providerTenant: z.object({
|
|
name: z.string().trim().min(1).max(200).optional(),
|
|
externalId: z.string().trim().min(1).max(400).optional(),
|
|
oauth: z.object({
|
|
strategy: z.string().trim().min(1).max(100).optional(),
|
|
accessTokenExpiresAt: z.string().datetime().nullable().optional(),
|
|
scopes: z.array(z.string().trim().min(1).max(500)).max(20).optional(),
|
|
tokenType: z.string().trim().min(1).max(100).optional(),
|
|
refreshTokenExpiresAt: z.string().datetime().optional(),
|
|
refreshedAt: z.string().datetime().optional(),
|
|
}).optional(),
|
|
github: z.object({
|
|
userId: z.string().regex(/^[1-9][0-9]{0,30}$/),
|
|
login: z.string().trim().min(1).max(100),
|
|
avatarUrl: z.string().url().max(2000).optional(),
|
|
installationCount: z.number().int().nonnegative(),
|
|
repositoryCount: z.number().int().nonnegative(),
|
|
repositorySelection: z.enum(["all", "selected", "mixed", "none"]),
|
|
installationIds: z.array(z.string().regex(/^[1-9][0-9]{0,30}$/)).max(100),
|
|
installationOwnerLogins: z.array(z.string().trim().min(1).max(100)).max(100),
|
|
installationUrl: z.string().url().max(2000).optional(),
|
|
managementUrl: z.string().url().max(2000).optional(),
|
|
appSlug: z.string().regex(/^[a-z0-9-]{1,100}$/).optional(),
|
|
lastAccessRefreshAt: z.string().datetime().optional(),
|
|
lastWebhookAt: z.string().datetime().optional(),
|
|
webhookHealth: z.enum(["pending", "healthy", "unhealthy"]).optional(),
|
|
}).optional(),
|
|
}).nullable(),
|
|
credentialSecretRefs: z.array(toolCredentialSecretRefSchema),
|
|
externalCredential: vercelConnectGrantSummarySchema.nullable().optional(),
|
|
status: connectionGrantStatusSchema,
|
|
isDefault: z.boolean(),
|
|
createdByAgentId: z.string().guid().nullable(),
|
|
createdByUserId: z.string().nullable(),
|
|
revokedAt: z.coerce.date().nullable(),
|
|
revokedByAgentId: z.string().guid().nullable(),
|
|
revokedByUserId: z.string().nullable(),
|
|
lastUsedAt: z.coerce.date().nullable(),
|
|
createdAt: z.coerce.date(),
|
|
updatedAt: z.coerce.date(),
|
|
}).superRefine((grant, ctx) => {
|
|
const validSubject = (grant.kind === "user" && Boolean(grant.subjectUserId) && !grant.subjectAgentId)
|
|
|| (grant.kind === "agent" && Boolean(grant.subjectAgentId) && !grant.subjectUserId)
|
|
|| (grant.kind === "organization" && !grant.subjectUserId && !grant.subjectAgentId);
|
|
if (!validSubject) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["kind"], message: "User and agent grants require exactly their matching subject; organization grants cannot have a subject" });
|
|
}
|
|
if (grant.externalCredential && grant.credentialSecretRefs.length > 0) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["credentialSecretRefs"], message: "External grants cannot also contain Paperclip secret references" });
|
|
}
|
|
});
|
|
|
|
export const putToolConnectionInstallsSchema = z.object({
|
|
installs: z.array(z.object({
|
|
targetType: toolConnectionInstallTargetTypeSchema,
|
|
targetId: z.string().trim().min(1).max(200),
|
|
})).max(1000),
|
|
}).strict();
|
|
|
|
export type PutToolConnectionInstalls = z.infer<typeof putToolConnectionInstallsSchema>;
|
|
|
|
/**
|
|
* Audience replacement for an organization grant (PAP-17835). An empty array is
|
|
* the canonical encoding of "all organization members" — the UI never exposes
|
|
* "empty list" as the mental model, so the wire format carries the emptiness and
|
|
* the copy layer translates it.
|
|
*/
|
|
export const replaceConnectionGrantMembersSchema = z.object({
|
|
memberUserIds: z.array(z.string().trim().min(1).max(500)).max(1000),
|
|
}).strict();
|
|
|
|
export type ReplaceConnectionGrantMembersInput = z.infer<typeof replaceConnectionGrantMembersSchema>;
|
|
|
|
export const connectionTokenIssuancePathSchema = z.enum(CONNECTION_TOKEN_ISSUANCE_PATHS);
|
|
|
|
export const connectionTokenScopeSchema = z.union([
|
|
z.string().trim().min(1).max(500),
|
|
z.array(z.string().trim().min(1).max(240)).max(100),
|
|
]);
|
|
|
|
export const connectionTokenSubjectSchema = z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("app") }).strict(),
|
|
z.object({ type: z.literal("user"), userId: z.string().trim().min(1).max(500) }).strict(),
|
|
]);
|
|
|
|
export const connectionTokenRequestSchema = z.object({
|
|
subject: connectionTokenSubjectSchema.optional().default({ type: "app" }),
|
|
scope: connectionTokenScopeSchema.optional(),
|
|
requestedTtlSeconds: z.number().int().positive().max(86_400).optional(),
|
|
grantId: z.string().guid().optional(),
|
|
}).strict();
|
|
|
|
export const startConnectionAuthorizationSchema = z.object({
|
|
subjectUserId: z.string().trim().min(1).max(500),
|
|
scopes: z.array(z.string().trim().min(1).max(240)).max(100).optional(),
|
|
returnTo: z.string().trim().max(2000).optional(),
|
|
}).strict();
|
|
|
|
export type ConnectionTokenRequestInput = z.infer<typeof connectionTokenRequestSchema>;
|
|
|
|
const envKeyPattern = /^[A-Z_][A-Z0-9_]*$/i;
|
|
|
|
export const toolStdioTemplateToolSchema = z.object({
|
|
name: z.string().trim().min(1).max(240),
|
|
title: z.string().trim().max(240).optional().nullable(),
|
|
description: z.string().max(8000).optional().nullable(),
|
|
inputSchema: jsonSchemaSchema.optional().nullable(),
|
|
annotations: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export const createToolStdioCommandTemplateSchema = z.object({
|
|
templateId: z.string().trim().min(1).max(160).regex(safeKeyPattern),
|
|
name: z.string().trim().min(1).max(160),
|
|
description: z.string().max(4000).optional().nullable(),
|
|
command: z.string().trim().min(1).max(2000),
|
|
args: z.array(z.string().max(2000)).max(100).default([]),
|
|
envKeys: z.array(z.string().trim().min(1).max(160).regex(envKeyPattern)).max(200).default([]),
|
|
tools: z.array(toolStdioTemplateToolSchema).max(500).default([]),
|
|
});
|
|
|
|
export type CreateToolStdioCommandTemplate = z.infer<typeof createToolStdioCommandTemplateSchema>;
|
|
|
|
export const disableToolStdioCommandTemplateSchema = z.object({
|
|
reason: z.string().trim().max(1000).optional().nullable(),
|
|
});
|
|
|
|
export type DisableToolStdioCommandTemplate = z.infer<typeof disableToolStdioCommandTemplateSchema>;
|
|
|
|
/**
|
|
* How an operator says a generic remote MCP endpoint authenticates (PAP-17087).
|
|
*
|
|
* `auto` is the default and the only value the simple path sends: Paperclip
|
|
* probes the endpoint and branches on what it finds. The rest are the explicit
|
|
* choices behind "Advanced authentication", where the operator already knows.
|
|
*/
|
|
export const GENERIC_MCP_AUTH_MODES = ["auto", "none", "bearer", "custom_headers", "oauth"] as const;
|
|
|
|
export const genericMcpAuthModeSchema = z.enum(GENERIC_MCP_AUTH_MODES);
|
|
|
|
export type GenericMcpAuthMode = z.infer<typeof genericMcpAuthModeSchema>;
|
|
|
|
/**
|
|
* A preregistered OAuth client an operator pasted in because the authorization
|
|
* server supports neither CIMD nor dynamic registration. The secret is write-only:
|
|
* it becomes a Paperclip secret ref and is never read back.
|
|
*/
|
|
export const genericMcpOAuthClientSchema = z.object({
|
|
clientId: z.string().trim().min(1).max(4096),
|
|
clientSecret: z.string().min(1).max(16384).optional(),
|
|
}).strict();
|
|
|
|
export type GenericMcpOAuthClient = z.infer<typeof genericMcpOAuthClientSchema>;
|
|
|
|
/**
|
|
* Reject `headers.*` credential paths whose header name Paperclip refuses to
|
|
* send, and any value that could split the outbound request. This runs at the
|
|
* API boundary so both the guided wizard and normalized paste-config go through
|
|
* exactly one gate; the service re-checks when it projects the headers.
|
|
*/
|
|
function rejectUnsafeHeaderCredentials(
|
|
credentialValues: Record<string, string>,
|
|
ctx: z.RefinementCtx,
|
|
path: (string | number)[],
|
|
) {
|
|
for (const [configPath, value] of Object.entries(credentialValues)) {
|
|
const headerName = mcpRemoteHeaderNameFromConfigPath(configPath);
|
|
if (configPath.startsWith("headers.") && !headerName) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: [...path, configPath],
|
|
message: "Header names cannot be blank.",
|
|
});
|
|
continue;
|
|
}
|
|
if (!headerName) continue;
|
|
const nameCheck = checkMcpRemoteHeaderName(headerName);
|
|
if (!nameCheck.ok) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: [...path, configPath],
|
|
message: mcpRemoteHeaderRejectionMessage(headerName, nameCheck.reason!),
|
|
});
|
|
continue;
|
|
}
|
|
const valueCheck = checkMcpRemoteHeaderValue(value);
|
|
if (!valueCheck.ok) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: [...path, configPath],
|
|
message: mcpRemoteHeaderRejectionMessage(headerName, valueCheck.reason!),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export const connectToolAppSchema = z.object({
|
|
galleryKey: z.string().trim().min(1).max(120).optional(),
|
|
connectionMethodKey: z.string().trim().min(1).max(120).optional(),
|
|
link: z.string().trim().url().max(2000).optional(),
|
|
name: z.string().trim().min(1).max(160).optional(),
|
|
credentialValues: z.record(z.string().trim().min(1).max(200), z.string().min(1)).optional(),
|
|
configValues: z.record(z.string().trim().min(1).max(200), z.unknown()).optional(),
|
|
applicationId: z.string().guid().optional(),
|
|
/** Pending connection request this setup should resolve after authorization. */
|
|
interactionId: z.string().uuid().optional(),
|
|
/** Exact draft to continue after an interrupted setup. */
|
|
resumeConnectionId: z.string().guid().optional(),
|
|
/** Exact configured connection to reauthorize without replacing its identity. */
|
|
reconnectConnectionId: z.string().guid().optional(),
|
|
saveDraft: z.boolean().optional(),
|
|
authMode: genericMcpAuthModeSchema.optional(),
|
|
oauthClient: genericMcpOAuthClientSchema.optional(),
|
|
credentialSource: z.enum(["paperclip_vault", "vercel_connect"]).optional(),
|
|
vercelConnect: z.object({ connector: z.string().trim().min(1).max(255) }).strict().optional(),
|
|
/**
|
|
* Which identity this credential becomes (PAP-17835). `user` means "Just me":
|
|
* the credential is committed to the caller's own personal grant and never to
|
|
* the connection row's shared secret refs or the default organization grant.
|
|
* Omitted keeps the historical shared-credential behaviour.
|
|
*/
|
|
grantKind: connectionGrantKindSchema.optional(),
|
|
/** Same-company agent that owns a dedicated provider identity. */
|
|
subjectAgentId: z.string().guid().optional(),
|
|
}).superRefine((value, ctx) => {
|
|
if (value.configValues) rejectSensitiveConfigKeys(value.configValues, ctx, ["configValues"]);
|
|
if (value.credentialValues) rejectUnsafeHeaderCredentials(value.credentialValues, ctx, ["credentialValues"]);
|
|
if ((value.grantKind === "agent") !== Boolean(value.subjectAgentId)) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["subjectAgentId"], message: "subjectAgentId is required exactly for an agent grant" });
|
|
}
|
|
if (value.saveDraft && !isRemoteMcpConnectorMethod(value.galleryKey, value.connectionMethodKey)) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["saveDraft"], message: "Draft saving requires a remote MCP connector" });
|
|
}
|
|
if (value.authMode && value.galleryKey && !isRemoteMcpConnectorMethod(value.galleryKey, value.connectionMethodKey)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["authMode"],
|
|
message: "Authentication mode selection applies to a pasted URL, not a gallery app",
|
|
});
|
|
}
|
|
if (value.resumeConnectionId && value.reconnectConnectionId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["reconnectConnectionId"], message: "Choose resume or reconnect, not both" });
|
|
}
|
|
if (value.resumeConnectionId && !value.galleryKey) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["resumeConnectionId"],
|
|
message: "Only gallery app setup can resume a draft connection",
|
|
});
|
|
}
|
|
const source = value.credentialSource ?? "paperclip_vault";
|
|
if (source === "vercel_connect") {
|
|
if (!value.vercelConnect) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["vercelConnect"], message: "A Vercel connector UID is required" });
|
|
}
|
|
if (value.credentialValues && Object.keys(value.credentialValues).length > 0) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["credentialValues"], message: "Vercel-backed connections cannot include provider credentials" });
|
|
}
|
|
if (value.oauthClient) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["oauthClient"], message: "Vercel-backed connections cannot include OAuth client credentials" });
|
|
}
|
|
} else if (value.vercelConnect) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["vercelConnect"], message: "Vercel connector metadata requires the vercel_connect credential source" });
|
|
}
|
|
}).refine(
|
|
(value) => Boolean(value.galleryKey) || Boolean(value.link),
|
|
{ message: "Provide a galleryKey or link" },
|
|
);
|
|
|
|
export type ConnectToolApp = z.infer<typeof connectToolAppSchema>;
|
|
|
|
export const reconnectToolAppSchema = z.object({
|
|
credentialValues: z.record(z.string().trim().min(1).max(200), z.string().min(1)),
|
|
});
|
|
|
|
export type ReconnectToolApp = z.infer<typeof reconnectToolAppSchema>;
|
|
|
|
export const finishToolAppSchema = z.object({
|
|
/** Task setup adds access while preserving existing assignments and action policies. */
|
|
preserveExistingAccess: z.boolean().optional(),
|
|
enabledCatalogEntryIds: z.array(z.string().guid()).max(500).default([]),
|
|
askFirstCatalogEntryIds: z.array(z.string().guid()).max(500).default([]),
|
|
reviewedCatalogEntryIds: z.array(z.string().guid()).max(500).optional(),
|
|
access: z.union([
|
|
z.literal("all_agents"),
|
|
// Choosing specific agents may intentionally leave the connection unassigned.
|
|
z.object({ agentIds: z.array(z.string().guid()).max(250) }),
|
|
]),
|
|
});
|
|
|
|
export type FinishToolApp = z.infer<typeof finishToolAppSchema>;
|
|
|
|
/**
|
|
* Legacy promotion boundary retained for connection-intent flows. Interactive
|
|
* app setup chooses identity before OAuth and writes the token directly to that
|
|
* credential scope.
|
|
*/
|
|
export const finalizeOAuthAccessSchema = z.object({
|
|
grantKind: connectionGrantKindSchema,
|
|
}).strict();
|
|
|
|
export type FinalizeOAuthAccess = z.infer<typeof finalizeOAuthAccessSchema>;
|
|
|
|
export const startToolOAuthSchema = z.object({
|
|
asCurrentUser: z.boolean().optional(),
|
|
asAgentId: z.string().uuid().optional(),
|
|
interactionId: z.string().uuid().optional(),
|
|
}).strict().superRefine((value, ctx) => {
|
|
if (value.asCurrentUser && value.asAgentId) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["asAgentId"], message: "Choose either the current user or one dedicated agent" });
|
|
}
|
|
}).default({});
|
|
|
|
export type StartToolOAuth = z.infer<typeof startToolOAuthSchema>;
|
|
|
|
export const upsertToolCatalogEntrySchema = z.object({
|
|
applicationId: z.string().guid(),
|
|
connectionId: z.string().guid(),
|
|
entryKind: toolCatalogEntryKindSchema.default("tool"),
|
|
toolName: z.string().trim().min(1).max(240),
|
|
title: z.string().trim().max(240).optional().nullable(),
|
|
description: z.string().max(8000).optional().nullable(),
|
|
inputSchema: jsonSchemaSchema.optional().nullable(),
|
|
outputSchema: jsonSchemaSchema.optional().nullable(),
|
|
annotations: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
riskLevel: toolRiskLevelSchema.default("medium"),
|
|
isReadOnly: z.boolean().default(false),
|
|
isWrite: z.boolean().default(false),
|
|
isDestructive: z.boolean().default(false),
|
|
status: toolCatalogEntryStatusSchema.default("active"),
|
|
version: z.string().trim().max(200).optional().nullable(),
|
|
schemaHash: z.string().trim().max(128).optional().nullable(),
|
|
});
|
|
|
|
export type UpsertToolCatalogEntry = z.infer<typeof upsertToolCatalogEntrySchema>;
|
|
|
|
export const createToolProfileSchema = z.object({
|
|
profileKey: z.string().trim().min(1).max(160).regex(safeKeyPattern),
|
|
name: z.string().trim().min(1).max(160),
|
|
description: z.string().max(4000).optional().nullable(),
|
|
status: toolProfileStatusSchema.default("active"),
|
|
defaultAction: toolProfileDefaultActionSchema.default("deny"),
|
|
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolProfile = z.infer<typeof createToolProfileSchema>;
|
|
|
|
export const updateToolProfileSchema = objectWithoutDefaults(createToolProfileSchema).partial().refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "At least one tool profile field is required" },
|
|
);
|
|
|
|
export type UpdateToolProfile = z.infer<typeof updateToolProfileSchema>;
|
|
|
|
export const createToolProfileEntrySchema = z.object({
|
|
profileId: z.string().guid(),
|
|
selectorType: toolProfileEntrySelectorTypeSchema,
|
|
effect: toolProfileEntryEffectSchema.default("include"),
|
|
applicationId: z.string().guid().optional().nullable(),
|
|
connectionId: z.string().guid().optional().nullable(),
|
|
catalogEntryId: z.string().guid().optional().nullable(),
|
|
toolName: z.string().trim().min(1).max(240).optional().nullable(),
|
|
riskLevel: toolRiskLevelSchema.optional().nullable(),
|
|
conditions: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolProfileEntry = z.infer<typeof createToolProfileEntrySchema>;
|
|
|
|
export const createToolProfileEntryForProfileSchema = createToolProfileEntrySchema.omit({ profileId: true });
|
|
|
|
export type CreateToolProfileEntryForProfile = z.infer<typeof createToolProfileEntryForProfileSchema>;
|
|
|
|
export const updateToolProfileEntrySchema = objectWithoutDefaults(createToolProfileEntryForProfileSchema).partial().refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "At least one tool profile entry field is required" },
|
|
);
|
|
|
|
export type UpdateToolProfileEntry = z.infer<typeof updateToolProfileEntrySchema>;
|
|
|
|
export const createToolProfileWithEntriesSchema = createToolProfileSchema.extend({
|
|
entries: z.array(createToolProfileEntryForProfileSchema).max(250).optional(),
|
|
});
|
|
|
|
export type CreateToolProfileWithEntries = z.infer<typeof createToolProfileWithEntriesSchema>;
|
|
|
|
export const duplicateToolProfileSchema = z.object({
|
|
name: z.string().trim().min(1).max(160),
|
|
includeAssignments: z.boolean().default(false),
|
|
});
|
|
|
|
export type DuplicateToolProfile = z.infer<typeof duplicateToolProfileSchema>;
|
|
|
|
export const updateToolProfileWithEntriesSchema = objectWithoutDefaults(createToolProfileSchema).partial().extend({
|
|
entries: z.array(createToolProfileEntryForProfileSchema).max(250).optional(),
|
|
}).refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "At least one tool profile field is required" },
|
|
);
|
|
|
|
export type UpdateToolProfileWithEntries = z.infer<typeof updateToolProfileWithEntriesSchema>;
|
|
|
|
export const reviewToolProfileNewToolsSchema = z.object({
|
|
decisions: z.array(z.object({
|
|
catalogEntryId: z.string().guid(),
|
|
decision: z.enum(["allow", "keep_blocked"]),
|
|
})).min(1).max(250),
|
|
});
|
|
|
|
export type ReviewToolProfileNewTools = z.infer<typeof reviewToolProfileNewToolsSchema>;
|
|
|
|
export const deleteToolProfileSchema = z.object({
|
|
force: z.boolean().default(false),
|
|
reassignToProfileId: z.string().guid().optional(),
|
|
}).prefault({});
|
|
|
|
export type DeleteToolProfile = z.infer<typeof deleteToolProfileSchema>;
|
|
|
|
export const createToolProfileBindingSchema = z.object({
|
|
profileId: z.string().guid(),
|
|
targetType: toolProfileBindingTargetTypeSchema,
|
|
targetId: z.string().trim().min(1).max(200),
|
|
priority: z.number().int().min(0).max(10000).default(100),
|
|
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolProfileBinding = z.infer<typeof createToolProfileBindingSchema>;
|
|
|
|
export const createToolProfileBindingForProfileSchema = createToolProfileBindingSchema.omit({ profileId: true });
|
|
|
|
export type CreateToolProfileBindingForProfile = z.infer<typeof createToolProfileBindingForProfileSchema>;
|
|
|
|
export const unbindToolProfileBindingSchema = createToolProfileBindingForProfileSchema.pick({
|
|
targetType: true,
|
|
targetId: true,
|
|
});
|
|
|
|
export type UnbindToolProfileBinding = z.infer<typeof unbindToolProfileBindingSchema>;
|
|
|
|
const headerNameSchema = z.string().trim().min(1).max(120).regex(/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);
|
|
|
|
export const toolMcpGatewayAuthConfigSchema = z.object({
|
|
version: z.literal(1).default(1),
|
|
bearer: z.object({
|
|
enabled: z.boolean().default(true),
|
|
tokenPrefix: z.literal("pcgw").default("pcgw"),
|
|
defaultTtlSeconds: z.number().int().positive().max(31_536_000).nullable().default(7_776_000),
|
|
requireFiniteExpiry: z.boolean().default(true),
|
|
longLivedTokenRequiresOverride: z.boolean().default(true),
|
|
}).prefault({}),
|
|
oauth: z.object({
|
|
enabled: z.literal(false).default(false),
|
|
reservedFor: z.literal("v1_5").default("v1_5"),
|
|
protectedResourceMetadataPath: z.string().trim().max(240).optional().nullable(),
|
|
dynamicClientRegistration: z.literal(false).optional(),
|
|
authorizationCodePkce: z.literal(false).optional(),
|
|
}).prefault({}),
|
|
});
|
|
|
|
export const toolMcpGatewayHeaderPolicySchema = z.object({
|
|
version: z.literal(1).default(1),
|
|
callerPassthrough: z.object({
|
|
enabled: z.boolean().default(false),
|
|
allowedHeaders: z.array(headerNameSchema).max(50).default([]),
|
|
}).prefault({}),
|
|
staticHeaders: z.array(z.object({
|
|
name: headerNameSchema,
|
|
valueRef: z.string().trim().max(240).optional().nullable(),
|
|
value: z.string().max(4000).optional().nullable(),
|
|
})).max(50).default([]),
|
|
generatedMetadata: z.object({
|
|
enabled: z.boolean().default(false),
|
|
allowedHeaders: z.array(headerNameSchema).max(20).default([]),
|
|
}).prefault({}),
|
|
responseHeaders: z.object({
|
|
forwardMcpRequiredHeaders: z.boolean().default(true),
|
|
forwardSafeCacheHeaders: z.boolean().default(true),
|
|
}).prefault({}),
|
|
});
|
|
|
|
export const toolMcpGatewayMetadataPolicySchema = z.object({
|
|
version: z.literal(1).default(1),
|
|
forwardCompanyId: z.boolean().default(false),
|
|
forwardGatewayId: z.boolean().default(false),
|
|
forwardProjectId: z.boolean().default(false),
|
|
forwardIssueId: z.boolean().default(false),
|
|
forwardAgentId: z.boolean().default(false),
|
|
forwardRunId: z.boolean().default(false),
|
|
forwardCorrelationId: z.boolean().default(true),
|
|
});
|
|
|
|
export const toolMcpGatewayOnDemandToolsConfigSchema = z.object({
|
|
enabled: z.boolean().default(false),
|
|
searchToolName: z.literal("search_tools").default("search_tools"),
|
|
runToolName: z.literal("run_tool").default("run_tool"),
|
|
});
|
|
|
|
export const createToolMcpGatewaySchema = z.object({
|
|
name: z.string().trim().min(1).max(160),
|
|
slug: z.string().trim().min(1).max(120).regex(safeKeyPattern).optional(),
|
|
displaySlug: z.string().trim().min(1).max(120).regex(safeKeyPattern).optional(),
|
|
description: z.string().max(4000).optional().nullable(),
|
|
profileId: z.string().guid(),
|
|
defaultProfileMode: toolMcpGatewayDefaultProfileModeSchema.default("gateway_only").optional(),
|
|
contextScopeType: toolMcpGatewayContextScopeTypeSchema.default("none").optional(),
|
|
contextScopeId: z.string().trim().min(1).max(200).optional().nullable(),
|
|
agentId: z.string().guid().optional().nullable(),
|
|
projectId: z.string().guid().optional().nullable(),
|
|
issueId: z.string().guid().optional().nullable(),
|
|
approvalIssueId: z.string().guid().optional().nullable(),
|
|
authConfig: toolMcpGatewayAuthConfigSchema.optional(),
|
|
headerPolicy: toolMcpGatewayHeaderPolicySchema.optional(),
|
|
metadataPolicy: toolMcpGatewayMetadataPolicySchema.optional(),
|
|
onDemandToolsConfig: toolMcpGatewayOnDemandToolsConfigSchema.optional(),
|
|
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolMcpGateway = z.infer<typeof createToolMcpGatewaySchema>;
|
|
|
|
export const updateToolMcpGatewaySchema = objectWithoutDefaults(createToolMcpGatewaySchema)
|
|
.partial()
|
|
.extend({ status: toolMcpGatewayStatusSchema.optional() })
|
|
.refine((value) => Object.keys(value).length > 0, { message: "At least one gateway field is required" });
|
|
|
|
export type UpdateToolMcpGateway = z.infer<typeof updateToolMcpGatewaySchema>;
|
|
|
|
export const createToolMcpGatewayTokenSchema = z.object({
|
|
name: z.string().trim().min(1).max(160),
|
|
subjectType: toolMcpGatewayTokenSubjectTypeSchema.default("gateway_client").optional(),
|
|
subjectId: z.string().trim().min(1).max(240).optional().nullable(),
|
|
clientLabel: z.string().trim().min(1).max(160),
|
|
ownerNote: z.string().trim().max(1000).default(""),
|
|
allowedActions: z.array(toolMcpGatewayTokenActionSchema).min(1).max(TOOL_MCP_GATEWAY_TOKEN_ACTIONS.length).default(["tools/list", "tools/call"]).optional(),
|
|
expiresAt: z.coerce.date().optional().nullable(),
|
|
expiryOverrideReason: z.string().trim().min(1).max(1000).optional().nullable(),
|
|
}).superRefine((value, ctx) => {
|
|
if (value.subjectType && value.subjectType !== "gateway_client") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["subjectType"],
|
|
message: "Public V1 token minting only supports gateway_client subjects; heartbeat_run is runtime-managed, while board_user and agent are reserved for later OAuth/user-bound flows.",
|
|
});
|
|
}
|
|
if (value.expiresAt === null && !value.expiryOverrideReason) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["expiryOverrideReason"],
|
|
message: "Non-expiring gateway tokens require an override reason.",
|
|
});
|
|
}
|
|
});
|
|
|
|
export type CreateToolMcpGatewayToken = z.infer<typeof createToolMcpGatewayTokenSchema>;
|
|
|
|
const argumentConditionSchema = z.object({
|
|
fieldEquals: z.record(z.string().trim().min(1).max(120), z.unknown()).optional(),
|
|
fieldNotEquals: z.record(z.string().trim().min(1).max(120), z.unknown()).optional(),
|
|
fieldIn: z.record(z.string().trim().min(1).max(120), z.array(z.unknown()).min(1).max(100)).optional(),
|
|
fieldMatches: z.record(z.string().trim().min(1).max(120), z.string().trim().min(1).max(500)).optional(),
|
|
fieldExists: z.array(z.string().trim().min(1).max(120)).max(100).optional(),
|
|
fieldAbsent: z.array(z.string().trim().min(1).max(120)).max(100).optional(),
|
|
}).strict().refine(
|
|
(value) => Object.values(value).some((nested) => Array.isArray(nested) ? nested.length > 0 : Boolean(nested && Object.keys(nested).length > 0)),
|
|
{ message: "Argument conditions must include at least one field predicate" },
|
|
);
|
|
|
|
const timeWindowConditionSchema = z.object({
|
|
startAt: z.string().trim().datetime({ offset: true }).optional(),
|
|
endAt: z.string().trim().datetime({ offset: true }).optional(),
|
|
daysOfWeekUtc: z.array(z.number().int().min(0).max(6)).max(7).optional(),
|
|
startHourUtc: z.number().int().min(0).max(23).optional(),
|
|
endHourUtc: z.number().int().min(0).max(24).optional(),
|
|
}).strict().refine(
|
|
(value) => value.startAt || value.endAt || value.daysOfWeekUtc?.length || value.startHourUtc !== undefined || value.endHourUtc !== undefined,
|
|
{ message: "timeWindow must include at least one bound" },
|
|
);
|
|
|
|
const actorConditionSchema = z.object({
|
|
actorType: z.enum(["agent", "user", "system", "plugin"]).optional(),
|
|
actorTypes: z.array(z.enum(["agent", "user", "system", "plugin"])).max(20).optional(),
|
|
agentId: z.string().guid().optional(),
|
|
agentIds: z.array(z.string().guid()).max(100).optional(),
|
|
}).strict();
|
|
|
|
const contextConditionSchema = z.object({
|
|
projectId: z.string().guid().optional(),
|
|
projectIds: z.array(z.string().guid()).max(100).optional(),
|
|
routineId: z.string().guid().optional(),
|
|
routineIds: z.array(z.string().guid()).max(100).optional(),
|
|
issueId: z.string().guid().optional(),
|
|
issueIds: z.array(z.string().guid()).max(100).optional(),
|
|
requireIssue: z.boolean().optional(),
|
|
requireProject: z.boolean().optional(),
|
|
requireRoutine: z.boolean().optional(),
|
|
}).strict();
|
|
|
|
const credentialScopeConditionSchema = z.object({
|
|
applicationId: z.string().guid().optional(),
|
|
applicationIds: z.array(z.string().guid()).max(100).optional(),
|
|
connectionId: z.string().guid().optional(),
|
|
connectionIds: z.array(z.string().guid()).max(100).optional(),
|
|
catalogEntryId: z.string().guid().optional(),
|
|
catalogEntryIds: z.array(z.string().guid()).max(100).optional(),
|
|
applicationKey: z.string().trim().min(1).max(160).optional(),
|
|
applicationKeys: z.array(z.string().trim().min(1).max(160)).max(100).optional(),
|
|
providerType: z.string().trim().min(1).max(160).optional(),
|
|
providerTypes: z.array(z.string().trim().min(1).max(160)).max(100).optional(),
|
|
}).strict();
|
|
|
|
export const toolPolicyConditionsSchema = z.object({
|
|
arguments: argumentConditionSchema.optional(),
|
|
args: argumentConditionSchema.optional(),
|
|
actor: actorConditionSchema.optional(),
|
|
context: contextConditionSchema.optional(),
|
|
risk: z.object({
|
|
levels: z.array(toolRiskLevelSchema).max(20).optional(),
|
|
max: toolRiskLevelSchema.optional(),
|
|
isWrite: z.boolean().optional(),
|
|
isDestructive: z.boolean().optional(),
|
|
}).strict().optional(),
|
|
credentialScope: credentialScopeConditionSchema.optional(),
|
|
trustBoundary: z.object({
|
|
providerType: z.string().trim().min(1).max(160).optional(),
|
|
providerTypes: z.array(z.string().trim().min(1).max(160)).max(100).optional(),
|
|
applicationKey: z.string().trim().min(1).max(160).optional(),
|
|
applicationKeys: z.array(z.string().trim().min(1).max(160)).max(100).optional(),
|
|
remoteHttpOnly: z.boolean().optional(),
|
|
paperclipSelfOnly: z.boolean().optional(),
|
|
}).strict().optional(),
|
|
timeWindow: timeWindowConditionSchema.optional(),
|
|
}).strict().refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "Tool policy conditions must include at least one supported condition group" },
|
|
);
|
|
|
|
export const createToolPolicySchema = z.object({
|
|
name: z.string().trim().min(1).max(160),
|
|
description: z.string().max(4000).optional().nullable(),
|
|
policyType: toolPolicyTypeSchema,
|
|
priority: z.number().int().min(0).max(10000).default(100),
|
|
enabled: z.boolean().default(true),
|
|
selectors: z.record(z.string(), z.unknown()).default({}),
|
|
conditions: toolPolicyConditionsSchema.optional().nullable(),
|
|
config: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolPolicy = z.infer<typeof createToolPolicySchema>;
|
|
|
|
export const updateToolPolicySchema = objectWithoutDefaults(createToolPolicySchema).partial().refine(
|
|
(value) => Object.keys(value).length > 0,
|
|
{ message: "At least one tool policy field is required" },
|
|
);
|
|
|
|
export type UpdateToolPolicy = z.infer<typeof updateToolPolicySchema>;
|
|
|
|
export const reorderToolPoliciesSchema = z.object({
|
|
policyIds: z.array(z.string().guid()).min(1).max(500),
|
|
});
|
|
|
|
export type ReorderToolPolicies = z.infer<typeof reorderToolPoliciesSchema>;
|
|
|
|
export const duplicateToolPolicySchema = z.object({
|
|
name: z.string().trim().min(1).max(160).optional(),
|
|
});
|
|
|
|
export type DuplicateToolPolicy = z.infer<typeof duplicateToolPolicySchema>;
|
|
|
|
export const createToolInvocationSchema = z.object({
|
|
idempotencyKey: z.string().trim().min(1).max(300).optional().nullable(),
|
|
issueId: z.string().guid().optional().nullable(),
|
|
runId: z.string().guid().optional().nullable(),
|
|
applicationId: z.string().guid().optional().nullable(),
|
|
connectionId: z.string().guid().optional().nullable(),
|
|
catalogEntryId: z.string().guid().optional().nullable(),
|
|
toolName: z.string().trim().min(1).max(240),
|
|
argumentsHash: z.string().trim().max(128).optional().nullable(),
|
|
argumentsSummary: toolRedactedValueSummarySchema.optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolInvocation = z.infer<typeof createToolInvocationSchema>;
|
|
|
|
export const createToolActionRequestSchema = z.object({
|
|
invocationId: z.string().guid(),
|
|
issueId: z.string().guid().optional().nullable(),
|
|
canonicalArgumentsHash: z.string().trim().min(1).max(128),
|
|
canonicalArgumentsSummary: toolRedactedValueSummarySchema,
|
|
signedArguments: z.string().trim().max(4096).optional().nullable(),
|
|
previewMarkdown: z.string().max(20_000).optional().nullable(),
|
|
expiresAt: z.coerce.date().optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolActionRequest = z.infer<typeof createToolActionRequestSchema>;
|
|
|
|
export const toolConnectionTestCallSchema = z.object({
|
|
agentId: z.string().guid(),
|
|
toolName: z.string().trim().min(1).max(240),
|
|
parameters: z.unknown().optional(),
|
|
});
|
|
|
|
export type ToolConnectionTestCallInput = z.infer<typeof toolConnectionTestCallSchema>;
|
|
|
|
export const importMcpJsonSchema = z.object({
|
|
mcpJson: z.union([z.string(), z.record(z.string(), z.unknown())]),
|
|
});
|
|
|
|
export type ImportMcpJson = z.infer<typeof importMcpJsonSchema>;
|
|
|
|
export const toolAccessSelectorSchema = z.object({
|
|
actorType: z.enum(["agent", "user", "system", "plugin"]).optional(),
|
|
agentId: z.string().guid().optional(),
|
|
agentIds: z.array(z.string().guid()).optional(),
|
|
projectId: z.string().guid().optional(),
|
|
projectIds: z.array(z.string().guid()).optional(),
|
|
routineId: z.string().guid().optional(),
|
|
routineIds: z.array(z.string().guid()).optional(),
|
|
issueId: z.string().guid().optional(),
|
|
issueIds: z.array(z.string().guid()).optional(),
|
|
gatewayId: z.string().guid().optional(),
|
|
gatewayIds: z.array(z.string().guid()).optional(),
|
|
gatewayPublicId: z.string().trim().min(1).max(120).regex(safeKeyPattern).optional(),
|
|
gatewayPublicIds: z.array(z.string().trim().min(1).max(120).regex(safeKeyPattern)).optional(),
|
|
gatewayTokenId: z.string().guid().optional(),
|
|
gatewayTokenIds: z.array(z.string().guid()).optional(),
|
|
clientSubjectType: toolMcpGatewayTokenSubjectTypeSchema.optional(),
|
|
clientSubjectTypes: z.array(toolMcpGatewayTokenSubjectTypeSchema).optional(),
|
|
clientName: z.string().trim().min(1).max(160).optional(),
|
|
clientNames: z.array(z.string().trim().min(1).max(160)).optional(),
|
|
externalClient: z.boolean().optional(),
|
|
applicationId: z.string().guid().optional(),
|
|
applicationIds: z.array(z.string().guid()).optional(),
|
|
connectionId: z.string().guid().optional(),
|
|
connectionIds: z.array(z.string().guid()).optional(),
|
|
catalogEntryId: z.string().guid().optional(),
|
|
catalogEntryIds: z.array(z.string().guid()).optional(),
|
|
toolName: z.string().trim().min(1).max(240).optional(),
|
|
toolNames: z.array(z.string().trim().min(1).max(240)).optional(),
|
|
riskLevel: toolRiskLevelSchema.optional(),
|
|
riskLevels: z.array(toolRiskLevelSchema).optional(),
|
|
});
|
|
|
|
export const toolRateLimitRuleSchema = z.object({
|
|
limit: z.number().int().positive().max(1_000_000),
|
|
windowSeconds: z.number().int().positive().max(31_536_000),
|
|
keyBy: z.array(z.enum(["company", "agent", "application", "connection", "tool"])).optional(),
|
|
});
|
|
|
|
export const toolTrustRuleArgumentFiltersSchema = z.object({
|
|
allowAny: z.boolean().optional(),
|
|
exactHash: z.string().trim().regex(/^[a-f0-9]{64}$/i).optional().nullable(),
|
|
allowedHashes: z.array(z.string().trim().regex(/^[a-f0-9]{64}$/i)).max(100).optional(),
|
|
fieldEquals: z.record(z.string().trim().min(1).max(120), z.unknown()).optional(),
|
|
fieldNotEquals: z.record(z.string().trim().min(1).max(120), z.unknown()).optional(),
|
|
fieldIn: z.record(z.string().trim().min(1).max(120), z.array(z.unknown()).min(1).max(100)).optional(),
|
|
fieldMatches: z.record(z.string().trim().min(1).max(120), z.string().trim().min(1).max(500)).optional(),
|
|
fieldExists: z.array(z.string().trim().min(1).max(120)).max(100).optional(),
|
|
fieldAbsent: z.array(z.string().trim().min(1).max(120)).max(100).optional(),
|
|
}).refine(
|
|
(value) => value.allowAny === true
|
|
|| Boolean(value.exactHash)
|
|
|| Boolean(value.allowedHashes?.length)
|
|
|| Boolean(value.fieldEquals && Object.keys(value.fieldEquals).length > 0)
|
|
|| Boolean(value.fieldNotEquals && Object.keys(value.fieldNotEquals).length > 0)
|
|
|| Boolean(value.fieldIn && Object.keys(value.fieldIn).length > 0)
|
|
|| Boolean(value.fieldMatches && Object.keys(value.fieldMatches).length > 0)
|
|
|| Boolean(value.fieldExists?.length)
|
|
|| Boolean(value.fieldAbsent?.length),
|
|
{ message: "Trust-rule argument filters must specify allowAny, a hash filter, or a field predicate" },
|
|
);
|
|
|
|
export const toolTrustRuleScopeSchema = z.object({
|
|
includeAgent: z.boolean().optional(),
|
|
includeProject: z.boolean().optional(),
|
|
includeIssue: z.boolean().optional(),
|
|
includeApplication: z.boolean().optional(),
|
|
includeConnection: z.boolean().optional(),
|
|
includeCatalogEntry: z.boolean().optional(),
|
|
includeTool: z.boolean().optional(),
|
|
});
|
|
|
|
export const toolTrustRuleBatchApprovalSchema = z.object({
|
|
enabled: z.boolean().optional(),
|
|
maxBatchSize: z.number().int().positive().max(100).optional(),
|
|
windowSeconds: z.number().int().positive().max(31_536_000).optional(),
|
|
});
|
|
|
|
export const createToolTrustRuleFromActionRequestSchema = z.object({
|
|
argumentMode: z.enum(["exact", "action"]).optional(),
|
|
name: z.string().trim().min(1).max(160).optional(),
|
|
description: z.string().max(4000).optional().nullable(),
|
|
priority: z.number().int().min(0).max(10000).default(40),
|
|
approvalThreshold: z.number().int().min(1).max(50).default(2),
|
|
selectors: toolAccessSelectorSchema.optional(),
|
|
scope: toolTrustRuleScopeSchema.optional(),
|
|
argumentFilters: toolTrustRuleArgumentFiltersSchema.optional(),
|
|
expiresAt: z.coerce.date().optional().nullable(),
|
|
batchApproval: toolTrustRuleBatchApprovalSchema.optional().nullable(),
|
|
});
|
|
|
|
export type CreateToolTrustRuleFromActionRequest = z.infer<typeof createToolTrustRuleFromActionRequestSchema>;
|
|
|
|
export const revokeToolTrustRuleSchema = z.object({
|
|
reason: z.string().trim().max(1000).optional().nullable(),
|
|
});
|
|
|
|
export type RevokeToolTrustRule = z.infer<typeof revokeToolTrustRuleSchema>;
|
|
|
|
export const toolPolicyTestRequestSchema = z.object({
|
|
companyId: z.string().guid(),
|
|
actor: z.object({
|
|
actorType: z.enum(["agent", "user", "system", "plugin"]),
|
|
actorId: z.string().trim().min(1).max(240),
|
|
agentId: z.string().guid().optional().nullable(),
|
|
}),
|
|
runContext: z.object({
|
|
heartbeatRunId: z.string().guid().optional().nullable(),
|
|
issueId: z.string().guid().optional().nullable(),
|
|
projectId: z.string().guid().optional().nullable(),
|
|
routineId: z.string().guid().optional().nullable(),
|
|
gatewayId: z.string().guid().optional().nullable(),
|
|
gatewayPublicId: z.string().trim().min(1).max(120).regex(safeKeyPattern).optional().nullable(),
|
|
gatewayTokenId: z.string().guid().optional().nullable(),
|
|
clientSubjectType: toolMcpGatewayTokenSubjectTypeSchema.optional().nullable(),
|
|
clientSubjectId: z.string().trim().min(1).max(240).optional().nullable(),
|
|
clientName: z.string().trim().min(1).max(160).optional().nullable(),
|
|
externalClient: z.boolean().optional().nullable(),
|
|
}).optional().nullable(),
|
|
request: z.object({
|
|
applicationId: z.string().guid().optional().nullable(),
|
|
connectionId: z.string().guid().optional().nullable(),
|
|
catalogEntryId: z.string().guid().optional().nullable(),
|
|
toolName: z.string().trim().min(1).max(240),
|
|
arguments: z.unknown().optional(),
|
|
idempotencyKey: z.string().trim().min(1).max(512).optional().nullable(),
|
|
sideEffecting: z.boolean().optional(),
|
|
}),
|
|
consumeRateLimit: z.boolean().optional(),
|
|
writeAuditEvent: z.boolean().optional(),
|
|
});
|
|
|
|
export type ToolPolicyTestRequestInput = z.infer<typeof toolPolicyTestRequestSchema>;
|