-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat(copilot): add share_file tool + surface file share state to the agent #5656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' | ||
| import { createLogger } from '@sim/logger' | ||
| import type { ShareAuthType } from '@/lib/api/contracts/public-shares' | ||
| import { ShareFile } from '@/lib/copilot/generated/tool-catalog-v1' | ||
| import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access' | ||
| import { | ||
| assertServerToolNotAborted, | ||
| type BaseServerTool, | ||
| type ServerToolContext, | ||
| } from '@/lib/copilot/tools/server/base-tool' | ||
| import { | ||
| getShareForResource, | ||
| ShareValidationError, | ||
| upsertFileShare, | ||
| } from '@/lib/public-shares/share-manager' | ||
| import { | ||
| getWorkspaceFile, | ||
| resolveWorkspaceFileReference, | ||
| } from '@/lib/uploads/contexts/workspace/workspace-file-manager' | ||
| import { | ||
| PublicFileSharingNotAllowedError, | ||
| validatePublicFileSharing, | ||
| } from '@/ee/access-control/utils/permission-check' | ||
|
|
||
| const logger = createLogger('ShareFileServerTool') | ||
|
|
||
| interface ShareFileArgs { | ||
| path?: string | ||
| fileId?: string | ||
| action?: 'share' | 'unshare' | ||
| authType?: ShareAuthType | ||
| password?: string | ||
| allowedEmails?: string[] | ||
| args?: Record<string, unknown> | ||
| } | ||
|
|
||
| interface ShareFileResult { | ||
| success: boolean | ||
| message: string | ||
| data?: { | ||
| url: string | ||
| token: string | ||
| authType: ShareAuthType | ||
| hasPassword: boolean | ||
| isActive: boolean | ||
| } | ||
| } | ||
|
|
||
| export const shareFileServerTool: BaseServerTool<ShareFileArgs, ShareFileResult> = { | ||
| name: ShareFile.id, | ||
| async execute(params: ShareFileArgs, context?: ServerToolContext): Promise<ShareFileResult> { | ||
| if (!context?.userId) { | ||
| throw new Error('Authentication required') | ||
| } | ||
| const workspaceId = context.workspaceId | ||
| if (!workspaceId) { | ||
| return { success: false, message: 'Workspace ID is required' } | ||
| } | ||
| await ensureWorkspaceAccess(workspaceId, context.userId, 'write') | ||
|
|
||
| const nested = params.args | ||
| const path = params.path || (nested?.path as string) || '' | ||
| const legacyFileId = params.fileId || (nested?.fileId as string) || '' | ||
| const action = (params.action || (nested?.action as string) || 'share') as 'share' | 'unshare' | ||
| const authType = (params.authType || (nested?.authType as ShareAuthType | undefined)) as | ||
| | ShareAuthType | ||
| | undefined | ||
| const password = params.password || (nested?.password as string) || undefined | ||
| const allowedEmails = | ||
| params.allowedEmails || (nested?.allowedEmails as string[] | undefined) || undefined | ||
|
|
||
| const targetRef = path || legacyFileId | ||
| if (!targetRef) return { success: false, message: 'path is required' } | ||
|
|
||
| const existingFile = path | ||
| ? await resolveWorkspaceFileReference(workspaceId, path) | ||
| : await getWorkspaceFile(workspaceId, legacyFileId) | ||
| if (!existingFile) { | ||
| return { success: false, message: `File not found: ${targetRef}` } | ||
| } | ||
| const fileId = existingFile.id | ||
| const isActive = action !== 'unshare' | ||
|
|
||
| // Enabling a share is gated by the org's access-control policy (both the | ||
| // master on/off and the per-auth-type allow-list); disabling is always | ||
| // allowed so users can still un-share after the policy is turned on. | ||
| if (isActive) { | ||
| // Validate the auth type that will ACTUALLY be persisted. upsertFileShare | ||
| // falls back to the existing share's authType when none is passed, so a bare | ||
| // re-enable must be checked against that stored mode — not 'public' — or a | ||
| // now-disallowed password/email/sso share could be silently reactivated. | ||
| const existingShare = await getShareForResource('file', fileId) | ||
| const effectiveAuthType = authType ?? existingShare?.authType ?? 'public' | ||
| try { | ||
| await validatePublicFileSharing(context.userId, workspaceId, effectiveAuthType) | ||
| } catch (error) { | ||
| if (error instanceof PublicFileSharingNotAllowedError) { | ||
| return { success: false, message: error.message } | ||
| } | ||
| throw error | ||
| } | ||
| } | ||
|
|
||
| assertServerToolNotAborted(context) | ||
|
|
||
| let share | ||
| try { | ||
| share = await upsertFileShare({ | ||
| workspaceId, | ||
| fileId, | ||
| userId: context.userId, | ||
| isActive, | ||
| authType, | ||
| password, | ||
| allowedEmails, | ||
| }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unshare clears stored auth typeHigh Severity
Reviewed by Cursor Bugbot for commit 9e1e71a. Configure here. |
||
| } catch (error) { | ||
| if (error instanceof ShareValidationError) { | ||
| return { success: false, message: error.message } | ||
| } | ||
| throw error | ||
| } | ||
|
|
||
| logger.info(`${isActive ? 'Enabled' : 'Disabled'} share for file via share_file`, { | ||
| fileId, | ||
| workspaceId, | ||
| authType: share.authType, | ||
| userId: context.userId, | ||
| }) | ||
|
|
||
| recordAudit({ | ||
| workspaceId, | ||
| actorId: context.userId, | ||
| action: isActive ? AuditAction.FILE_SHARED : AuditAction.FILE_SHARE_DISABLED, | ||
| resourceType: AuditResourceType.FILE, | ||
| resourceId: fileId, | ||
| resourceName: existingFile.name, | ||
| description: `${isActive ? 'Enabled' : 'Disabled'} public share for "${existingFile.name}"`, | ||
| }) | ||
|
|
||
| if (!isActive) { | ||
| return { | ||
| success: true, | ||
| message: `Stopped sharing "${existingFile.name}". The previous link no longer works.`, | ||
| data: { | ||
| url: share.url, | ||
| token: share.token, | ||
| authType: share.authType, | ||
| hasPassword: share.hasPassword, | ||
| isActive: share.isActive, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| const authNote = | ||
| share.authType === 'password' | ||
| ? ' (password-protected — share the password separately)' | ||
| : share.authType === 'email' | ||
| ? ' (restricted to allowed emails via one-time code)' | ||
| : share.authType === 'sso' | ||
| ? ' (restricted to allowed emails via SSO)' | ||
| : '' | ||
|
|
||
| return { | ||
| success: true, | ||
| message: `Shared "${existingFile.name}"${authNote}: ${share.url}`, | ||
| data: { | ||
| url: share.url, | ||
| token: share.token, | ||
| authType: share.authType, | ||
| hasPassword: share.hasPassword, | ||
| isActive: share.isActive, | ||
| }, | ||
| } | ||
| }, | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.