mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 20:50:08 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Chat connectors let people start and continue agent tasks from other services. > - A Slack conversation needs access to its surrounding discussion and Slack collaboration tools. > - The agent must use the linked requester's access and keep private material within its permitted audience. > - This pull request adds Slack tools through the existing connector contribution and approval framework. > - People can ask an invited bot to read a discussion, create follow-up tasks, and collaborate in Slack. ## Linked Issues or Issue Description **Subsystem affected** Chat connectors, connector runtime, tool gateway, and connection Settings/Access. **Problem or motivation** Slack-origin tasks can receive messages but cannot inspect the rest of a channel or act through the originating bot. People must paste context or configure a separate integration. **Proposed solution** Supply typed Slack tools and a bundled skill only to the originating task and assigned agent. Resolve the linked requester on the server. Check bot and requester access before reads and writes. Use existing durable actions and approvals. Retrieved messages remain source material. **Alternatives considered** Slack's user-OAuth MCP server does not replace the customer-created chat bot. An unrestricted Web API proxy would not provide suitable permission or publication boundaries. **Roadmap alignment** This extends the existing MCP Tool Gateway & Apps work with a provider contribution. It does not add a task dispatcher or a separate Slack task lifecycle. Related: #11144 covers generic per-user MCP grant execution; this change binds Slack bot operations to chat-origin tasks. ## What Changed - Add 39 typed Slack tools, a method/scope matrix, a bundled skill, and shared native/HTTP execution. - Bind tools to company, endpoint, task, run, assigned agent, and admitted linked requester. Check membership and revocation on each call and before queued writes. - Add paginated reads, bounded history search, source links, messages, file uploads, reactions, pins, bookmarks, topics, canvases, lists, and approved channel operations. - Restrict private-source publication, including automatic replies and uploaded deliverables. Keep other people's bot DMs inaccessible. - Reuse action receipts, idempotency, approvals, and reconciliation. Suppress an identical explicit-send/final-reply duplicate. Return governed results through their verified originating conversation. - Add endpoint-bound personal search OAuth storage and lifecycle. Keep native real-time search disabled until a runtime meets Slack's transient-result requirements. Current runtimes use bounded history search. - Show capabilities, scope upgrades, and personal search authorization in Settings/Access and Storybook. Document provider and runtime limits. ## Verification - Current head `0eb21cba4`: CI checks pass and Greptile is 5/5 with no unresolved findings. One unchanged rapid-callback timing test passed on a single CI retry. - Approval presentation regressions cover board-comment precedence and exact Slack publication; the expanded database assertion passed in CI. The local PostgreSQL startup probe later became unavailable, so that final assertion was verified in CI. Slack setup and failed-run retry browser tests also passed locally. - Full workspace typecheck and build passed. Server typecheck/build passed again after the approval routing fix. - Broad local suites passed in separate groups: server 12,958 tests, UI 6,555, shared 770, skills catalog 20, and other workspace packages 2,652. CLI and serialized server checks passed after environment/timeout retries. These are composite results, not one uninterrupted green full-suite invocation. - PostgreSQL authority regression covers admitted identity, cross-company/task/agent rejection, recovery, retained-session revocation, OAuth refresh/disconnect races, approval execution, exact publication lineage, retries, uncertain sends, and duplicate suppression. - Gateway/response regressions cover separate-origin approval batches and durable continuation. Focused provider, access, search, native runtime, route, and AgentMail regressions pass. - Storybook capability, missing-scope, OAuth configuration, authorization, and disconnect states were inspected in the browser. - Live staging: read a channel decision and full thread, create exactly two assigned backlog tasks, add a reaction, paginate discovery to exhaustion, and return bounded search matches with source links and coverage. - Live staging: create/edit/read a canvas and list, inspect the canvas in Slack, post/edit one message, and create a channel only after approval. New channels remain disabled for responses. - Live staging: read a response-disabled channel from the requester's DM; writes to that channel were denied. The test setting was restored. - Final live retest passed: explicit file upload and exact content read-back; approved deletion of only the disposable bot message; continuation confirmation returned to the original Slack thread without repeating the action. - Optional OAuth, private multi-user boundaries, native RTS, and CLI provider execution are not fully live-qualified. The staging agent initially supplied malformed tool arguments; valid arguments succeeded, and the tool/skill descriptions now emphasize UUID write keys. ## Risks - Existing Slack apps must add scopes and reinstall for new capabilities. Provider plans and document permissions can still restrict operations. - Instances need an independent `PAPERCLIP_TOOL_ACTION_SIGNING_SECRET` for governed tool actions. The staging instance was configured with explicit operator approval; fleet provisioning is a separate gap. - Native RTS is not exposed on current transcript-retaining runtimes. Bounded history scans are deliberately reported as incomplete. Inline file reads support text/canvas content up to 256 KiB; other types return metadata. - Private document edits fail closed when the full audience cannot be verified. Uncertain effects other than posts/uploads require inspection instead of blind retries. - Shared approval-delivery code now separates outcomes by source run to preserve origin boundaries. No database migration is required. - A separate completion-validator gap remains when the agent cites a prior run's registered artifact during finalization. It asked for registration again even though Slack delivery was confirmed. This change does not add a connector-specific task-completion policy. ## Model Used OpenAI GPT-6 through Codex, with repository tools, code execution, and browser testing. The exact deployed model identifier and context-window size were not exposed in the session. ## 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>
981 lines
53 KiB
TypeScript
981 lines
53 KiB
TypeScript
import {
|
|
sql,
|
|
} from "drizzle-orm";
|
|
import {
|
|
boolean,
|
|
check,
|
|
foreignKey,
|
|
index,
|
|
integer,
|
|
jsonb,
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
unique,
|
|
uniqueIndex,
|
|
uuid,
|
|
} from "drizzle-orm/pg-core";
|
|
import type {
|
|
ConnectionTokenIssuanceOutcome,
|
|
ConnectionTokenIssuancePath,
|
|
McpConnectionCredentialRef,
|
|
ToolActionRequestStatus,
|
|
ToolApplicationStatus,
|
|
ToolApplicationType,
|
|
ToolAuditEventType,
|
|
ToolAuditOutcome,
|
|
ToolCatalogEntryKind,
|
|
ToolCatalogEntryStatus,
|
|
ToolConnectionHealthStatus,
|
|
ToolConnectionAuthKind,
|
|
ToolConnectionCredentialSource,
|
|
ToolConnectionKind,
|
|
ToolConnectionCredentialPolicy,
|
|
ToolConnectionOwnership,
|
|
ToolConnectionPurpose,
|
|
ToolConnectionInstallTargetType,
|
|
ToolConnectionStatus,
|
|
ToolConnectionTransport,
|
|
ConnectionGrantKind,
|
|
ConnectionGrantMemberSubjectType,
|
|
ConnectionGrantStatus,
|
|
ToolCredentialSecretRef,
|
|
ToolInvocationApprovalState,
|
|
ToolInvocationStatus,
|
|
ToolMcpGatewayAuthConfig,
|
|
ToolMcpGatewayContextScopeType,
|
|
ToolMcpGatewayDefaultProfileMode,
|
|
ToolMcpGatewayHeaderPolicy,
|
|
ToolMcpGatewayMetadataPolicy,
|
|
ToolMcpGatewayOnDemandToolsConfig,
|
|
ToolMcpGatewayStatus,
|
|
ToolMcpGatewayTokenAction,
|
|
ToolMcpGatewayTokenSubjectType,
|
|
ToolPolicyDecision,
|
|
ToolPolicyType,
|
|
ToolProfileBindingTargetType,
|
|
ToolProfileDefaultAction,
|
|
ToolProfileEntryEffect,
|
|
ToolProfileEntrySelectorType,
|
|
ToolProfileStatus,
|
|
ToolRateLimitWindowKind,
|
|
ToolRedactedValueSummary,
|
|
ToolRiskLevel,
|
|
ToolRuntimeKind,
|
|
ToolRuntimeSlotStatus,
|
|
VercelConnectCredentialReference,
|
|
VercelConnectGrantReference,
|
|
} from "@paperclipai/shared";
|
|
import { agents } from "./agents.js";
|
|
import { approvals } from "./approvals.js";
|
|
import { companies } from "./companies.js";
|
|
import { executionWorkspaces } from "./execution_workspaces.js";
|
|
import { heartbeatRuns } from "./heartbeat_runs.js";
|
|
import { issueThreadInteractions } from "./issue_thread_interactions.js";
|
|
import { issues } from "./issues.js";
|
|
import { plugins } from "./plugins.js";
|
|
import { projects } from "./projects.js";
|
|
import { projectWorkspaces } from "./project_workspaces.js";
|
|
|
|
export const toolApplications = pgTable(
|
|
"tool_applications",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
applicationKey: text("application_key"),
|
|
name: text("name").notNull(),
|
|
description: text("description"),
|
|
type: text("type").$type<ToolApplicationType>().notNull(),
|
|
status: text("status").$type<ToolApplicationStatus>().notNull().default("active"),
|
|
pluginId: uuid("plugin_id").references(() => plugins.id, { onDelete: "set null" }),
|
|
ownerAgentId: uuid("owner_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
ownerUserId: text("owner_user_id"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
archivedAt: timestamp("archived_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_applications_company_idx").on(table.companyId),
|
|
index("tool_applications_company_status_idx").on(table.companyId, table.status),
|
|
uniqueIndex("tool_applications_company_name_uq").on(table.companyId, table.name),
|
|
uniqueIndex("tool_applications_company_key_uq").on(table.companyId, table.applicationKey),
|
|
],
|
|
);
|
|
|
|
export const toolConnections = pgTable(
|
|
"tool_connections",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
// NO ACTION (not CASCADE) so the database itself refuses to delete an application that still
|
|
// has connections. This closes the delete-vs-create race in DELETE
|
|
// /tool-applications/:applicationId: a connection inserted concurrently takes a FOR KEY SHARE
|
|
// lock on the parent row, so the delete fails closed with a foreign-key violation instead of
|
|
// silently cascading the new connection away. NO ACTION (checked at end-of-statement) rather
|
|
// than RESTRICT (checked immediately) is deliberate: it lets a company delete still cascade —
|
|
// companies → tool_applications and companies → tool_connections both fire in one statement,
|
|
// and the connections are gone by the time this constraint is checked.
|
|
applicationId: uuid("application_id").notNull().references(() => toolApplications.id, { onDelete: "no action" }),
|
|
name: text("name").notNull(),
|
|
uid: text("uid").notNull(),
|
|
connectionKind: text("connection_kind").$type<ToolConnectionKind>().notNull().default("managed"),
|
|
connectionPurpose: text("connection_purpose").$type<ToolConnectionPurpose>().notNull().default("tool"),
|
|
ownership: text("ownership").$type<ToolConnectionOwnership>().notNull().default("customer"),
|
|
transport: text("transport").$type<ToolConnectionTransport>().notNull(),
|
|
authKind: text("auth_kind").$type<ToolConnectionAuthKind>().notNull().default("none"),
|
|
credentialSource: text("credential_source").$type<ToolConnectionCredentialSource>().notNull().default("paperclip_vault"),
|
|
externalCredential: jsonb("external_credential").$type<VercelConnectCredentialReference>(),
|
|
credentialPolicy: text("credential_policy").$type<ToolConnectionCredentialPolicy>().notNull().default("shared"),
|
|
status: text("status").$type<ToolConnectionStatus>().notNull().default("draft"),
|
|
enabled: boolean("enabled").notNull().default(false),
|
|
config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
|
|
transportConfig: jsonb("transport_config").$type<Record<string, unknown>>().notNull().default({}),
|
|
credentialRefs: jsonb("credential_refs").$type<McpConnectionCredentialRef[]>().notNull().default([]),
|
|
credentialSecretRefs: jsonb("credential_secret_refs").$type<ToolCredentialSecretRef[]>().notNull().default([]),
|
|
healthStatus: text("health_status").$type<ToolConnectionHealthStatus>().notNull().default("unchecked"),
|
|
healthMessage: text("health_message"),
|
|
healthCheckedAt: timestamp("health_checked_at", { withTimezone: true }),
|
|
lastHealthAt: timestamp("last_health_at", { withTimezone: true }),
|
|
lastCatalogRefreshAt: timestamp("last_catalog_refresh_at", { withTimezone: true }),
|
|
lastError: text("last_error"),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
check("tool_connections_ownership_check", sql`${table.ownership} in ('platform_shared', 'platform_provisioned', 'customer', 'dcr')`),
|
|
check("tool_connections_transport_check", sql`${table.transport} in ('mcp_remote', 'rest_api', 'local_stdio', 'chat_sdk', 'runtime_auth')`),
|
|
check("tool_connections_purpose_check", sql`${table.connectionPurpose} in ('tool', 'channel', 'ai')`),
|
|
check("tool_connections_channel_transport_check", sql`(
|
|
(${table.connectionPurpose} = 'tool' and ${table.transport} not in ('chat_sdk', 'runtime_auth'))
|
|
or
|
|
(${table.connectionPurpose} = 'channel' and (${table.transport} = 'chat_sdk' or (${table.transport} = 'rest_api' and ${table.config}->>'provider' = 'agentmail')))
|
|
or
|
|
(${table.connectionPurpose} = 'ai' and ${table.transport} = 'runtime_auth')
|
|
)`),
|
|
check("tool_connections_auth_kind_check", sql`${table.authKind} in ('oauth', 'api_key', 'none')`),
|
|
check("tool_connections_credential_source_check", sql`${table.credentialSource} in ('paperclip_vault', 'vercel_connect')`),
|
|
check("tool_connections_credential_source_one_of_check", sql`(
|
|
(${table.credentialSource} = 'paperclip_vault' and ${table.externalCredential} is null)
|
|
or
|
|
(${table.credentialSource} = 'vercel_connect' and ${table.externalCredential} is not null and jsonb_array_length(${table.credentialRefs}) = 0 and jsonb_array_length(${table.credentialSecretRefs}) = 0)
|
|
)`),
|
|
check("tool_connections_credential_policy_check", sql`${table.credentialPolicy} in ('shared', 'per_user', 'per_user_with_fallback', 'per_agent')`),
|
|
index("tool_connections_company_idx").on(table.companyId),
|
|
index("tool_connections_application_idx").on(table.applicationId),
|
|
index("tool_connections_company_enabled_idx").on(table.companyId, table.enabled),
|
|
uniqueIndex("tool_connections_company_uid_uq").on(table.companyId, table.uid),
|
|
unique("tool_connections_company_id_uq").on(table.companyId, table.id),
|
|
],
|
|
);
|
|
|
|
export const connectionGrants = pgTable(
|
|
"connection_grants",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
connectionId: uuid("connection_id").notNull(),
|
|
kind: text("kind").$type<ConnectionGrantKind>().notNull(),
|
|
subjectUserId: text("subject_user_id"),
|
|
subjectAgentId: uuid("subject_agent_id").references(() => agents.id, { onDelete: "cascade" }),
|
|
providerTenant: jsonb("provider_tenant").$type<{
|
|
name?: string;
|
|
externalId?: string;
|
|
oauth?: {
|
|
strategy?: string;
|
|
accessTokenExpiresAt?: string | null;
|
|
scopes?: string[];
|
|
tokenType?: string;
|
|
refreshedAt?: string;
|
|
refreshTokenExpiresAt?: string;
|
|
refreshLease?: {
|
|
id?: string;
|
|
expiresAt?: string;
|
|
};
|
|
};
|
|
slackSearch?: { endpointId: string; workspaceId: string; slackUserId: string; clientRevision: string };
|
|
github?: {
|
|
userId: string;
|
|
login: string;
|
|
avatarUrl?: string;
|
|
installationCount: number;
|
|
repositoryCount: number;
|
|
repositorySelection: "all" | "selected" | "mixed" | "none";
|
|
installationIds: string[];
|
|
installationOwnerLogins: string[];
|
|
/** Repository metadata visible to this credential; refreshed from GitHub. */
|
|
repositories?: Array<{ id: string; fullName: string; installationId: string; private?: boolean }>;
|
|
installationUrl?: string;
|
|
managementUrl?: string;
|
|
appSlug?: string;
|
|
accessRevision?: string;
|
|
lastAccessRefreshAt?: string;
|
|
lastWebhookAt?: string;
|
|
webhookHealth?: "pending" | "healthy" | "unhealthy";
|
|
};
|
|
}>(),
|
|
credentialSecretRefs: jsonb("credential_secret_refs").$type<ToolCredentialSecretRef[]>().notNull().default([]),
|
|
externalCredential: jsonb("external_credential").$type<VercelConnectGrantReference>(),
|
|
status: text("status").$type<ConnectionGrantStatus>().notNull().default("active"),
|
|
isDefault: boolean("is_default").notNull().default(false),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
revokedByAgentId: uuid("revoked_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
revokedByUserId: text("revoked_by_user_id"),
|
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
check("connection_grants_kind_check", sql`${table.kind} in ('organization', 'user', 'agent')`),
|
|
check("connection_grants_status_check", sql`${table.status} in ('active', 'revoked', 'expired', 'needs_reauthorization')`),
|
|
check("connection_grants_credential_source_one_of_check", sql`${table.externalCredential} is null or jsonb_array_length(${table.credentialSecretRefs}) = 0`),
|
|
check("connection_grants_subject_check", sql`(${table.kind} = 'user' and ${table.subjectUserId} is not null and ${table.subjectAgentId} is null) or (${table.kind} = 'agent' and ${table.subjectAgentId} is not null and ${table.subjectUserId} is null) or (${table.kind} = 'organization' and ${table.subjectUserId} is null and ${table.subjectAgentId} is null)`),
|
|
check("connection_grants_default_check", sql`${table.isDefault} = false or ${table.kind} = 'organization'`),
|
|
foreignKey({
|
|
columns: [table.companyId, table.connectionId],
|
|
foreignColumns: [toolConnections.companyId, toolConnections.id],
|
|
name: "connection_grants_company_connection_fk",
|
|
}).onDelete("cascade"),
|
|
index("connection_grants_company_connection_idx").on(table.companyId, table.connectionId),
|
|
index("connection_grants_subject_user_idx").on(table.companyId, table.subjectUserId),
|
|
index("connection_grants_subject_agent_idx").on(table.companyId, table.subjectAgentId),
|
|
unique("connection_grants_company_id_uq").on(table.companyId, table.id),
|
|
uniqueIndex("connection_grants_user_uq").on(table.connectionId, table.subjectUserId),
|
|
uniqueIndex("connection_grants_agent_uq").on(table.connectionId, table.subjectAgentId),
|
|
uniqueIndex("connection_grants_default_uq").on(table.connectionId).where(sql`${table.isDefault} = true and ${table.kind} = 'organization'`),
|
|
],
|
|
);
|
|
|
|
export const connectionGrantMembers = pgTable(
|
|
"connection_grant_members",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
grantId: uuid("grant_id").notNull(),
|
|
subjectType: text("subject_type").$type<ConnectionGrantMemberSubjectType>().notNull(),
|
|
subjectId: text("subject_id").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
check("connection_grant_members_subject_type_check", sql`${table.subjectType} in ('user')`),
|
|
foreignKey({
|
|
columns: [table.companyId, table.grantId],
|
|
foreignColumns: [connectionGrants.companyId, connectionGrants.id],
|
|
name: "connection_grant_members_company_grant_fk",
|
|
}).onDelete("cascade"),
|
|
index("connection_grant_members_company_subject_idx").on(table.companyId, table.subjectType, table.subjectId),
|
|
uniqueIndex("connection_grant_members_grant_subject_uq").on(table.grantId, table.subjectType, table.subjectId),
|
|
],
|
|
);
|
|
|
|
export const connectionGrantDelegations = pgTable(
|
|
"connection_grant_delegations",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
grantId: uuid("grant_id").notNull(),
|
|
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
|
|
createdByUserId: text("created_by_user_id").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
foreignKey({
|
|
columns: [table.companyId, table.grantId],
|
|
foreignColumns: [connectionGrants.companyId, connectionGrants.id],
|
|
name: "connection_grant_delegations_company_grant_fk",
|
|
}).onDelete("cascade"),
|
|
index("connection_grant_delegations_company_agent_idx").on(table.companyId, table.agentId),
|
|
uniqueIndex("connection_grant_delegations_grant_agent_uq").on(table.grantId, table.agentId),
|
|
],
|
|
);
|
|
|
|
export const toolConnectionInstalls = pgTable(
|
|
"tool_connection_installs",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
connectionId: uuid("connection_id").notNull().references(() => toolConnections.id, { onDelete: "cascade" }),
|
|
targetType: text("target_type").$type<ToolConnectionInstallTargetType>().notNull(),
|
|
targetId: text("target_id").notNull(),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
check("tool_connection_installs_target_type_check", sql`${table.targetType} in ('company', 'agent')`),
|
|
index("tool_connection_installs_company_target_idx").on(table.companyId, table.targetType, table.targetId),
|
|
index("tool_connection_installs_connection_idx").on(table.companyId, table.connectionId),
|
|
uniqueIndex("tool_connection_installs_target_uq").on(
|
|
table.companyId,
|
|
table.connectionId,
|
|
table.targetType,
|
|
table.targetId,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const toolOauthStates = pgTable(
|
|
"tool_oauth_states",
|
|
{
|
|
state: text("state").primaryKey(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
connectionId: uuid("connection_id").notNull().references(() => toolConnections.id, { onDelete: "cascade" }),
|
|
codeVerifier: text("code_verifier").notNull(),
|
|
createdByActorType: text("created_by_actor_type"),
|
|
createdByActorId: text("created_by_actor_id"),
|
|
createdBySessionId: text("created_by_session_id"),
|
|
subjectUserId: text("subject_user_id"),
|
|
subjectAgentId: uuid("subject_agent_id").references(() => agents.id, { onDelete: "cascade" }),
|
|
requestedScopes: jsonb("requested_scopes").$type<string[]>(),
|
|
returnTo: text("return_to"),
|
|
issueId: uuid("issue_id"),
|
|
interactionId: uuid("interaction_id"),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_oauth_states_company_idx").on(table.companyId),
|
|
index("tool_oauth_states_connection_idx").on(table.connectionId),
|
|
index("tool_oauth_states_actor_idx").on(table.createdByActorType, table.createdByActorId),
|
|
index("tool_oauth_states_subject_agent_idx").on(table.companyId, table.subjectAgentId),
|
|
index("tool_oauth_states_expires_at_idx").on(table.expiresAt),
|
|
],
|
|
);
|
|
|
|
export const toolCatalogEntries = pgTable(
|
|
"tool_catalog_entries",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
applicationId: uuid("application_id").references(() => toolApplications.id, { onDelete: "cascade" }),
|
|
connectionId: uuid("connection_id").notNull().references(() => toolConnections.id, { onDelete: "cascade" }),
|
|
entryKind: text("entry_kind").$type<ToolCatalogEntryKind>().notNull().default("tool"),
|
|
name: text("name").notNull(),
|
|
toolName: text("tool_name").notNull(),
|
|
title: text("title"),
|
|
description: text("description"),
|
|
inputSchema: jsonb("input_schema").$type<Record<string, unknown>>().notNull().default({}),
|
|
outputSchema: jsonb("output_schema").$type<Record<string, unknown>>(),
|
|
annotations: jsonb("annotations").$type<Record<string, unknown>>().notNull().default({}),
|
|
riskLevel: text("risk_level").$type<ToolRiskLevel>().notNull().default("read"),
|
|
isReadOnly: boolean("is_read_only").notNull().default(true),
|
|
isWrite: boolean("is_write").notNull().default(false),
|
|
isDestructive: boolean("is_destructive").notNull().default(false),
|
|
status: text("status").$type<ToolCatalogEntryStatus>().notNull().default("active"),
|
|
version: text("version"),
|
|
versionHash: text("version_hash").notNull(),
|
|
schemaHash: text("schema_hash"),
|
|
firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(),
|
|
lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
|
|
reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
|
|
reviewedByAgentId: uuid("reviewed_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
reviewedByUserId: text("reviewed_by_user_id"),
|
|
quarantinedAt: timestamp("quarantined_at", { withTimezone: true }),
|
|
quarantineReason: text("quarantine_reason"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_catalog_entries_company_idx").on(table.companyId),
|
|
index("tool_catalog_entries_application_idx").on(table.applicationId),
|
|
index("tool_catalog_entries_connection_idx").on(table.connectionId),
|
|
index("tool_catalog_entries_company_status_idx").on(table.companyId, table.status),
|
|
uniqueIndex("tool_catalog_entries_connection_name_uq").on(table.connectionId, table.name),
|
|
],
|
|
);
|
|
|
|
export const toolProfiles = pgTable(
|
|
"tool_profiles",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
profileKey: text("profile_key").notNull(),
|
|
name: text("name").notNull(),
|
|
description: text("description"),
|
|
status: text("status").$type<ToolProfileStatus>().notNull().default("active"),
|
|
defaultAction: text("default_action").$type<ToolProfileDefaultAction>().notNull().default("deny"),
|
|
newToolsReviewedAt: timestamp("new_tools_reviewed_at", { withTimezone: true }),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_profiles_company_status_idx").on(table.companyId, table.status),
|
|
uniqueIndex("tool_profiles_company_key_uq").on(table.companyId, table.profileKey),
|
|
uniqueIndex("tool_profiles_company_name_uq").on(table.companyId, table.name),
|
|
],
|
|
);
|
|
|
|
export const toolProfileEntries = pgTable(
|
|
"tool_profile_entries",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
profileId: uuid("profile_id").notNull().references(() => toolProfiles.id, { onDelete: "cascade" }),
|
|
selectorType: text("selector_type").$type<ToolProfileEntrySelectorType>().notNull(),
|
|
effect: text("effect").$type<ToolProfileEntryEffect>().notNull().default("include"),
|
|
applicationId: uuid("application_id").references(() => toolApplications.id, { onDelete: "cascade" }),
|
|
connectionId: uuid("connection_id").references(() => toolConnections.id, { onDelete: "cascade" }),
|
|
catalogEntryId: uuid("catalog_entry_id").references(() => toolCatalogEntries.id, { onDelete: "cascade" }),
|
|
toolName: text("tool_name"),
|
|
riskLevel: text("risk_level").$type<ToolRiskLevel>(),
|
|
conditions: jsonb("conditions").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_profile_entries_company_profile_idx").on(table.companyId, table.profileId),
|
|
index("tool_profile_entries_application_idx").on(table.companyId, table.applicationId),
|
|
index("tool_profile_entries_connection_idx").on(table.companyId, table.connectionId),
|
|
index("tool_profile_entries_catalog_entry_idx").on(table.companyId, table.catalogEntryId),
|
|
],
|
|
);
|
|
|
|
export const toolProfileBindings = pgTable(
|
|
"tool_profile_bindings",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
profileId: uuid("profile_id").notNull().references(() => toolProfiles.id, { onDelete: "cascade" }),
|
|
targetType: text("target_type").$type<ToolProfileBindingTargetType>().notNull(),
|
|
targetId: text("target_id").notNull(),
|
|
priority: integer("priority").notNull().default(100),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_profile_bindings_company_target_idx").on(table.companyId, table.targetType, table.targetId),
|
|
uniqueIndex("tool_profile_bindings_target_profile_uq").on(
|
|
table.companyId,
|
|
table.targetType,
|
|
table.targetId,
|
|
table.profileId,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const toolMcpGateways = pgTable(
|
|
"tool_mcp_gateways",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
gatewayPublicId: text("gateway_public_id").notNull().default(sql`'gw_' || replace(gen_random_uuid()::text, '-', '')`),
|
|
name: text("name").notNull(),
|
|
slug: text("slug").notNull(),
|
|
displaySlug: text("display_slug").notNull().default(""),
|
|
description: text("description"),
|
|
status: text("status").$type<ToolMcpGatewayStatus>().notNull().default("active"),
|
|
profileId: uuid("profile_id").notNull().references(() => toolProfiles.id, { onDelete: "restrict" }),
|
|
defaultProfileMode: text("default_profile_mode").$type<ToolMcpGatewayDefaultProfileMode>().notNull().default("gateway_only"),
|
|
contextScopeType: text("context_scope_type").$type<ToolMcpGatewayContextScopeType>().notNull().default("none"),
|
|
contextScopeId: text("context_scope_id"),
|
|
agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
approvalIssueId: uuid("approval_issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
authConfig: jsonb("auth_config").$type<ToolMcpGatewayAuthConfig>().notNull().default({
|
|
version: 1,
|
|
bearer: {
|
|
enabled: true,
|
|
tokenPrefix: "pcgw",
|
|
defaultTtlSeconds: 7_776_000,
|
|
requireFiniteExpiry: true,
|
|
longLivedTokenRequiresOverride: true,
|
|
},
|
|
oauth: {
|
|
enabled: false,
|
|
reservedFor: "v1_5",
|
|
dynamicClientRegistration: false,
|
|
authorizationCodePkce: false,
|
|
},
|
|
}),
|
|
headerPolicy: jsonb("header_policy").$type<ToolMcpGatewayHeaderPolicy>().notNull().default({
|
|
version: 1,
|
|
callerPassthrough: { enabled: false, allowedHeaders: [] },
|
|
staticHeaders: [],
|
|
generatedMetadata: { enabled: false, allowedHeaders: [] },
|
|
responseHeaders: { forwardMcpRequiredHeaders: true, forwardSafeCacheHeaders: true },
|
|
}),
|
|
metadataPolicy: jsonb("metadata_policy").$type<ToolMcpGatewayMetadataPolicy>().notNull().default({
|
|
version: 1,
|
|
forwardCompanyId: false,
|
|
forwardGatewayId: false,
|
|
forwardProjectId: false,
|
|
forwardIssueId: false,
|
|
forwardAgentId: false,
|
|
forwardRunId: false,
|
|
forwardCorrelationId: true,
|
|
}),
|
|
onDemandToolsConfig: jsonb("on_demand_tools_config").$type<ToolMcpGatewayOnDemandToolsConfig>().notNull().default({
|
|
enabled: false,
|
|
searchToolName: "search_tools",
|
|
runToolName: "run_tool",
|
|
}),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
archivedAt: timestamp("archived_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_mcp_gateways_company_idx").on(table.companyId),
|
|
index("tool_mcp_gateways_company_status_idx").on(table.companyId, table.status),
|
|
index("tool_mcp_gateways_profile_idx").on(table.companyId, table.profileId),
|
|
uniqueIndex("tool_mcp_gateways_public_id_uq").on(table.gatewayPublicId),
|
|
uniqueIndex("tool_mcp_gateways_company_slug_uq").on(table.companyId, table.slug),
|
|
uniqueIndex("tool_mcp_gateways_company_name_uq").on(table.companyId, table.name),
|
|
],
|
|
);
|
|
|
|
export const toolMcpGatewayTokens = pgTable(
|
|
"tool_mcp_gateway_tokens",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
gatewayId: uuid("gateway_id").notNull().references(() => toolMcpGateways.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
tokenHash: text("token_hash").notNull(),
|
|
tokenPrefix: text("token_prefix").notNull().default(""),
|
|
subjectType: text("subject_type").$type<ToolMcpGatewayTokenSubjectType>().notNull().default("gateway_client"),
|
|
subjectId: text("subject_id"),
|
|
clientLabel: text("client_label").notNull().default(""),
|
|
ownerNote: text("owner_note").notNull().default(""),
|
|
allowedActions: jsonb("allowed_actions").$type<ToolMcpGatewayTokenAction[]>().notNull().default(["tools/list", "tools/call"]),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
expiryOverrideReason: text("expiry_override_reason"),
|
|
expiryOverrideByUserId: text("expiry_override_by_user_id"),
|
|
expiryOverrideByAgentId: uuid("expiry_override_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
expiryOverrideAt: timestamp("expiry_override_at", { withTimezone: true }),
|
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
uniqueIndex("tool_mcp_gateway_tokens_token_hash_uq").on(table.tokenHash),
|
|
index("tool_mcp_gateway_tokens_gateway_idx").on(table.companyId, table.gatewayId),
|
|
index("tool_mcp_gateway_tokens_subject_idx").on(table.companyId, table.subjectType, table.subjectId),
|
|
index("tool_mcp_gateway_tokens_company_expires_idx").on(table.companyId, table.expiresAt),
|
|
],
|
|
);
|
|
|
|
export const toolPolicies = pgTable(
|
|
"tool_policies",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
description: text("description"),
|
|
policyType: text("policy_type").$type<ToolPolicyType>().notNull(),
|
|
priority: integer("priority").notNull().default(100),
|
|
enabled: boolean("enabled").notNull().default(true),
|
|
selectors: jsonb("selectors").$type<Record<string, unknown>>().notNull().default({}),
|
|
conditions: jsonb("conditions").$type<Record<string, unknown>>(),
|
|
config: jsonb("config").$type<Record<string, unknown>>(),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_policies_company_enabled_idx").on(table.companyId, table.enabled),
|
|
index("tool_policies_company_type_idx").on(table.companyId, table.policyType),
|
|
uniqueIndex("tool_policies_company_name_uq").on(table.companyId, table.name),
|
|
],
|
|
);
|
|
|
|
export const toolRuntimeSlots = pgTable(
|
|
"tool_runtime_slots",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
applicationId: uuid("application_id").references(() => toolApplications.id, { onDelete: "set null" }),
|
|
connectionId: uuid("connection_id").references(() => toolConnections.id, { onDelete: "cascade" }),
|
|
projectWorkspaceId: uuid("project_workspace_id").references(() => projectWorkspaces.id, { onDelete: "set null" }),
|
|
executionWorkspaceId: uuid("execution_workspace_id").references(() => executionWorkspaces.id, { onDelete: "set null" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
ownerScopeType: text("owner_scope_type").notNull().default("connection"),
|
|
ownerScopeId: text("owner_scope_id"),
|
|
runtimeKind: text("runtime_kind").$type<ToolRuntimeKind>().notNull().default("local_stdio"),
|
|
slotKey: text("slot_key").notNull(),
|
|
status: text("status").$type<ToolRuntimeSlotStatus>().notNull().default("stopped"),
|
|
reuseKey: text("reuse_key"),
|
|
workspaceScope: text("workspace_scope"),
|
|
credentialScopeHash: text("credential_scope_hash"),
|
|
provider: text("provider"),
|
|
providerRef: text("provider_ref"),
|
|
processId: integer("process_id"),
|
|
commandTemplateKey: text("command_template_key"),
|
|
healthStatus: text("health_status").$type<ToolConnectionHealthStatus>().notNull().default("unchecked"),
|
|
healthMessage: text("health_message"),
|
|
lastHealthCheckAt: timestamp("last_health_check_at", { withTimezone: true }),
|
|
lastStartedAt: timestamp("last_started_at", { withTimezone: true }),
|
|
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
stoppedAt: timestamp("stopped_at", { withTimezone: true }),
|
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
idleExpiresAt: timestamp("idle_expires_at", { withTimezone: true }),
|
|
idleDeadlineAt: timestamp("idle_deadline_at", { withTimezone: true }),
|
|
lastError: text("last_error"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_runtime_slots_company_idx").on(table.companyId),
|
|
index("tool_runtime_slots_connection_idx").on(table.connectionId),
|
|
index("tool_runtime_slots_execution_workspace_idx").on(table.companyId, table.executionWorkspaceId),
|
|
uniqueIndex("tool_runtime_slots_slot_key_uq").on(table.companyId, table.slotKey),
|
|
],
|
|
);
|
|
|
|
export const toolStdioCommandTemplates = pgTable(
|
|
"tool_stdio_command_templates",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
templateKey: text("template_key").notNull(),
|
|
name: text("name").notNull(),
|
|
description: text("description"),
|
|
status: text("status").$type<"active" | "disabled">().notNull().default("active"),
|
|
command: text("command").notNull(),
|
|
args: jsonb("args").$type<string[]>().notNull().default([]),
|
|
envKeys: jsonb("env_keys").$type<string[]>().notNull().default([]),
|
|
tools: jsonb("tools").$type<Array<Record<string, unknown>>>().notNull().default([]),
|
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
createdByUserId: text("created_by_user_id"),
|
|
disabledAt: timestamp("disabled_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_stdio_command_templates_company_idx").on(table.companyId),
|
|
index("tool_stdio_command_templates_company_status_idx").on(table.companyId, table.status),
|
|
uniqueIndex("tool_stdio_command_templates_company_key_uq").on(table.companyId, table.templateKey),
|
|
],
|
|
);
|
|
|
|
export const toolGatewaySessions = pgTable(
|
|
"tool_gateway_sessions",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
|
|
runId: uuid("run_id").notNull().references(() => heartbeatRuns.id, { onDelete: "cascade" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
gatewayId: uuid("gateway_id").references(() => toolMcpGateways.id, { onDelete: "set null" }),
|
|
gatewayTokenId: uuid("gateway_token_id").references(() => toolMcpGatewayTokens.id, { onDelete: "set null" }),
|
|
gatewayPublicId: text("gateway_public_id"),
|
|
clientSubjectType: text("client_subject_type").$type<ToolMcpGatewayTokenSubjectType>(),
|
|
clientSubjectId: text("client_subject_id"),
|
|
clientName: text("client_name"),
|
|
mcpSessionId: text("mcp_session_id"),
|
|
correlationId: text("correlation_id"),
|
|
tokenHash: text("token_hash").notNull(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
|
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
uniqueIndex("tool_gateway_sessions_token_hash_uq").on(table.tokenHash),
|
|
index("tool_gateway_sessions_company_agent_idx").on(table.companyId, table.agentId),
|
|
index("tool_gateway_sessions_company_expires_idx").on(table.companyId, table.expiresAt),
|
|
index("tool_gateway_sessions_run_idx").on(table.companyId, table.runId),
|
|
index("tool_gateway_sessions_issue_idx").on(table.companyId, table.issueId),
|
|
index("tool_gateway_sessions_gateway_idx").on(table.companyId, table.gatewayId),
|
|
],
|
|
);
|
|
|
|
export const toolGatewayRateLimitCounters = pgTable(
|
|
"tool_gateway_rate_limit_counters",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
counterKey: text("counter_key").notNull(),
|
|
windowStartAt: timestamp("window_start_at", { withTimezone: true }).notNull(),
|
|
windowMs: integer("window_ms").notNull(),
|
|
limit: integer("limit").notNull(),
|
|
count: integer("count").notNull().default(0),
|
|
resetAt: timestamp("reset_at", { withTimezone: true }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_gateway_rate_limit_counters_company_idx").on(table.companyId),
|
|
uniqueIndex("tool_gateway_rate_limit_counters_window_uq").on(
|
|
table.companyId,
|
|
table.counterKey,
|
|
table.windowStartAt,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const toolInvocations = pgTable(
|
|
"tool_invocations",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
idempotencyKey: text("idempotency_key"),
|
|
actorType: text("actor_type").notNull().default("system"),
|
|
actorId: text("actor_id"),
|
|
agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
runId: uuid("run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
|
gatewayId: uuid("gateway_id").references(() => toolMcpGateways.id, { onDelete: "set null" }),
|
|
gatewayTokenId: uuid("gateway_token_id").references(() => toolMcpGatewayTokens.id, { onDelete: "set null" }),
|
|
gatewayPublicId: text("gateway_public_id"),
|
|
clientSubjectType: text("client_subject_type").$type<ToolMcpGatewayTokenSubjectType>(),
|
|
clientSubjectId: text("client_subject_id"),
|
|
clientName: text("client_name"),
|
|
mcpSessionId: text("mcp_session_id"),
|
|
correlationId: text("correlation_id"),
|
|
applicationId: uuid("application_id").references(() => toolApplications.id, { onDelete: "set null" }),
|
|
connectionId: uuid("connection_id").references(() => toolConnections.id, { onDelete: "set null" }),
|
|
catalogEntryId: uuid("catalog_entry_id").references(() => toolCatalogEntries.id, { onDelete: "set null" }),
|
|
catalogVersionHash: text("catalog_version_hash"),
|
|
catalogSchemaHash: text("catalog_schema_hash"),
|
|
providerType: text("provider_type"),
|
|
applicationKey: text("application_key"),
|
|
upstreamToolName: text("upstream_tool_name"),
|
|
riskLevel: text("risk_level").$type<ToolRiskLevel>(),
|
|
toolName: text("tool_name").notNull(),
|
|
argumentsHash: text("arguments_hash"),
|
|
argumentsSummary: jsonb("arguments_summary").$type<ToolRedactedValueSummary>(),
|
|
policyDecision: text("policy_decision").$type<ToolPolicyDecision>(),
|
|
matchedPolicyIds: jsonb("matched_policy_ids").$type<string[]>().notNull().default([]),
|
|
policyExplanation: jsonb("policy_explanation").$type<Record<string, unknown>>(),
|
|
credentialScopeSummary: jsonb("credential_scope_summary").$type<Record<string, unknown>>(),
|
|
headerPolicySummary: jsonb("header_policy_summary").$type<Record<string, unknown>>(),
|
|
approvalState: text("approval_state").$type<ToolInvocationApprovalState>().notNull().default("not_required"),
|
|
status: text("status").$type<ToolInvocationStatus>().notNull().default("pending"),
|
|
upstreamRequestId: text("upstream_request_id"),
|
|
resultHash: text("result_hash"),
|
|
resultSummary: jsonb("result_summary").$type<ToolRedactedValueSummary>(),
|
|
resultSizeBytes: integer("result_size_bytes"),
|
|
resultArtifactId: uuid("result_artifact_id"),
|
|
errorCode: text("error_code"),
|
|
errorMessage: text("error_message"),
|
|
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_invocations_company_created_idx").on(table.companyId, table.createdAt),
|
|
index("tool_invocations_run_idx").on(table.companyId, table.runId),
|
|
index("tool_invocations_issue_idx").on(table.companyId, table.issueId),
|
|
index("tool_invocations_gateway_idx").on(table.companyId, table.gatewayId),
|
|
uniqueIndex("tool_invocations_company_idempotency_uq").on(table.companyId, table.idempotencyKey),
|
|
],
|
|
);
|
|
|
|
export const toolActionRequests = pgTable(
|
|
"tool_action_requests",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
invocationId: uuid("invocation_id").notNull().references(() => toolInvocations.id, { onDelete: "cascade" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
interactionId: uuid("interaction_id").references(() => issueThreadInteractions.id, { onDelete: "set null" }),
|
|
approvalId: uuid("approval_id").references(() => approvals.id, { onDelete: "set null" }),
|
|
status: text("status").$type<ToolActionRequestStatus>().notNull().default("pending"),
|
|
canonicalArgumentsHash: text("canonical_arguments_hash").notNull(),
|
|
canonicalArgumentsSummary: jsonb("canonical_arguments_summary").$type<ToolRedactedValueSummary>().notNull(),
|
|
signedArguments: text("signed_arguments"),
|
|
previewMarkdown: text("preview_markdown"),
|
|
requestedByAgentId: uuid("requested_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
requestedByUserId: text("requested_by_user_id"),
|
|
resolvedByAgentId: uuid("resolved_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
resolvedByUserId: text("resolved_by_user_id"),
|
|
decidedByAgentId: uuid("decided_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
decidedByUserId: text("decided_by_user_id"),
|
|
decidedAt: timestamp("decided_at", { withTimezone: true }),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_action_requests_company_status_idx").on(table.companyId, table.status),
|
|
index("tool_action_requests_invocation_idx").on(table.invocationId),
|
|
index("tool_action_requests_issue_idx").on(table.companyId, table.issueId),
|
|
],
|
|
);
|
|
|
|
export const toolCallEvents = pgTable(
|
|
"tool_call_events",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
eventType: text("event_type").$type<ToolAuditEventType>().notNull(),
|
|
actorType: text("actor_type").notNull().default("system"),
|
|
actorId: text("actor_id"),
|
|
agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
runId: uuid("run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
gatewayId: uuid("gateway_id").references(() => toolMcpGateways.id, { onDelete: "set null" }),
|
|
gatewayTokenId: uuid("gateway_token_id").references(() => toolMcpGatewayTokens.id, { onDelete: "set null" }),
|
|
gatewayPublicId: text("gateway_public_id"),
|
|
clientSubjectType: text("client_subject_type").$type<ToolMcpGatewayTokenSubjectType>(),
|
|
clientSubjectId: text("client_subject_id"),
|
|
clientName: text("client_name"),
|
|
mcpSessionId: text("mcp_session_id"),
|
|
correlationId: text("correlation_id"),
|
|
applicationId: uuid("application_id").references(() => toolApplications.id, { onDelete: "set null" }),
|
|
connectionId: uuid("connection_id").references(() => toolConnections.id, { onDelete: "set null" }),
|
|
catalogEntryId: uuid("catalog_entry_id").references(() => toolCatalogEntries.id, { onDelete: "set null" }),
|
|
invocationId: uuid("invocation_id").references(() => toolInvocations.id, { onDelete: "set null" }),
|
|
actionRequestId: uuid("action_request_id").references(() => toolActionRequests.id, { onDelete: "set null" }),
|
|
runtimeSlotId: uuid("runtime_slot_id").references(() => toolRuntimeSlots.id, { onDelete: "set null" }),
|
|
toolName: text("tool_name"),
|
|
decision: text("decision").$type<ToolPolicyDecision>(),
|
|
matchedPolicyIds: jsonb("matched_policy_ids").$type<string[]>().notNull().default([]),
|
|
reasonCode: text("reason_code"),
|
|
policyExplanation: jsonb("policy_explanation").$type<Record<string, unknown>>(),
|
|
credentialScopeSummary: jsonb("credential_scope_summary").$type<Record<string, unknown>>(),
|
|
headerPolicySummary: jsonb("header_policy_summary").$type<Record<string, unknown>>(),
|
|
outcome: text("outcome").$type<ToolAuditOutcome>().notNull().default("pending"),
|
|
latencyMs: integer("latency_ms"),
|
|
argumentsSummary: jsonb("arguments_summary").$type<ToolRedactedValueSummary>(),
|
|
requestHash: text("request_hash"),
|
|
requestSummary: jsonb("request_summary").$type<ToolRedactedValueSummary>(),
|
|
resultHash: text("result_hash"),
|
|
resultSummary: jsonb("result_summary").$type<ToolRedactedValueSummary>(),
|
|
resultSizeBytes: integer("result_size_bytes"),
|
|
redactionPlan: jsonb("redaction_plan").$type<Record<string, unknown>>(),
|
|
rateLimitState: jsonb("rate_limit_state").$type<Record<string, unknown>>(),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
errorCode: text("error_code"),
|
|
errorMessage: text("error_message"),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_call_events_company_created_idx").on(table.companyId, table.createdAt),
|
|
index("tool_call_events_run_idx").on(table.companyId, table.runId),
|
|
index("tool_call_events_issue_idx").on(table.companyId, table.issueId),
|
|
index("tool_call_events_invocation_idx").on(table.invocationId),
|
|
index("tool_call_events_gateway_idx").on(table.companyId, table.gatewayId),
|
|
],
|
|
);
|
|
|
|
export const connectionTokenIssuances = pgTable(
|
|
"connection_token_issuances",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
applicationId: uuid("application_id").references(() => toolApplications.id, { onDelete: "set null" }),
|
|
connectionId: uuid("connection_id").notNull().references(() => toolConnections.id, { onDelete: "cascade" }),
|
|
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
|
|
runId: uuid("run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
|
issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
projectId: uuid("project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
responsibleUserId: text("responsible_user_id"),
|
|
path: text("path").$type<ConnectionTokenIssuancePath>().notNull(),
|
|
requestedScope: jsonb("requested_scope").$type<string[]>().notNull().default([]),
|
|
issuedScope: jsonb("issued_scope").$type<string[]>().notNull().default([]),
|
|
ttlSeconds: integer("ttl_seconds"),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
tokenHash: text("token_hash"),
|
|
outcome: text("outcome").$type<ConnectionTokenIssuanceOutcome>().notNull(),
|
|
errorCode: text("error_code"),
|
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("connection_token_issuances_company_created_idx").on(table.companyId, table.createdAt),
|
|
index("connection_token_issuances_connection_created_idx").on(table.companyId, table.connectionId, table.createdAt),
|
|
index("connection_token_issuances_agent_connection_idx").on(table.companyId, table.agentId, table.connectionId, table.createdAt),
|
|
index("connection_token_issuances_run_idx").on(table.companyId, table.runId),
|
|
sql`CONSTRAINT connection_token_issuances_path_check CHECK (${table.path} IN ('exchange', 'oauth_access', 'static'))`,
|
|
sql`CONSTRAINT connection_token_issuances_outcome_check CHECK (${table.outcome} IN ('success', 'denied', 'rate_limited', 'use_env_lease', 'upstream_error', 'failure'))`,
|
|
sql`CONSTRAINT connection_token_issuances_ttl_bounds CHECK (${table.ttlSeconds} IS NULL OR (${table.ttlSeconds} >= 1 AND ${table.ttlSeconds} <= 900))`,
|
|
sql`CONSTRAINT connection_token_issuances_token_hash_format CHECK (${table.tokenHash} IS NULL OR ${table.tokenHash} ~ '^[a-f0-9]{64}$')`,
|
|
],
|
|
);
|
|
|
|
export const toolRateLimitCounters = pgTable(
|
|
"tool_rate_limit_counters",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
policyId: uuid("policy_id").notNull().references(() => toolPolicies.id, { onDelete: "cascade" }),
|
|
counterKey: text("counter_key").notNull(),
|
|
scopeType: text("scope_type").notNull(),
|
|
scopeId: text("scope_id").notNull(),
|
|
windowKind: text("window_kind").$type<ToolRateLimitWindowKind>().notNull(),
|
|
windowStartAt: timestamp("window_start_at", { withTimezone: true }).notNull(),
|
|
limit: integer("limit").notNull(),
|
|
remaining: integer("remaining").notNull(),
|
|
resetAt: timestamp("reset_at", { withTimezone: true }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_rate_limit_counters_company_idx").on(table.companyId),
|
|
uniqueIndex("tool_rate_limit_counters_window_uq").on(
|
|
table.companyId,
|
|
table.policyId,
|
|
table.counterKey,
|
|
table.windowKind,
|
|
table.windowStartAt,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const toolRuntimeMetricCounters = pgTable(
|
|
"tool_runtime_metric_counters",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
metric: text("metric").notNull(),
|
|
bucketStartAt: timestamp("bucket_start_at", { withTimezone: true }).notNull(),
|
|
count: integer("count").notNull().default(0),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_runtime_metric_counters_company_metric_idx").on(table.companyId, table.metric, table.bucketStartAt),
|
|
uniqueIndex("tool_runtime_metric_counters_bucket_uq").on(table.companyId, table.metric, table.bucketStartAt),
|
|
sql`CONSTRAINT tool_runtime_metric_counters_count_nonnegative CHECK (${table.count} >= 0)`,
|
|
],
|
|
);
|
|
|
|
export const toolAccessAuditEvents = pgTable(
|
|
"tool_access_audit_events",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
|
gatewayId: uuid("gateway_id").references(() => toolMcpGateways.id, { onDelete: "set null" }),
|
|
gatewayTokenId: uuid("gateway_token_id").references(() => toolMcpGatewayTokens.id, { onDelete: "set null" }),
|
|
gatewayPublicId: text("gateway_public_id"),
|
|
clientName: text("client_name"),
|
|
correlationId: text("correlation_id"),
|
|
connectionId: uuid("connection_id").references(() => toolConnections.id, { onDelete: "set null" }),
|
|
catalogEntryId: uuid("catalog_entry_id").references(() => toolCatalogEntries.id, { onDelete: "set null" }),
|
|
actorType: text("actor_type").notNull().default("system"),
|
|
actorId: text("actor_id"),
|
|
action: text("action").notNull(),
|
|
outcome: text("outcome").notNull(),
|
|
reasonCode: text("reason_code"),
|
|
details: jsonb("details").$type<Record<string, unknown>>().notNull().default({}),
|
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(table) => [
|
|
index("tool_access_audit_company_created_idx").on(table.companyId, table.createdAt),
|
|
index("tool_access_audit_connection_idx").on(table.connectionId),
|
|
index("tool_access_audit_gateway_idx").on(table.companyId, table.gatewayId),
|
|
],
|
|
);
|