diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 6f53db86f8..cb3b0d1fcc 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -683,6 +683,45 @@ export function SerperIcon(props: SVGProps) { ) } +export function TailscaleIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function TavilyIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index e721126ade..7d0f8838df 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -155,6 +155,7 @@ import { StagehandIcon, StripeIcon, SupabaseIcon, + TailscaleIcon, TavilyIcon, TelegramIcon, TextractIcon, @@ -333,6 +334,7 @@ export const blockTypeToIconMap: Record = { stripe: StripeIcon, stt_v2: STTIcon, supabase: SupabaseIcon, + tailscale: TailscaleIcon, tavily: TavilyIcon, telegram: TelegramIcon, textract_v2: TextractIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 49ee064ffb..5b957649a3 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -152,6 +152,7 @@ "stt", "supabase", "table", + "tailscale", "tavily", "telegram", "textract", diff --git a/apps/docs/content/docs/en/tools/tailscale.mdx b/apps/docs/content/docs/en/tools/tailscale.mdx new file mode 100644 index 0000000000..17d71352e3 --- /dev/null +++ b/apps/docs/content/docs/en/tools/tailscale.mdx @@ -0,0 +1,490 @@ +--- +title: Tailscale +description: Manage devices and network settings in your Tailscale tailnet +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Overview + +[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on WireGuard that makes it easy to connect devices, services, and users across any network. The Tailscale block lets you automate network management tasks like device provisioning, access control, route management, and DNS configuration directly from your Sim workflows. + +## Authentication + +The Tailscale block uses API key authentication. To get an API key: + +1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys) +2. Navigate to **Settings > Keys** +3. Click **Generate API key** +4. Set an expiry (1-90 days) and copy the key (starts with `tskey-api-`) + +You must have an **Owner**, **Admin**, **IT admin**, or **Network admin** role to generate API keys. + +## Tailnet Identifier + +Every operation requires a **tailnet** parameter. This is typically your organization's domain name (e.g., `example.com`). You can also use `"-"` to refer to your default tailnet. + +## Common Use Cases + +- **Device inventory**: List and monitor all devices connected to your network +- **Automated provisioning**: Create and manage auth keys to pre-authorize new devices +- **Access control**: Authorize or deauthorize devices, manage device tags for ACL policies +- **Route management**: View and enable subnet routes for devices acting as subnet routers +- **DNS management**: Configure nameservers, MagicDNS, and search paths +- **Key lifecycle**: Create, list, inspect, and revoke auth keys +- **User auditing**: List all users in the tailnet and their roles +- **Policy review**: Retrieve the current ACL policy for inspection or backup + +## Tools + +### `tailscale_list_devices` + +List all devices in the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `devices` | array | List of devices in the tailnet | +| ↳ `id` | string | Device ID | +| ↳ `name` | string | Device name | +| ↳ `hostname` | string | Device hostname | +| ↳ `user` | string | Associated user | +| ↳ `os` | string | Operating system | +| ↳ `clientVersion` | string | Tailscale client version | +| ↳ `addresses` | array | Tailscale IP addresses | +| ↳ `tags` | array | Device tags | +| ↳ `authorized` | boolean | Whether the device is authorized | +| ↳ `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections | +| ↳ `lastSeen` | string | Last seen timestamp | +| ↳ `created` | string | Creation timestamp | +| `count` | number | Total number of devices | + +### `tailscale_get_device` + +Get details of a specific device by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Device ID | +| `name` | string | Device name | +| `hostname` | string | Device hostname | +| `user` | string | Associated user | +| `os` | string | Operating system | +| `clientVersion` | string | Tailscale client version | +| `addresses` | array | Tailscale IP addresses | +| `tags` | array | Device tags | +| `authorized` | boolean | Whether the device is authorized | +| `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections | +| `lastSeen` | string | Last seen timestamp | +| `created` | string | Creation timestamp | +| `enabledRoutes` | array | Approved subnet routes | +| `advertisedRoutes` | array | Requested subnet routes | +| `isExternal` | boolean | Whether the device is external | +| `updateAvailable` | boolean | Whether an update is available | +| `machineKey` | string | Machine key | +| `nodeKey` | string | Node key | + +### `tailscale_delete_device` + +Remove a device from the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the device was successfully deleted | +| `deviceId` | string | ID of the deleted device | + +### `tailscale_authorize_device` + +Authorize or deauthorize a device on the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID to authorize | +| `authorized` | boolean | Yes | Whether to authorize \(true\) or deauthorize \(false\) the device | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the operation succeeded | +| `deviceId` | string | Device ID | +| `authorized` | boolean | Authorization status after the operation | + +### `tailscale_set_device_tags` + +Set tags on a device in the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID | +| `tags` | string | Yes | Comma-separated list of tags \(e.g., "tag:server,tag:production"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the tags were successfully set | +| `deviceId` | string | Device ID | +| `tags` | array | Tags set on the device | + +### `tailscale_get_device_routes` + +Get the subnet routes for a device + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `advertisedRoutes` | array | Subnet routes the device is advertising | +| `enabledRoutes` | array | Subnet routes that are approved/enabled | + +### `tailscale_set_device_routes` + +Set the enabled subnet routes for a device + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID | +| `routes` | string | Yes | Comma-separated list of subnet routes to enable \(e.g., "10.0.0.0/24,192.168.1.0/24"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `advertisedRoutes` | array | Subnet routes the device is advertising | +| `enabledRoutes` | array | Subnet routes that are now enabled | + +### `tailscale_update_device_key` + +Enable or disable key expiry on a device + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `deviceId` | string | Yes | Device ID | +| `keyExpiryDisabled` | boolean | Yes | Whether to disable key expiry \(true\) or enable it \(false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the operation succeeded | +| `deviceId` | string | Device ID | +| `keyExpiryDisabled` | boolean | Whether key expiry is now disabled | + +### `tailscale_list_dns_nameservers` + +Get the DNS nameservers configured for the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `dns` | array | List of DNS nameserver addresses | +| `magicDNS` | boolean | Whether MagicDNS is enabled | + +### `tailscale_set_dns_nameservers` + +Set the DNS nameservers for the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `dns` | string | Yes | Comma-separated list of DNS nameserver IP addresses \(e.g., "8.8.8.8,8.8.4.4"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `dns` | array | Updated list of DNS nameserver addresses | + +### `tailscale_get_dns_preferences` + +Get the DNS preferences for the tailnet including MagicDNS status + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `magicDNS` | boolean | Whether MagicDNS is enabled | + +### `tailscale_set_dns_preferences` + +Set DNS preferences for the tailnet (enable/disable MagicDNS) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `magicDNS` | boolean | Yes | Whether to enable \(true\) or disable \(false\) MagicDNS | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `magicDNS` | boolean | Updated MagicDNS status | + +### `tailscale_get_dns_searchpaths` + +Get the DNS search paths configured for the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `searchPaths` | array | List of DNS search path domains | + +### `tailscale_set_dns_searchpaths` + +Set the DNS search paths for the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `searchPaths` | string | Yes | Comma-separated list of DNS search path domains \(e.g., "corp.example.com,internal.example.com"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `searchPaths` | array | Updated list of DNS search path domains | + +### `tailscale_list_users` + +List all users in the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | List of users in the tailnet | +| ↳ `id` | string | User ID | +| ↳ `displayName` | string | Display name | +| ↳ `loginName` | string | Login name / email | +| ↳ `profilePicURL` | string | Profile picture URL | +| ↳ `role` | string | User role \(owner, admin, member, etc.\) | +| ↳ `status` | string | User status \(active, suspended, etc.\) | +| ↳ `type` | string | User type \(member, shared, tagged\) | +| ↳ `created` | string | Creation timestamp | +| ↳ `lastSeen` | string | Last seen timestamp | +| ↳ `deviceCount` | number | Number of devices owned by user | +| `count` | number | Total number of users | + +### `tailscale_create_auth_key` + +Create a new auth key for the tailnet to pre-authorize devices + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `reusable` | boolean | No | Whether the key can be used more than once | +| `ephemeral` | boolean | No | Whether devices authenticated with this key are ephemeral | +| `preauthorized` | boolean | No | Whether devices are pre-authorized \(skip manual approval\) | +| `tags` | string | Yes | Comma-separated list of tags for devices using this key \(e.g., "tag:server,tag:prod"\) | +| `description` | string | No | Description for the auth key | +| `expirySeconds` | number | No | Key expiry time in seconds \(default: 90 days\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Auth key ID | +| `key` | string | The auth key value \(only shown once at creation\) | +| `description` | string | Key description | +| `created` | string | Creation timestamp | +| `expires` | string | Expiration timestamp | +| `revoked` | string | Revocation timestamp \(empty if not revoked\) | +| `capabilities` | object | Key capabilities | +| ↳ `reusable` | boolean | Whether the key is reusable | +| ↳ `ephemeral` | boolean | Whether devices are ephemeral | +| ↳ `preauthorized` | boolean | Whether devices are pre-authorized | +| ↳ `tags` | array | Tags applied to devices using this key | + +### `tailscale_list_auth_keys` + +List all auth keys in the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `keys` | array | List of auth keys | +| ↳ `id` | string | Auth key ID | +| ↳ `description` | string | Key description | +| ↳ `created` | string | Creation timestamp | +| ↳ `expires` | string | Expiration timestamp | +| ↳ `revoked` | string | Revocation timestamp | +| ↳ `capabilities` | object | Key capabilities | +| ↳ `reusable` | boolean | Whether the key is reusable | +| ↳ `ephemeral` | boolean | Whether devices are ephemeral | +| ↳ `preauthorized` | boolean | Whether devices are pre-authorized | +| ↳ `tags` | array | Tags applied to devices | +| `count` | number | Total number of auth keys | + +### `tailscale_get_auth_key` + +Get details of a specific auth key + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `keyId` | string | Yes | Auth key ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Auth key ID | +| `description` | string | Key description | +| `created` | string | Creation timestamp | +| `expires` | string | Expiration timestamp | +| `revoked` | string | Revocation timestamp | +| `capabilities` | object | Key capabilities | +| ↳ `reusable` | boolean | Whether the key is reusable | +| ↳ `ephemeral` | boolean | Whether devices are ephemeral | +| ↳ `preauthorized` | boolean | Whether devices are pre-authorized | +| ↳ `tags` | array | Tags applied to devices using this key | + +### `tailscale_delete_auth_key` + +Revoke and delete an auth key + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | +| `keyId` | string | Yes | Auth key ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the auth key was successfully deleted | +| `keyId` | string | ID of the deleted auth key | + +### `tailscale_get_acl` + +Get the current ACL policy for the tailnet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Tailscale API key | +| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `acl` | string | ACL policy as JSON string | +| `etag` | string | ETag for the current ACL version \(use with If-Match header for updates\) | + + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 841cda375b..65d501c1e8 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -155,6 +155,7 @@ import { StagehandIcon, StripeIcon, SupabaseIcon, + TailscaleIcon, TavilyIcon, TelegramIcon, TextractIcon, @@ -333,6 +334,7 @@ export const blockTypeToIconMap: Record = { stripe: StripeIcon, stt_v2: STTIcon, supabase: SupabaseIcon, + tailscale: TailscaleIcon, tavily: TavilyIcon, telegram: TelegramIcon, textract_v2: TextractIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 789e5ef4a3..72621ae258 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -10482,6 +10482,105 @@ "integrationType": "databases", "tags": ["cloud", "data-warehouse", "vector-search"] }, + { + "type": "tailscale", + "slug": "tailscale", + "name": "Tailscale", + "description": "Manage devices and network settings in your Tailscale tailnet", + "longDescription": "Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.", + "bgColor": "#2E2D2D", + "iconName": "TailscaleIcon", + "docsUrl": "https://docs.sim.ai/tools/tailscale", + "operations": [ + { + "name": "List Devices", + "description": "List all devices in the tailnet" + }, + { + "name": "Get Device", + "description": "Get details of a specific device by ID" + }, + { + "name": "Delete Device", + "description": "Remove a device from the tailnet" + }, + { + "name": "Authorize Device", + "description": "Authorize or deauthorize a device on the tailnet" + }, + { + "name": "Set Device Tags", + "description": "Set tags on a device in the tailnet" + }, + { + "name": "Get Device Routes", + "description": "Get the subnet routes for a device" + }, + { + "name": "Set Device Routes", + "description": "Set the enabled subnet routes for a device" + }, + { + "name": "Update Device Key", + "description": "Enable or disable key expiry on a device" + }, + { + "name": "List DNS Nameservers", + "description": "Get the DNS nameservers configured for the tailnet" + }, + { + "name": "Set DNS Nameservers", + "description": "Set the DNS nameservers for the tailnet" + }, + { + "name": "Get DNS Preferences", + "description": "Get the DNS preferences for the tailnet including MagicDNS status" + }, + { + "name": "Set DNS Preferences", + "description": "Set DNS preferences for the tailnet (enable/disable MagicDNS)" + }, + { + "name": "Get DNS Search Paths", + "description": "Get the DNS search paths configured for the tailnet" + }, + { + "name": "Set DNS Search Paths", + "description": "Set the DNS search paths for the tailnet" + }, + { + "name": "List Users", + "description": "List all users in the tailnet" + }, + { + "name": "Create Auth Key", + "description": "Create a new auth key for the tailnet to pre-authorize devices" + }, + { + "name": "List Auth Keys", + "description": "List all auth keys in the tailnet" + }, + { + "name": "Get Auth Key", + "description": "Get details of a specific auth key" + }, + { + "name": "Delete Auth Key", + "description": "Revoke and delete an auth key" + }, + { + "name": "Get ACL", + "description": "Get the current ACL policy for the tailnet" + } + ], + "operationCount": 20, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "security", + "tags": ["monitoring"] + }, { "type": "tavily", "slug": "tavily", diff --git a/apps/sim/blocks/blocks/tailscale.ts b/apps/sim/blocks/blocks/tailscale.ts new file mode 100644 index 0000000000..2a1f47dd0b --- /dev/null +++ b/apps/sim/blocks/blocks/tailscale.ts @@ -0,0 +1,341 @@ +import { TailscaleIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' + +export const TailscaleBlock: BlockConfig = { + type: 'tailscale', + name: 'Tailscale', + description: 'Manage devices and network settings in your Tailscale tailnet', + longDescription: + 'Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.', + docsLink: 'https://docs.sim.ai/tools/tailscale', + category: 'tools', + integrationType: IntegrationType.Security, + tags: ['monitoring'], + bgColor: '#2E2D2D', + icon: TailscaleIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Devices', id: 'list_devices' }, + { label: 'Get Device', id: 'get_device' }, + { label: 'Delete Device', id: 'delete_device' }, + { label: 'Authorize Device', id: 'authorize_device' }, + { label: 'Set Device Tags', id: 'set_device_tags' }, + { label: 'Get Device Routes', id: 'get_device_routes' }, + { label: 'Set Device Routes', id: 'set_device_routes' }, + { label: 'Update Device Key', id: 'update_device_key' }, + { label: 'List DNS Nameservers', id: 'list_dns_nameservers' }, + { label: 'Set DNS Nameservers', id: 'set_dns_nameservers' }, + { label: 'Get DNS Preferences', id: 'get_dns_preferences' }, + { label: 'Set DNS Preferences', id: 'set_dns_preferences' }, + { label: 'Get DNS Search Paths', id: 'get_dns_searchpaths' }, + { label: 'Set DNS Search Paths', id: 'set_dns_searchpaths' }, + { label: 'List Users', id: 'list_users' }, + { label: 'Create Auth Key', id: 'create_auth_key' }, + { label: 'List Auth Keys', id: 'list_auth_keys' }, + { label: 'Get Auth Key', id: 'get_auth_key' }, + { label: 'Delete Auth Key', id: 'delete_auth_key' }, + { label: 'Get ACL', id: 'get_acl' }, + ], + value: () => 'list_devices', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + password: true, + placeholder: 'tskey-api-...', + required: true, + }, + { + id: 'tailnet', + title: 'Tailnet', + type: 'short-input', + placeholder: 'example.com or "-" for default', + required: true, + }, + { + id: 'deviceId', + title: 'Device ID', + type: 'short-input', + placeholder: 'Enter device ID', + condition: { + field: 'operation', + value: [ + 'get_device', + 'delete_device', + 'authorize_device', + 'set_device_tags', + 'get_device_routes', + 'set_device_routes', + 'update_device_key', + ], + }, + required: { + field: 'operation', + value: [ + 'get_device', + 'delete_device', + 'authorize_device', + 'set_device_tags', + 'get_device_routes', + 'set_device_routes', + 'update_device_key', + ], + }, + }, + { + id: 'authorized', + title: 'Authorized', + type: 'dropdown', + options: [ + { label: 'Authorize', id: 'true' }, + { label: 'Deauthorize', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'authorize_device' }, + }, + { + id: 'keyExpiryDisabled', + title: 'Key Expiry Disabled', + type: 'dropdown', + options: [ + { label: 'Disable Expiry', id: 'true' }, + { label: 'Enable Expiry', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'update_device_key' }, + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'tag:server,tag:production', + condition: { field: 'operation', value: ['set_device_tags', 'create_auth_key'] }, + required: { field: 'operation', value: 'set_device_tags' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of Tailscale ACL tags. Each tag must start with "tag:" (e.g., tag:server,tag:production). Return ONLY the comma-separated tags - no explanations, no extra text.', + }, + }, + { + id: 'routes', + title: 'Routes', + type: 'short-input', + placeholder: '10.0.0.0/24,192.168.1.0/24', + condition: { field: 'operation', value: 'set_device_routes' }, + required: { field: 'operation', value: 'set_device_routes' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of subnet routes in CIDR notation (e.g., 10.0.0.0/24,192.168.1.0/24). Return ONLY the comma-separated routes - no explanations, no extra text.', + }, + }, + { + id: 'dnsServers', + title: 'DNS Nameservers', + type: 'short-input', + placeholder: '8.8.8.8,8.8.4.4', + condition: { field: 'operation', value: 'set_dns_nameservers' }, + required: { field: 'operation', value: 'set_dns_nameservers' }, + }, + { + id: 'magicDNS', + title: 'MagicDNS', + type: 'dropdown', + options: [ + { label: 'Enable', id: 'true' }, + { label: 'Disable', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'set_dns_preferences' }, + }, + { + id: 'searchPaths', + title: 'Search Paths', + type: 'short-input', + placeholder: 'corp.example.com,internal.example.com', + condition: { field: 'operation', value: 'set_dns_searchpaths' }, + required: { field: 'operation', value: 'set_dns_searchpaths' }, + }, + { + id: 'keyId', + title: 'Auth Key ID', + type: 'short-input', + placeholder: 'Enter auth key ID', + condition: { field: 'operation', value: ['get_auth_key', 'delete_auth_key'] }, + required: { field: 'operation', value: ['get_auth_key', 'delete_auth_key'] }, + }, + { + id: 'reusable', + title: 'Reusable', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'create_auth_key' }, + mode: 'advanced', + }, + { + id: 'ephemeral', + title: 'Ephemeral', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'create_auth_key' }, + mode: 'advanced', + }, + { + id: 'preauthorized', + title: 'Preauthorized', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'create_auth_key' }, + mode: 'advanced', + }, + { + id: 'authKeyDescription', + title: 'Description', + type: 'short-input', + placeholder: 'Auth key description', + condition: { field: 'operation', value: 'create_auth_key' }, + mode: 'advanced', + }, + { + id: 'expirySeconds', + title: 'Expiry (seconds)', + type: 'short-input', + placeholder: '7776000 (90 days)', + condition: { field: 'operation', value: 'create_auth_key' }, + mode: 'advanced', + }, + ], + + tools: { + access: [ + 'tailscale_list_devices', + 'tailscale_get_device', + 'tailscale_delete_device', + 'tailscale_authorize_device', + 'tailscale_set_device_tags', + 'tailscale_get_device_routes', + 'tailscale_set_device_routes', + 'tailscale_update_device_key', + 'tailscale_list_dns_nameservers', + 'tailscale_set_dns_nameservers', + 'tailscale_get_dns_preferences', + 'tailscale_set_dns_preferences', + 'tailscale_get_dns_searchpaths', + 'tailscale_set_dns_searchpaths', + 'tailscale_list_users', + 'tailscale_create_auth_key', + 'tailscale_list_auth_keys', + 'tailscale_get_auth_key', + 'tailscale_delete_auth_key', + 'tailscale_get_acl', + ], + config: { + tool: (params) => `tailscale_${params.operation}`, + params: (params) => { + const mapped: Record = { + apiKey: params.apiKey, + tailnet: params.tailnet, + } + if (params.deviceId) mapped.deviceId = params.deviceId + if (params.keyId) mapped.keyId = params.keyId + if (params.tags) mapped.tags = params.tags + if (params.routes) mapped.routes = params.routes + if (params.dnsServers) mapped.dns = params.dnsServers + if (params.searchPaths) mapped.searchPaths = params.searchPaths + if (params.authorized !== undefined) mapped.authorized = params.authorized === 'true' + if (params.keyExpiryDisabled !== undefined) + mapped.keyExpiryDisabled = params.keyExpiryDisabled === 'true' + if (params.magicDNS !== undefined) mapped.magicDNS = params.magicDNS === 'true' + if (params.authKeyDescription) mapped.description = params.authKeyDescription + if (params.reusable !== undefined) mapped.reusable = params.reusable === 'true' + if (params.ephemeral !== undefined) mapped.ephemeral = params.ephemeral === 'true' + if (params.preauthorized !== undefined) + mapped.preauthorized = params.preauthorized === 'true' + if (params.expirySeconds) mapped.expirySeconds = Number(params.expirySeconds) + return mapped + }, + }, + }, + + inputs: { + apiKey: { type: 'string', description: 'Tailscale API key' }, + tailnet: { type: 'string', description: 'Tailnet name' }, + deviceId: { type: 'string', description: 'Device ID' }, + keyId: { type: 'string', description: 'Auth key ID' }, + authorized: { type: 'string', description: 'Authorization status' }, + keyExpiryDisabled: { type: 'string', description: 'Whether to disable key expiry' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + routes: { type: 'string', description: 'Comma-separated subnet routes' }, + dnsServers: { type: 'string', description: 'Comma-separated DNS nameserver IPs' }, + magicDNS: { type: 'string', description: 'Enable or disable MagicDNS' }, + searchPaths: { type: 'string', description: 'Comma-separated DNS search path domains' }, + reusable: { type: 'string', description: 'Whether the auth key is reusable' }, + ephemeral: { type: 'string', description: 'Whether devices are ephemeral' }, + preauthorized: { type: 'string', description: 'Whether devices are pre-authorized' }, + authKeyDescription: { type: 'string', description: 'Auth key description' }, + expirySeconds: { type: 'string', description: 'Auth key expiry in seconds' }, + }, + + outputs: { + devices: { type: 'json', description: 'List of devices in the tailnet' }, + count: { type: 'number', description: 'Total count of items returned' }, + id: { type: 'string', description: 'Device or auth key ID' }, + name: { type: 'string', description: 'Device name' }, + hostname: { type: 'string', description: 'Device hostname' }, + user: { type: 'string', description: 'Associated user' }, + os: { type: 'string', description: 'Operating system' }, + clientVersion: { type: 'string', description: 'Tailscale client version' }, + addresses: { type: 'json', description: 'Tailscale IP addresses' }, + tags: { type: 'json', description: 'Device or auth key tags' }, + authorized: { type: 'boolean', description: 'Whether the device is authorized' }, + blocksIncomingConnections: { + type: 'boolean', + description: 'Whether the device blocks incoming connections', + }, + lastSeen: { type: 'string', description: 'Last seen timestamp' }, + created: { type: 'string', description: 'Creation timestamp' }, + enabledRoutes: { type: 'json', description: 'Enabled subnet routes' }, + advertisedRoutes: { type: 'json', description: 'Advertised subnet routes' }, + isExternal: { type: 'boolean', description: 'Whether the device is external' }, + updateAvailable: { type: 'boolean', description: 'Whether an update is available' }, + machineKey: { type: 'string', description: 'Machine key' }, + nodeKey: { type: 'string', description: 'Node key' }, + success: { type: 'boolean', description: 'Whether the operation succeeded' }, + deviceId: { type: 'string', description: 'Device ID' }, + keyExpiryDisabled: { type: 'boolean', description: 'Whether key expiry is disabled' }, + dns: { type: 'json', description: 'DNS nameserver addresses' }, + magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' }, + searchPaths: { type: 'json', description: 'DNS search paths' }, + users: { type: 'json', description: 'List of users in the tailnet' }, + keys: { type: 'json', description: 'List of auth keys' }, + key: { type: 'string', description: 'Auth key value (only at creation)' }, + keyId: { type: 'string', description: 'Auth key ID' }, + description: { type: 'string', description: 'Auth key description' }, + expires: { type: 'string', description: 'Expiration timestamp' }, + revoked: { type: 'string', description: 'Revocation timestamp' }, + capabilities: { type: 'json', description: 'Auth key capabilities' }, + acl: { type: 'string', description: 'ACL policy as JSON string' }, + etag: { type: 'string', description: 'ACL ETag for conditional updates' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 1461cd58a6..bde265f8cf 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -175,6 +175,7 @@ import { StripeBlock } from '@/blocks/blocks/stripe' import { SttBlock, SttV2Block } from '@/blocks/blocks/stt' import { SupabaseBlock } from '@/blocks/blocks/supabase' import { TableBlock } from '@/blocks/blocks/table' +import { TailscaleBlock } from '@/blocks/blocks/tailscale' import { TavilyBlock } from '@/blocks/blocks/tavily' import { TelegramBlock } from '@/blocks/blocks/telegram' import { TextractBlock, TextractV2Block } from '@/blocks/blocks/textract' @@ -403,6 +404,7 @@ export const registry: Record = { stt_v2: SttV2Block, supabase: SupabaseBlock, table: TableBlock, + tailscale: TailscaleBlock, tavily: TavilyBlock, telegram: TelegramBlock, textract: TextractBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 6f53db86f8..cb3b0d1fcc 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -683,6 +683,45 @@ export function SerperIcon(props: SVGProps) { ) } +export function TailscaleIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function TavilyIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5c269a7c16..cc8d6492ea 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2261,6 +2261,28 @@ import { tableUpdateRowTool, tableUpsertRowTool, } from '@/tools/table' +import { + tailscaleAuthorizeDeviceTool, + tailscaleCreateAuthKeyTool, + tailscaleDeleteAuthKeyTool, + tailscaleDeleteDeviceTool, + tailscaleGetAclTool, + tailscaleGetAuthKeyTool, + tailscaleGetDeviceRoutesTool, + tailscaleGetDeviceTool, + tailscaleGetDnsPreferencesTool, + tailscaleGetDnsSearchpathsTool, + tailscaleListAuthKeysTool, + tailscaleListDevicesTool, + tailscaleListDnsNameserversTool, + tailscaleListUsersTool, + tailscaleSetDeviceRoutesTool, + tailscaleSetDeviceTagsTool, + tailscaleSetDnsNameserversTool, + tailscaleSetDnsPreferencesTool, + tailscaleSetDnsSearchpathsTool, + tailscaleUpdateDeviceKeyTool, +} from '@/tools/tailscale' import { tavilyCrawlTool, tavilyExtractTool, tavilyMapTool, tavilySearchTool } from '@/tools/tavily' import { telegramDeleteMessageTool, @@ -2964,6 +2986,26 @@ export const tools: Record = { supabase_storage_delete_bucket: supabaseStorageDeleteBucketTool, supabase_storage_get_public_url: supabaseStorageGetPublicUrlTool, supabase_storage_create_signed_url: supabaseStorageCreateSignedUrlTool, + tailscale_list_devices: tailscaleListDevicesTool, + tailscale_get_device: tailscaleGetDeviceTool, + tailscale_delete_device: tailscaleDeleteDeviceTool, + tailscale_authorize_device: tailscaleAuthorizeDeviceTool, + tailscale_set_device_tags: tailscaleSetDeviceTagsTool, + tailscale_get_device_routes: tailscaleGetDeviceRoutesTool, + tailscale_set_device_routes: tailscaleSetDeviceRoutesTool, + tailscale_update_device_key: tailscaleUpdateDeviceKeyTool, + tailscale_list_dns_nameservers: tailscaleListDnsNameserversTool, + tailscale_set_dns_nameservers: tailscaleSetDnsNameserversTool, + tailscale_get_dns_preferences: tailscaleGetDnsPreferencesTool, + tailscale_set_dns_preferences: tailscaleSetDnsPreferencesTool, + tailscale_get_dns_searchpaths: tailscaleGetDnsSearchpathsTool, + tailscale_set_dns_searchpaths: tailscaleSetDnsSearchpathsTool, + tailscale_list_users: tailscaleListUsersTool, + tailscale_create_auth_key: tailscaleCreateAuthKeyTool, + tailscale_list_auth_keys: tailscaleListAuthKeysTool, + tailscale_get_auth_key: tailscaleGetAuthKeyTool, + tailscale_delete_auth_key: tailscaleDeleteAuthKeyTool, + tailscale_get_acl: tailscaleGetAclTool, calendly_get_current_user: calendlyGetCurrentUserTool, calendly_list_event_types: calendlyListEventTypesTool, calendly_get_event_type: calendlyGetEventTypeTool, diff --git a/apps/sim/tools/tailscale/authorize_device.ts b/apps/sim/tools/tailscale/authorize_device.ts new file mode 100644 index 0000000000..f809fb95eb --- /dev/null +++ b/apps/sim/tools/tailscale/authorize_device.ts @@ -0,0 +1,78 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleAuthorizeDeviceParams, TailscaleAuthorizeDeviceResponse } from './types' + +export const tailscaleAuthorizeDeviceTool: ToolConfig< + TailscaleAuthorizeDeviceParams, + TailscaleAuthorizeDeviceResponse +> = { + id: 'tailscale_authorize_device', + name: 'Tailscale Authorize Device', + description: 'Authorize or deauthorize a device on the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID to authorize', + }, + authorized: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Whether to authorize (true) or deauthorize (false) the device', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/authorized`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + authorized: params.authorized, + }), + }, + + transformResponse: async (response: Response, params?: TailscaleAuthorizeDeviceParams) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { success: false, deviceId: '', authorized: false }, + error: (data as Record).message ?? 'Failed to authorize device', + } + } + + return { + success: true, + output: { + success: true, + deviceId: params?.deviceId ?? '', + authorized: params?.authorized ?? true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the operation succeeded' }, + deviceId: { type: 'string', description: 'Device ID' }, + authorized: { type: 'boolean', description: 'Authorization status after the operation' }, + }, +} diff --git a/apps/sim/tools/tailscale/create_auth_key.ts b/apps/sim/tools/tailscale/create_auth_key.ts new file mode 100644 index 0000000000..52ab595440 --- /dev/null +++ b/apps/sim/tools/tailscale/create_auth_key.ts @@ -0,0 +1,172 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleCreateAuthKeyParams, TailscaleCreateAuthKeyResponse } from './types' + +export const tailscaleCreateAuthKeyTool: ToolConfig< + TailscaleCreateAuthKeyParams, + TailscaleCreateAuthKeyResponse +> = { + id: 'tailscale_create_auth_key', + name: 'Tailscale Create Auth Key', + description: 'Create a new auth key for the tailnet to pre-authorize devices', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + reusable: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the key can be used more than once', + default: false, + }, + ephemeral: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether devices authenticated with this key are ephemeral', + default: false, + }, + preauthorized: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether devices are pre-authorized (skip manual approval)', + default: true, + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated list of tags for devices using this key (e.g., "tag:server,tag:prod")', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description for the auth key', + }, + expirySeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Key expiry time in seconds (default: 90 days)', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const tags = params.tags + ? params.tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean) + : [] + + const createCaps: Record = { + reusable: params.reusable ?? false, + ephemeral: params.ephemeral ?? false, + preauthorized: params.preauthorized ?? true, + } + + if (tags.length > 0) { + createCaps.tags = tags + } + + const body: Record = { + capabilities: { + devices: { + create: createCaps, + }, + }, + } + + if (params.description) body.description = params.description + if (params.expirySeconds !== undefined && params.expirySeconds !== null) + body.expirySeconds = params.expirySeconds + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { + id: '', + key: '', + description: '', + created: '', + expires: '', + revoked: '', + capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] }, + }, + error: (data as Record).message ?? 'Failed to create auth key', + } + } + + const data = await response.json() + const deviceCaps = data.capabilities?.devices?.create ?? {} + + return { + success: true, + output: { + id: data.id ?? null, + key: data.key ?? null, + description: data.description ?? null, + created: data.created ?? null, + expires: data.expires ?? null, + revoked: data.revoked ?? null, + capabilities: { + reusable: deviceCaps.reusable ?? false, + ephemeral: deviceCaps.ephemeral ?? false, + preauthorized: deviceCaps.preauthorized ?? false, + tags: deviceCaps.tags ?? [], + }, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Auth key ID' }, + key: { type: 'string', description: 'The auth key value (only shown once at creation)' }, + description: { type: 'string', description: 'Key description', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + expires: { type: 'string', description: 'Expiration timestamp' }, + revoked: { + type: 'string', + description: 'Revocation timestamp (empty if not revoked)', + optional: true, + }, + capabilities: { + type: 'object', + description: 'Key capabilities', + properties: { + reusable: { type: 'boolean', description: 'Whether the key is reusable' }, + ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' }, + preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' }, + tags: { type: 'array', description: 'Tags applied to devices using this key' }, + }, + }, + }, +} diff --git a/apps/sim/tools/tailscale/delete_auth_key.ts b/apps/sim/tools/tailscale/delete_auth_key.ts new file mode 100644 index 0000000000..d4f00c7539 --- /dev/null +++ b/apps/sim/tools/tailscale/delete_auth_key.ts @@ -0,0 +1,78 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface TailscaleDeleteAuthKeyParams { + apiKey: string + tailnet: string + keyId: string +} + +interface TailscaleDeleteAuthKeyResponse extends ToolResponse { + output: { + success: boolean + keyId: string + } +} + +export const tailscaleDeleteAuthKeyTool: ToolConfig< + TailscaleDeleteAuthKeyParams, + TailscaleDeleteAuthKeyResponse +> = { + id: 'tailscale_delete_auth_key', + name: 'Tailscale Delete Auth Key', + description: 'Revoke and delete an auth key', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + keyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Auth key ID to delete', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response: Response, params?: TailscaleDeleteAuthKeyParams) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { success: false, keyId: '' }, + error: (data as Record).message ?? 'Failed to delete auth key', + } + } + + return { + success: true, + output: { + success: true, + keyId: params?.keyId ?? '', + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the auth key was successfully deleted' }, + keyId: { type: 'string', description: 'ID of the deleted auth key' }, + }, +} diff --git a/apps/sim/tools/tailscale/delete_device.ts b/apps/sim/tools/tailscale/delete_device.ts new file mode 100644 index 0000000000..16671acec1 --- /dev/null +++ b/apps/sim/tools/tailscale/delete_device.ts @@ -0,0 +1,66 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleDeleteDeviceResponse, TailscaleDeviceParams } from './types' + +export const tailscaleDeleteDeviceTool: ToolConfig< + TailscaleDeviceParams, + TailscaleDeleteDeviceResponse +> = { + id: 'tailscale_delete_device', + name: 'Tailscale Delete Device', + description: 'Remove a device from the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID to delete', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response: Response, params?: TailscaleDeviceParams) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { success: false, deviceId: '' }, + error: (data as Record).message ?? 'Failed to delete device', + } + } + + return { + success: true, + output: { + success: true, + deviceId: params?.deviceId ?? '', + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the device was successfully deleted' }, + deviceId: { type: 'string', description: 'ID of the deleted device' }, + }, +} diff --git a/apps/sim/tools/tailscale/get_acl.ts b/apps/sim/tools/tailscale/get_acl.ts new file mode 100644 index 0000000000..6d51ccbec6 --- /dev/null +++ b/apps/sim/tools/tailscale/get_acl.ts @@ -0,0 +1,72 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { TailscaleBaseParams } from './types' + +interface TailscaleGetAclResponse extends ToolResponse { + output: { + acl: string + etag: string + } +} + +export const tailscaleGetAclTool: ToolConfig = { + id: 'tailscale_get_acl', + name: 'Tailscale Get ACL', + description: 'Get the current ACL policy for the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/acl`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { acl: '', etag: '' }, + error: (data as Record).message ?? 'Failed to get ACL', + } + } + + const etag = response.headers.get('ETag') ?? '' + const data = await response.json() + + return { + success: true, + output: { + acl: JSON.stringify(data, null, 2), + etag, + }, + } + }, + + outputs: { + acl: { type: 'string', description: 'ACL policy as JSON string' }, + etag: { + type: 'string', + description: 'ETag for the current ACL version (use with If-Match header for updates)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tailscale/get_auth_key.ts b/apps/sim/tools/tailscale/get_auth_key.ts new file mode 100644 index 0000000000..5b4d200c03 --- /dev/null +++ b/apps/sim/tools/tailscale/get_auth_key.ts @@ -0,0 +1,119 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface TailscaleGetAuthKeyParams { + apiKey: string + tailnet: string + keyId: string +} + +interface TailscaleGetAuthKeyResponse extends ToolResponse { + output: { + id: string + description: string + created: string + expires: string + revoked: string + capabilities: { + reusable: boolean + ephemeral: boolean + preauthorized: boolean + tags: string[] + } + } +} + +export const tailscaleGetAuthKeyTool: ToolConfig< + TailscaleGetAuthKeyParams, + TailscaleGetAuthKeyResponse +> = { + id: 'tailscale_get_auth_key', + name: 'Tailscale Get Auth Key', + description: 'Get details of a specific auth key', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + keyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Auth key ID', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { + id: '', + description: '', + created: '', + expires: '', + revoked: '', + capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] }, + }, + error: (data as Record).message ?? 'Failed to get auth key', + } + } + + const data = await response.json() + const deviceCaps = data.capabilities?.devices?.create ?? {} + + return { + success: true, + output: { + id: data.id ?? null, + description: data.description ?? null, + created: data.created ?? null, + expires: data.expires ?? null, + revoked: data.revoked ?? null, + capabilities: { + reusable: deviceCaps.reusable ?? false, + ephemeral: deviceCaps.ephemeral ?? false, + preauthorized: deviceCaps.preauthorized ?? false, + tags: deviceCaps.tags ?? [], + }, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Auth key ID' }, + description: { type: 'string', description: 'Key description', optional: true }, + created: { type: 'string', description: 'Creation timestamp' }, + expires: { type: 'string', description: 'Expiration timestamp' }, + revoked: { type: 'string', description: 'Revocation timestamp', optional: true }, + capabilities: { + type: 'object', + description: 'Key capabilities', + properties: { + reusable: { type: 'boolean', description: 'Whether the key is reusable' }, + ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' }, + preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' }, + tags: { type: 'array', description: 'Tags applied to devices using this key' }, + }, + }, + }, +} diff --git a/apps/sim/tools/tailscale/get_device.ts b/apps/sim/tools/tailscale/get_device.ts new file mode 100644 index 0000000000..fe3ba670a7 --- /dev/null +++ b/apps/sim/tools/tailscale/get_device.ts @@ -0,0 +1,121 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleDeviceParams, TailscaleGetDeviceResponse } from './types' + +export const tailscaleGetDeviceTool: ToolConfig = + { + id: 'tailscale_get_device', + name: 'Tailscale Get Device', + description: 'Get details of a specific device by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { + id: '', + name: '', + hostname: '', + user: '', + os: '', + clientVersion: '', + addresses: [], + tags: [], + authorized: false, + blocksIncomingConnections: false, + lastSeen: '', + created: '', + isExternal: false, + updateAvailable: false, + machineKey: '', + nodeKey: '', + }, + error: (data as Record).message ?? 'Failed to get device', + } + } + + const data = await response.json() + return { + success: true, + output: { + id: data.id ?? null, + name: data.name ?? null, + hostname: data.hostname ?? null, + user: data.user ?? null, + os: data.os ?? null, + clientVersion: data.clientVersion ?? null, + addresses: data.addresses ?? [], + tags: data.tags ?? [], + authorized: data.authorized ?? false, + blocksIncomingConnections: data.blocksIncomingConnections ?? false, + lastSeen: data.lastSeen ?? null, + created: data.created ?? null, + isExternal: data.isExternal ?? false, + updateAvailable: data.updateAvailable ?? false, + machineKey: data.machineKey ?? null, + nodeKey: data.nodeKey ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Device ID' }, + name: { type: 'string', description: 'Device name' }, + hostname: { type: 'string', description: 'Device hostname' }, + user: { type: 'string', description: 'Associated user' }, + os: { type: 'string', description: 'Operating system' }, + clientVersion: { type: 'string', description: 'Tailscale client version' }, + addresses: { type: 'array', description: 'Tailscale IP addresses' }, + tags: { type: 'array', description: 'Device tags' }, + authorized: { type: 'boolean', description: 'Whether the device is authorized' }, + blocksIncomingConnections: { + type: 'boolean', + description: 'Whether the device blocks incoming connections', + }, + lastSeen: { type: 'string', description: 'Last seen timestamp' }, + created: { type: 'string', description: 'Creation timestamp' }, + isExternal: { + type: 'boolean', + description: 'Whether the device is external', + optional: true, + }, + updateAvailable: { + type: 'boolean', + description: 'Whether an update is available', + optional: true, + }, + machineKey: { type: 'string', description: 'Machine key', optional: true }, + nodeKey: { type: 'string', description: 'Node key', optional: true }, + }, + } diff --git a/apps/sim/tools/tailscale/get_device_routes.ts b/apps/sim/tools/tailscale/get_device_routes.ts new file mode 100644 index 0000000000..2d5b540750 --- /dev/null +++ b/apps/sim/tools/tailscale/get_device_routes.ts @@ -0,0 +1,67 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleDeviceParams, TailscaleGetDeviceRoutesResponse } from './types' + +export const tailscaleGetDeviceRoutesTool: ToolConfig< + TailscaleDeviceParams, + TailscaleGetDeviceRoutesResponse +> = { + id: 'tailscale_get_device_routes', + name: 'Tailscale Get Device Routes', + description: 'Get the subnet routes for a device', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { advertisedRoutes: [], enabledRoutes: [] }, + error: (data as Record).message ?? 'Failed to get device routes', + } + } + + const data = await response.json() + return { + success: true, + output: { + advertisedRoutes: data.advertisedRoutes ?? [], + enabledRoutes: data.enabledRoutes ?? [], + }, + } + }, + + outputs: { + advertisedRoutes: { type: 'array', description: 'Subnet routes the device is advertising' }, + enabledRoutes: { type: 'array', description: 'Subnet routes that are approved/enabled' }, + }, +} diff --git a/apps/sim/tools/tailscale/get_dns_preferences.ts b/apps/sim/tools/tailscale/get_dns_preferences.ts new file mode 100644 index 0000000000..248f962a19 --- /dev/null +++ b/apps/sim/tools/tailscale/get_dns_preferences.ts @@ -0,0 +1,65 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { TailscaleBaseParams } from './types' + +interface TailscaleGetDnsPreferencesResponse extends ToolResponse { + output: { + magicDNS: boolean + } +} + +export const tailscaleGetDnsPreferencesTool: ToolConfig< + TailscaleBaseParams, + TailscaleGetDnsPreferencesResponse +> = { + id: 'tailscale_get_dns_preferences', + name: 'Tailscale Get DNS Preferences', + description: 'Get the DNS preferences for the tailnet including MagicDNS status', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { magicDNS: false }, + error: (data as Record).message ?? 'Failed to get DNS preferences', + } + } + + const data = await response.json() + return { + success: true, + output: { + magicDNS: data.magicDNS ?? false, + }, + } + }, + + outputs: { + magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' }, + }, +} diff --git a/apps/sim/tools/tailscale/get_dns_searchpaths.ts b/apps/sim/tools/tailscale/get_dns_searchpaths.ts new file mode 100644 index 0000000000..a5f8e54b02 --- /dev/null +++ b/apps/sim/tools/tailscale/get_dns_searchpaths.ts @@ -0,0 +1,65 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { TailscaleBaseParams } from './types' + +interface TailscaleGetDnsSearchpathsResponse extends ToolResponse { + output: { + searchPaths: string[] + } +} + +export const tailscaleGetDnsSearchpathsTool: ToolConfig< + TailscaleBaseParams, + TailscaleGetDnsSearchpathsResponse +> = { + id: 'tailscale_get_dns_searchpaths', + name: 'Tailscale Get DNS Search Paths', + description: 'Get the DNS search paths configured for the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { searchPaths: [] }, + error: (data as Record).message ?? 'Failed to get DNS search paths', + } + } + + const data = await response.json() + return { + success: true, + output: { + searchPaths: data.searchPaths ?? [], + }, + } + }, + + outputs: { + searchPaths: { type: 'array', description: 'List of DNS search path domains' }, + }, +} diff --git a/apps/sim/tools/tailscale/index.ts b/apps/sim/tools/tailscale/index.ts new file mode 100644 index 0000000000..b334bb12cf --- /dev/null +++ b/apps/sim/tools/tailscale/index.ts @@ -0,0 +1,21 @@ +export { tailscaleAuthorizeDeviceTool } from './authorize_device' +export { tailscaleCreateAuthKeyTool } from './create_auth_key' +export { tailscaleDeleteAuthKeyTool } from './delete_auth_key' +export { tailscaleDeleteDeviceTool } from './delete_device' +export { tailscaleGetAclTool } from './get_acl' +export { tailscaleGetAuthKeyTool } from './get_auth_key' +export { tailscaleGetDeviceTool } from './get_device' +export { tailscaleGetDeviceRoutesTool } from './get_device_routes' +export { tailscaleGetDnsPreferencesTool } from './get_dns_preferences' +export { tailscaleGetDnsSearchpathsTool } from './get_dns_searchpaths' +export { tailscaleListAuthKeysTool } from './list_auth_keys' +export { tailscaleListDevicesTool } from './list_devices' +export { tailscaleListDnsNameserversTool } from './list_dns_nameservers' +export { tailscaleListUsersTool } from './list_users' +export { tailscaleSetDeviceRoutesTool } from './set_device_routes' +export { tailscaleSetDeviceTagsTool } from './set_device_tags' +export { tailscaleSetDnsNameserversTool } from './set_dns_nameservers' +export { tailscaleSetDnsPreferencesTool } from './set_dns_preferences' +export { tailscaleSetDnsSearchpathsTool } from './set_dns_searchpaths' +export * from './types' +export { tailscaleUpdateDeviceKeyTool } from './update_device_key' diff --git a/apps/sim/tools/tailscale/list_auth_keys.ts b/apps/sim/tools/tailscale/list_auth_keys.ts new file mode 100644 index 0000000000..2e94308e89 --- /dev/null +++ b/apps/sim/tools/tailscale/list_auth_keys.ts @@ -0,0 +1,126 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { TailscaleBaseParams } from './types' + +interface TailscaleAuthKeyOutput { + id: string + description: string + created: string + expires: string + revoked: string + capabilities: { + reusable: boolean + ephemeral: boolean + preauthorized: boolean + tags: string[] + } +} + +interface TailscaleListAuthKeysResponse extends ToolResponse { + output: { + keys: TailscaleAuthKeyOutput[] + count: number + } +} + +export const tailscaleListAuthKeysTool: ToolConfig< + TailscaleBaseParams, + TailscaleListAuthKeysResponse +> = { + id: 'tailscale_list_auth_keys', + name: 'Tailscale List Auth Keys', + description: 'List all auth keys in the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { keys: [], count: 0 }, + error: (data as Record).message ?? 'Failed to list auth keys', + } + } + + const data = await response.json() + const keys = (data.keys ?? []).map((key: Record) => { + const caps = (key.capabilities as Record)?.devices as Record + const create = caps?.create as Record + return { + id: (key.id as string) ?? null, + description: (key.description as string) ?? null, + created: (key.created as string) ?? null, + expires: (key.expires as string) ?? null, + revoked: (key.revoked as string) ?? null, + capabilities: { + reusable: (create?.reusable as boolean) ?? false, + ephemeral: (create?.ephemeral as boolean) ?? false, + preauthorized: (create?.preauthorized as boolean) ?? false, + tags: (create?.tags as string[]) ?? [], + }, + } + }) + + return { + success: true, + output: { + keys, + count: keys.length, + }, + } + }, + + outputs: { + keys: { + type: 'array', + description: 'List of auth keys', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Auth key ID' }, + description: { type: 'string', description: 'Key description' }, + created: { type: 'string', description: 'Creation timestamp' }, + expires: { type: 'string', description: 'Expiration timestamp' }, + revoked: { type: 'string', description: 'Revocation timestamp' }, + capabilities: { + type: 'object', + description: 'Key capabilities', + properties: { + reusable: { type: 'boolean', description: 'Whether the key is reusable' }, + ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' }, + preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' }, + tags: { type: 'array', description: 'Tags applied to devices' }, + }, + }, + }, + }, + }, + count: { + type: 'number', + description: 'Total number of auth keys', + }, + }, +} diff --git a/apps/sim/tools/tailscale/list_devices.ts b/apps/sim/tools/tailscale/list_devices.ts new file mode 100644 index 0000000000..b55835d482 --- /dev/null +++ b/apps/sim/tools/tailscale/list_devices.ts @@ -0,0 +1,102 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleBaseParams, TailscaleListDevicesResponse } from './types' + +export const tailscaleListDevicesTool: ToolConfig< + TailscaleBaseParams, + TailscaleListDevicesResponse +> = { + id: 'tailscale_list_devices', + name: 'Tailscale List Devices', + description: 'List all devices in the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/devices`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { devices: [], count: 0 }, + error: (data as Record).message ?? 'Failed to list devices', + } + } + + const data = await response.json() + const devices = (data.devices ?? []).map((device: Record) => ({ + id: (device.id as string) ?? null, + name: (device.name as string) ?? null, + hostname: (device.hostname as string) ?? null, + user: (device.user as string) ?? null, + os: (device.os as string) ?? null, + clientVersion: (device.clientVersion as string) ?? null, + addresses: (device.addresses as string[]) ?? [], + tags: (device.tags as string[]) ?? [], + authorized: (device.authorized as boolean) ?? false, + blocksIncomingConnections: (device.blocksIncomingConnections as boolean) ?? false, + lastSeen: (device.lastSeen as string) ?? null, + created: (device.created as string) ?? null, + })) + + return { + success: true, + output: { + devices, + count: devices.length, + }, + } + }, + + outputs: { + devices: { + type: 'array', + description: 'List of devices in the tailnet', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Device ID' }, + name: { type: 'string', description: 'Device name' }, + hostname: { type: 'string', description: 'Device hostname' }, + user: { type: 'string', description: 'Associated user' }, + os: { type: 'string', description: 'Operating system' }, + clientVersion: { type: 'string', description: 'Tailscale client version' }, + addresses: { type: 'array', description: 'Tailscale IP addresses' }, + tags: { type: 'array', description: 'Device tags' }, + authorized: { type: 'boolean', description: 'Whether the device is authorized' }, + blocksIncomingConnections: { + type: 'boolean', + description: 'Whether the device blocks incoming connections', + }, + lastSeen: { type: 'string', description: 'Last seen timestamp' }, + created: { type: 'string', description: 'Creation timestamp' }, + }, + }, + }, + count: { + type: 'number', + description: 'Total number of devices', + }, + }, +} diff --git a/apps/sim/tools/tailscale/list_dns_nameservers.ts b/apps/sim/tools/tailscale/list_dns_nameservers.ts new file mode 100644 index 0000000000..67b0ac6745 --- /dev/null +++ b/apps/sim/tools/tailscale/list_dns_nameservers.ts @@ -0,0 +1,61 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleBaseParams, TailscaleListDnsNameserversResponse } from './types' + +export const tailscaleListDnsNameserversTool: ToolConfig< + TailscaleBaseParams, + TailscaleListDnsNameserversResponse +> = { + id: 'tailscale_list_dns_nameservers', + name: 'Tailscale List DNS Nameservers', + description: 'Get the DNS nameservers configured for the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { dns: [], magicDNS: false }, + error: (data as Record).message ?? 'Failed to list DNS nameservers', + } + } + + const data = await response.json() + return { + success: true, + output: { + dns: data.dns ?? [], + magicDNS: data.magicDNS ?? false, + }, + } + }, + + outputs: { + dns: { type: 'array', description: 'List of DNS nameserver addresses' }, + magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' }, + }, +} diff --git a/apps/sim/tools/tailscale/list_users.ts b/apps/sim/tools/tailscale/list_users.ts new file mode 100644 index 0000000000..100719d637 --- /dev/null +++ b/apps/sim/tools/tailscale/list_users.ts @@ -0,0 +1,96 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleBaseParams, TailscaleListUsersResponse } from './types' + +export const tailscaleListUsersTool: ToolConfig = { + id: 'tailscale_list_users', + name: 'Tailscale List Users', + description: 'List all users in the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/users`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { users: [], count: 0 }, + error: (data as Record).message ?? 'Failed to list users', + } + } + + const data = await response.json() + const users = (data.users ?? []).map((user: Record) => ({ + id: (user.id as string) ?? null, + displayName: (user.displayName as string) ?? null, + loginName: (user.loginName as string) ?? null, + profilePicURL: (user.profilePicURL as string) ?? null, + role: (user.role as string) ?? null, + status: (user.status as string) ?? null, + type: (user.type as string) ?? null, + created: (user.created as string) ?? null, + lastSeen: (user.lastSeen as string) ?? null, + deviceCount: (user.deviceCount as number) ?? 0, + })) + + return { + success: true, + output: { + users, + count: users.length, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'List of users in the tailnet', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + displayName: { type: 'string', description: 'Display name' }, + loginName: { type: 'string', description: 'Login name / email' }, + profilePicURL: { type: 'string', description: 'Profile picture URL', optional: true }, + role: { type: 'string', description: 'User role (owner, admin, member, etc.)' }, + status: { type: 'string', description: 'User status (active, suspended, etc.)' }, + type: { type: 'string', description: 'User type (member, shared, tagged)' }, + created: { type: 'string', description: 'Creation timestamp' }, + lastSeen: { type: 'string', description: 'Last seen timestamp', optional: true }, + deviceCount: { + type: 'number', + description: 'Number of devices owned by user', + optional: true, + }, + }, + }, + }, + count: { + type: 'number', + description: 'Total number of users', + }, + }, +} diff --git a/apps/sim/tools/tailscale/set_device_routes.ts b/apps/sim/tools/tailscale/set_device_routes.ts new file mode 100644 index 0000000000..49b3ba3ca3 --- /dev/null +++ b/apps/sim/tools/tailscale/set_device_routes.ts @@ -0,0 +1,81 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleSetDeviceRoutesParams, TailscaleSetDeviceRoutesResponse } from './types' + +export const tailscaleSetDeviceRoutesTool: ToolConfig< + TailscaleSetDeviceRoutesParams, + TailscaleSetDeviceRoutesResponse +> = { + id: 'tailscale_set_device_routes', + name: 'Tailscale Set Device Routes', + description: 'Set the enabled subnet routes for a device', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID', + }, + routes: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated list of subnet routes to enable (e.g., "10.0.0.0/24,192.168.1.0/24")', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + routes: params.routes + .split(',') + .map((r) => r.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { advertisedRoutes: [], enabledRoutes: [] }, + error: (data as Record).message ?? 'Failed to set device routes', + } + } + + const data = await response.json() + return { + success: true, + output: { + advertisedRoutes: data.advertisedRoutes ?? [], + enabledRoutes: data.enabledRoutes ?? [], + }, + } + }, + + outputs: { + advertisedRoutes: { type: 'array', description: 'Subnet routes the device is advertising' }, + enabledRoutes: { type: 'array', description: 'Subnet routes that are now enabled' }, + }, +} diff --git a/apps/sim/tools/tailscale/set_device_tags.ts b/apps/sim/tools/tailscale/set_device_tags.ts new file mode 100644 index 0000000000..b760a7b79c --- /dev/null +++ b/apps/sim/tools/tailscale/set_device_tags.ts @@ -0,0 +1,88 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleSetDeviceTagsParams, TailscaleSetDeviceTagsResponse } from './types' + +export const tailscaleSetDeviceTagsTool: ToolConfig< + TailscaleSetDeviceTagsParams, + TailscaleSetDeviceTagsResponse +> = { + id: 'tailscale_set_device_tags', + name: 'Tailscale Set Device Tags', + description: 'Set tags on a device in the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID', + }, + tags: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags (e.g., "tag:server,tag:production")', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/tags`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + tags: params.tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response: Response, params?: TailscaleSetDeviceTagsParams) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { success: false, deviceId: '', tags: [] }, + error: (data as Record).message ?? 'Failed to set device tags', + } + } + + const tags = params?.tags + ? params.tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean) + : [] + + return { + success: true, + output: { + success: true, + deviceId: params?.deviceId ?? '', + tags, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the tags were successfully set' }, + deviceId: { type: 'string', description: 'Device ID' }, + tags: { type: 'array', description: 'Tags set on the device' }, + }, +} diff --git a/apps/sim/tools/tailscale/set_dns_nameservers.ts b/apps/sim/tools/tailscale/set_dns_nameservers.ts new file mode 100644 index 0000000000..52ecbe7ece --- /dev/null +++ b/apps/sim/tools/tailscale/set_dns_nameservers.ts @@ -0,0 +1,86 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface TailscaleSetDnsNameserversParams { + apiKey: string + tailnet: string + dns: string +} + +interface TailscaleSetDnsNameserversResponse extends ToolResponse { + output: { + dns: string[] + magicDNS: boolean + } +} + +export const tailscaleSetDnsNameserversTool: ToolConfig< + TailscaleSetDnsNameserversParams, + TailscaleSetDnsNameserversResponse +> = { + id: 'tailscale_set_dns_nameservers', + name: 'Tailscale Set DNS Nameservers', + description: 'Set the DNS nameservers for the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + dns: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated list of DNS nameserver IP addresses (e.g., "8.8.8.8,8.8.4.4")', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + dns: params.dns + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { dns: [], magicDNS: false }, + error: (data as Record).message ?? 'Failed to set DNS nameservers', + } + } + + const data = await response.json() + return { + success: true, + output: { + dns: data.dns ?? [], + magicDNS: data.magicDNS ?? false, + }, + } + }, + + outputs: { + dns: { type: 'array', description: 'Updated list of DNS nameserver addresses' }, + magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' }, + }, +} diff --git a/apps/sim/tools/tailscale/set_dns_preferences.ts b/apps/sim/tools/tailscale/set_dns_preferences.ts new file mode 100644 index 0000000000..d39bafbb82 --- /dev/null +++ b/apps/sim/tools/tailscale/set_dns_preferences.ts @@ -0,0 +1,80 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface TailscaleSetDnsPreferencesParams { + apiKey: string + tailnet: string + magicDNS: boolean +} + +interface TailscaleSetDnsPreferencesResponse extends ToolResponse { + output: { + magicDNS: boolean + } +} + +export const tailscaleSetDnsPreferencesTool: ToolConfig< + TailscaleSetDnsPreferencesParams, + TailscaleSetDnsPreferencesResponse +> = { + id: 'tailscale_set_dns_preferences', + name: 'Tailscale Set DNS Preferences', + description: 'Set DNS preferences for the tailnet (enable/disable MagicDNS)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + magicDNS: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Whether to enable (true) or disable (false) MagicDNS', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + magicDNS: params.magicDNS, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { magicDNS: false }, + error: (data as Record).message ?? 'Failed to set DNS preferences', + } + } + + const data = await response.json() + return { + success: true, + output: { + magicDNS: data.magicDNS ?? false, + }, + } + }, + + outputs: { + magicDNS: { type: 'boolean', description: 'Updated MagicDNS status' }, + }, +} diff --git a/apps/sim/tools/tailscale/set_dns_searchpaths.ts b/apps/sim/tools/tailscale/set_dns_searchpaths.ts new file mode 100644 index 0000000000..31d52f0328 --- /dev/null +++ b/apps/sim/tools/tailscale/set_dns_searchpaths.ts @@ -0,0 +1,84 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface TailscaleSetDnsSearchpathsParams { + apiKey: string + tailnet: string + searchPaths: string +} + +interface TailscaleSetDnsSearchpathsResponse extends ToolResponse { + output: { + searchPaths: string[] + } +} + +export const tailscaleSetDnsSearchpathsTool: ToolConfig< + TailscaleSetDnsSearchpathsParams, + TailscaleSetDnsSearchpathsResponse +> = { + id: 'tailscale_set_dns_searchpaths', + name: 'Tailscale Set DNS Search Paths', + description: 'Set the DNS search paths for the tailnet', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + searchPaths: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated list of DNS search path domains (e.g., "corp.example.com,internal.example.com")', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + searchPaths: params.searchPaths + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { searchPaths: [] }, + error: (data as Record).message ?? 'Failed to set DNS search paths', + } + } + + const data = await response.json() + return { + success: true, + output: { + searchPaths: data.searchPaths ?? [], + }, + } + }, + + outputs: { + searchPaths: { type: 'array', description: 'Updated list of DNS search path domains' }, + }, +} diff --git a/apps/sim/tools/tailscale/types.ts b/apps/sim/tools/tailscale/types.ts new file mode 100644 index 0000000000..80c8180bf9 --- /dev/null +++ b/apps/sim/tools/tailscale/types.ts @@ -0,0 +1,155 @@ +import type { ToolResponse } from '@/tools/types' + +export interface TailscaleBaseParams { + apiKey: string + tailnet: string +} + +export interface TailscaleDeviceParams extends TailscaleBaseParams { + deviceId: string +} + +export interface TailscaleSetDeviceTagsParams extends TailscaleDeviceParams { + tags: string +} + +export interface TailscaleAuthorizeDeviceParams extends TailscaleDeviceParams { + authorized: boolean +} + +export interface TailscaleSetDeviceRoutesParams extends TailscaleDeviceParams { + routes: string +} + +export interface TailscaleCreateAuthKeyParams extends TailscaleBaseParams { + reusable: boolean + ephemeral: boolean + preauthorized: boolean + tags?: string + description?: string + expirySeconds?: number +} + +export interface TailscaleDeviceOutput { + id: string + name: string + hostname: string + user: string + os: string + clientVersion: string + addresses: string[] + tags: string[] + authorized: boolean + blocksIncomingConnections: boolean + lastSeen: string + created: string +} + +export interface TailscaleUserOutput { + id: string + displayName: string + loginName: string + profilePicURL: string + role: string + status: string + type: string + created: string + lastSeen: string + deviceCount: number +} + +export interface TailscaleListDevicesResponse extends ToolResponse { + output: { + devices: TailscaleDeviceOutput[] + count: number + } +} + +export interface TailscaleGetDeviceResponse extends ToolResponse { + output: TailscaleDeviceOutput & { + isExternal: boolean + updateAvailable: boolean + machineKey: string + nodeKey: string + } +} + +export interface TailscaleUpdateDeviceKeyParams extends TailscaleDeviceParams { + keyExpiryDisabled: boolean +} + +export interface TailscaleUpdateDeviceKeyResponse extends ToolResponse { + output: { + success: boolean + deviceId: string + keyExpiryDisabled: boolean + } +} + +export interface TailscaleDeleteDeviceResponse extends ToolResponse { + output: { + success: boolean + deviceId: string + } +} + +export interface TailscaleAuthorizeDeviceResponse extends ToolResponse { + output: { + success: boolean + deviceId: string + authorized: boolean + } +} + +export interface TailscaleSetDeviceTagsResponse extends ToolResponse { + output: { + success: boolean + deviceId: string + tags: string[] + } +} + +export interface TailscaleGetDeviceRoutesResponse extends ToolResponse { + output: { + advertisedRoutes: string[] + enabledRoutes: string[] + } +} + +export interface TailscaleSetDeviceRoutesResponse extends ToolResponse { + output: { + advertisedRoutes: string[] + enabledRoutes: string[] + } +} + +export interface TailscaleListDnsNameserversResponse extends ToolResponse { + output: { + dns: string[] + magicDNS: boolean + } +} + +export interface TailscaleListUsersResponse extends ToolResponse { + output: { + users: TailscaleUserOutput[] + count: number + } +} + +export interface TailscaleCreateAuthKeyResponse extends ToolResponse { + output: { + id: string + key: string + description: string + created: string + expires: string + revoked: string + capabilities: { + reusable: boolean + ephemeral: boolean + preauthorized: boolean + tags: string[] + } + } +} diff --git a/apps/sim/tools/tailscale/update_device_key.ts b/apps/sim/tools/tailscale/update_device_key.ts new file mode 100644 index 0000000000..a26ff3b3ad --- /dev/null +++ b/apps/sim/tools/tailscale/update_device_key.ts @@ -0,0 +1,78 @@ +import type { ToolConfig } from '@/tools/types' +import type { TailscaleUpdateDeviceKeyParams, TailscaleUpdateDeviceKeyResponse } from './types' + +export const tailscaleUpdateDeviceKeyTool: ToolConfig< + TailscaleUpdateDeviceKeyParams, + TailscaleUpdateDeviceKeyResponse +> = { + id: 'tailscale_update_device_key', + name: 'Tailscale Update Device Key', + description: 'Enable or disable key expiry on a device', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tailscale API key', + }, + tailnet: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tailnet name (e.g., example.com) or "-" for default', + }, + deviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Device ID', + }, + keyExpiryDisabled: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Whether to disable key expiry (true) or enable it (false)', + }, + }, + + request: { + url: (params) => + `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/key`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + keyExpiryDisabled: params.keyExpiryDisabled, + }), + }, + + transformResponse: async (response: Response, params?: TailscaleUpdateDeviceKeyParams) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return { + success: false, + output: { success: false, deviceId: '', keyExpiryDisabled: false }, + error: (data as Record).message ?? 'Failed to update device key', + } + } + + return { + success: true, + output: { + success: true, + deviceId: params?.deviceId ?? '', + keyExpiryDisabled: params?.keyExpiryDisabled ?? true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the operation succeeded' }, + deviceId: { type: 'string', description: 'Device ID' }, + keyExpiryDisabled: { type: 'boolean', description: 'Whether key expiry is now disabled' }, + }, +}