Files
PaperClipAI/packages/shared/src/validators/tool-access.ts
T
DottaandPaperclip 1de0a3bb1e feat(mcp) [split 2/8]: add governed access contracts (#9557)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Governed MCP access spans contracts, runtime enforcement, adapters,
UI surfaces, and operator verification
> - The parity reference PR #9534 is too large for effective automated
or human review
> - The feature therefore needs a linear stack whose individual diffs
stay below the 100-file review limit
> - This pull request is split 2/8 and focuses on database schema and
shared governance contracts
> - The benefit is a standalone, testable review boundary while
preserving byte-for-byte parity at the top of the stack

## Linked Issues or Issue Description

- Related parity reference: #9534
- Problem: The governed access model needs additive persistence and
synchronized shared types before server enforcement can compile.
- Proposed solution: Adds migrations 0148–0169, tool-access and Smoke
Lab schema, shared types/validators/gallery helpers, and the minimal
compile-required contract consumers identified by boundary testing.
- Alternatives considered: keeping #9534 as one 403-file review, or
rewriting the feature to manufacture seams; both were rejected in favor
of path extraction plus compile-driven boundary moves.
- Roadmap alignment: this advances the existing governed MCP/tool-access
work already represented by #9534; it does not introduce a separate
roadmap initiative.
- Stack position: base branch is `pap10341-split/01-demo-servers`.
- Merge policy: merge bottom-up, in order, only after the complete
eight-PR stack has been reviewed and the top-of-stack parity gate
remains empty.
- Requested review: QA for migrations/validators; Greptile on every PR.

## What Changed

- Adds migrations 0148–0169, tool-access and Smoke Lab schema, shared
types/validators/gallery helpers, and the minimal compile-required
contract consumers identified by boundary testing.
- Keeps this PR below 100 changed files and independently typecheckable.
- Preserves the final tree from #9534 when combined with the other seven
stack levels.

## Verification

- `pnpm typecheck` — passed, including migration numbering and safety
checks
- `pnpm --filter @paperclipai/db test` — passed
- `pnpm --filter @paperclipai/shared test` — passed

## Risks

- Migration or contract mistakes could affect every upper layer; all
migrations are additive/idempotent and compile consumers are included in
this boundary.
- Stack risk: merging out of order can expose incomplete layers;
mitigate by following the documented bottom-up merge policy.
- Parity risk: later edits to an intermediate branch can drift from
#9534; mitigate by re-running the empty top-of-stack diff before merge.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, exact model ID `gpt-5.4`; runtime-managed context
window; medium reasoning with repository, shell, Git, GitHub CLI, and
code-execution tools enabled.

## 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] Internal references are omitted except the execution-plan link
explicitly required for this coordinated split stack
- [x] My branch name describes the change 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge


## Stack Coordination

- Internal execution plan:
[PAP-13874](/PAP/issues/PAP-13874#document-plan)
- Parity reference: #9534
- Stack: #9556 → #9557 → #9558 → #9559 → #9560 → #9561 → #9562 → #9563
- Merge bottom-up only after full-stack review and an empty parity diff
at #9563.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-14 12:57:20 -05:00

767 lines
34 KiB
TypeScript

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 { jsonSchemaSchema } from "./plugin.js";
export const toolApplicationTypeSchema = z.enum(TOOL_APPLICATION_TYPES);
export const toolApplicationStatusSchema = z.enum(TOOL_APPLICATION_STATUSES);
export const toolConnectionTransportSchema = z.enum(["remote_http", "local_stdio"]);
export const toolConnectionStatusSchema = z.enum(["draft", "active", "disabled", "archived"]);
export const toolConnectionInstallTargetTypeSchema = z.enum(["company", "agent"]);
export const toolCredentialPlacementSchema = z.enum(["header", "env"]);
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().uuid(),
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(),
});
export const mcpConnectionCredentialRefSchema = z.object({
name: z.string().trim().min(1).max(120),
secretId: z.string().uuid(),
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().uuid().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().uuid().optional().nullable(),
ownerAgentId: z.string().uuid().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().uuid().optional(),
applicationName: z.string().trim().min(1).max(160).optional(),
name: z.string().trim().min(1).max(160),
transport: toolConnectionTransportSchema.optional(),
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 = 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 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>;
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 connectionTokenRequestSchema = z.object({
scope: connectionTokenScopeSchema.optional(),
requestedTtlSeconds: z.number().int().positive().max(86_400).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>;
export const connectToolAppSchema = z.object({
galleryKey: 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().uuid().optional(),
}).refine(
(value) => Boolean(value.galleryKey) !== Boolean(value.link),
{ message: "Provide exactly one of 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({
enabledCatalogEntryIds: z.array(z.string().uuid()).max(500).default([]),
askFirstCatalogEntryIds: z.array(z.string().uuid()).max(500).default([]),
access: z.union([
z.literal("all_agents"),
z.object({ agentIds: z.array(z.string().uuid()).min(1).max(250) }),
]),
});
export type FinishToolApp = z.infer<typeof finishToolAppSchema>;
export const upsertToolCatalogEntrySchema = z.object({
applicationId: z.string().uuid(),
connectionId: z.string().uuid(),
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 = 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().uuid(),
selectorType: toolProfileEntrySelectorTypeSchema,
effect: toolProfileEntryEffectSchema.default("include"),
applicationId: z.string().uuid().optional().nullable(),
connectionId: z.string().uuid().optional().nullable(),
catalogEntryId: z.string().uuid().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 = 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 = 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().uuid(),
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().uuid().optional(),
}).default({});
export type DeleteToolProfile = z.infer<typeof deleteToolProfileSchema>;
export const createToolProfileBindingSchema = z.object({
profileId: z.string().uuid(),
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),
}).default({}),
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(),
}).default({}),
});
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([]),
}).default({}),
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([]),
}).default({}),
responseHeaders: z.object({
forwardMcpRequiredHeaders: z.boolean().default(true),
forwardSafeCacheHeaders: z.boolean().default(true),
}).default({}),
});
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().uuid(),
defaultProfileMode: toolMcpGatewayDefaultProfileModeSchema.default("gateway_only").optional(),
contextScopeType: toolMcpGatewayContextScopeTypeSchema.default("none").optional(),
contextScopeId: z.string().trim().min(1).max(200).optional().nullable(),
agentId: z.string().uuid().optional().nullable(),
projectId: z.string().uuid().optional().nullable(),
issueId: z.string().uuid().optional().nullable(),
approvalIssueId: z.string().uuid().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 = 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().min(1).max(1000),
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().uuid().optional(),
agentIds: z.array(z.string().uuid()).max(100).optional(),
}).strict();
const contextConditionSchema = z.object({
projectId: z.string().uuid().optional(),
projectIds: z.array(z.string().uuid()).max(100).optional(),
routineId: z.string().uuid().optional(),
routineIds: z.array(z.string().uuid()).max(100).optional(),
issueId: z.string().uuid().optional(),
issueIds: z.array(z.string().uuid()).max(100).optional(),
requireIssue: z.boolean().optional(),
requireProject: z.boolean().optional(),
requireRoutine: z.boolean().optional(),
}).strict();
const credentialScopeConditionSchema = z.object({
applicationId: z.string().uuid().optional(),
applicationIds: z.array(z.string().uuid()).max(100).optional(),
connectionId: z.string().uuid().optional(),
connectionIds: z.array(z.string().uuid()).max(100).optional(),
catalogEntryId: z.string().uuid().optional(),
catalogEntryIds: z.array(z.string().uuid()).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 = 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().uuid()).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().uuid().optional().nullable(),
runId: z.string().uuid().optional().nullable(),
applicationId: z.string().uuid().optional().nullable(),
connectionId: z.string().uuid().optional().nullable(),
catalogEntryId: z.string().uuid().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().uuid(),
issueId: z.string().uuid().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().uuid(),
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().uuid().optional(),
agentIds: z.array(z.string().uuid()).optional(),
projectId: z.string().uuid().optional(),
projectIds: z.array(z.string().uuid()).optional(),
routineId: z.string().uuid().optional(),
routineIds: z.array(z.string().uuid()).optional(),
issueId: z.string().uuid().optional(),
issueIds: z.array(z.string().uuid()).optional(),
gatewayId: z.string().uuid().optional(),
gatewayIds: z.array(z.string().uuid()).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().uuid().optional(),
gatewayTokenIds: z.array(z.string().uuid()).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().uuid().optional(),
applicationIds: z.array(z.string().uuid()).optional(),
connectionId: z.string().uuid().optional(),
connectionIds: z.array(z.string().uuid()).optional(),
catalogEntryId: z.string().uuid().optional(),
catalogEntryIds: z.array(z.string().uuid()).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({
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().uuid(),
actor: z.object({
actorType: z.enum(["agent", "user", "system", "plugin"]),
actorId: z.string().trim().min(1).max(240),
agentId: z.string().uuid().optional().nullable(),
}),
runContext: z.object({
heartbeatRunId: z.string().uuid().optional().nullable(),
issueId: z.string().uuid().optional().nullable(),
projectId: z.string().uuid().optional().nullable(),
routineId: z.string().uuid().optional().nullable(),
gatewayId: z.string().uuid().optional().nullable(),
gatewayPublicId: z.string().trim().min(1).max(120).regex(safeKeyPattern).optional().nullable(),
gatewayTokenId: z.string().uuid().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().uuid().optional().nullable(),
connectionId: z.string().uuid().optional().nullable(),
catalogEntryId: z.string().uuid().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>;