From 1457d8588257cfa34a0624578dd7a685a752ff5a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 31 Mar 2026 15:55:38 -0700 Subject: [PATCH 1/3] feat(tailscale): add Tailscale integration with 20 API operations --- apps/docs/components/icons.tsx | 39 ++ apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + apps/docs/content/docs/en/tools/tailscale.mdx | 490 ++++++++++++++++++ .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 99 ++++ apps/sim/blocks/blocks/tailscale.ts | 342 ++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 39 ++ apps/sim/tools/registry.ts | 42 ++ apps/sim/tools/tailscale/authorize_device.ts | 79 +++ apps/sim/tools/tailscale/create_auth_key.ts | 171 ++++++ apps/sim/tools/tailscale/delete_auth_key.ts | 78 +++ apps/sim/tools/tailscale/delete_device.ts | 66 +++ apps/sim/tools/tailscale/get_acl.ts | 72 +++ apps/sim/tools/tailscale/get_auth_key.ts | 119 +++++ apps/sim/tools/tailscale/get_device.ts | 121 +++++ apps/sim/tools/tailscale/get_device_routes.ts | 67 +++ .../tools/tailscale/get_dns_preferences.ts | 65 +++ .../tools/tailscale/get_dns_searchpaths.ts | 65 +++ apps/sim/tools/tailscale/index.ts | 21 + apps/sim/tools/tailscale/list_auth_keys.ts | 126 +++++ apps/sim/tools/tailscale/list_devices.ts | 102 ++++ .../tools/tailscale/list_dns_nameservers.ts | 61 +++ apps/sim/tools/tailscale/list_users.ts | 96 ++++ apps/sim/tools/tailscale/set_device_routes.ts | 81 +++ apps/sim/tools/tailscale/set_device_tags.ts | 89 ++++ .../tools/tailscale/set_dns_nameservers.ts | 86 +++ .../tools/tailscale/set_dns_preferences.ts | 80 +++ .../tools/tailscale/set_dns_searchpaths.ts | 84 +++ apps/sim/tools/tailscale/types.ts | 155 ++++++ apps/sim/tools/tailscale/update_device_key.ts | 79 +++ 32 files changed, 3021 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/tailscale.mdx create mode 100644 apps/sim/blocks/blocks/tailscale.ts create mode 100644 apps/sim/tools/tailscale/authorize_device.ts create mode 100644 apps/sim/tools/tailscale/create_auth_key.ts create mode 100644 apps/sim/tools/tailscale/delete_auth_key.ts create mode 100644 apps/sim/tools/tailscale/delete_device.ts create mode 100644 apps/sim/tools/tailscale/get_acl.ts create mode 100644 apps/sim/tools/tailscale/get_auth_key.ts create mode 100644 apps/sim/tools/tailscale/get_device.ts create mode 100644 apps/sim/tools/tailscale/get_device_routes.ts create mode 100644 apps/sim/tools/tailscale/get_dns_preferences.ts create mode 100644 apps/sim/tools/tailscale/get_dns_searchpaths.ts create mode 100644 apps/sim/tools/tailscale/index.ts create mode 100644 apps/sim/tools/tailscale/list_auth_keys.ts create mode 100644 apps/sim/tools/tailscale/list_devices.ts create mode 100644 apps/sim/tools/tailscale/list_dns_nameservers.ts create mode 100644 apps/sim/tools/tailscale/list_users.ts create mode 100644 apps/sim/tools/tailscale/set_device_routes.ts create mode 100644 apps/sim/tools/tailscale/set_device_tags.ts create mode 100644 apps/sim/tools/tailscale/set_dns_nameservers.ts create mode 100644 apps/sim/tools/tailscale/set_dns_preferences.ts create mode 100644 apps/sim/tools/tailscale/set_dns_searchpaths.ts create mode 100644 apps/sim/tools/tailscale/types.ts create mode 100644 apps/sim/tools/tailscale/update_device_key.ts 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..81a5347b60 --- /dev/null +++ b/apps/sim/blocks/blocks/tailscale.ts @@ -0,0 +1,342 @@ +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: { + response: { + type: { + devices: 'json', + count: 'number', + id: 'string', + name: 'string', + hostname: 'string', + user: 'string', + os: 'string', + clientVersion: 'string', + addresses: 'json', + tags: 'json', + authorized: 'boolean', + blocksIncomingConnections: 'boolean', + lastSeen: 'string', + created: 'string', + enabledRoutes: 'json', + advertisedRoutes: 'json', + isExternal: 'boolean', + updateAvailable: 'boolean', + machineKey: 'string', + nodeKey: 'string', + success: 'boolean', + deviceId: 'string', + keyExpiryDisabled: 'boolean', + dns: 'json', + magicDNS: 'boolean', + searchPaths: 'json', + users: 'json', + keys: 'json', + key: 'string', + keyId: 'string', + description: 'string', + expires: 'string', + revoked: 'string', + capabilities: 'json', + acl: 'string', + etag: 'string', + }, + }, + }, +} 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..861336a064 --- /dev/null +++ b/apps/sim/tools/tailscale/authorize_device.ts @@ -0,0 +1,79 @@ +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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + authorized: params.authorized, + }), + }, + + transformResponse: async (response, _ctx, params) => { + const typedParams = params as { deviceId?: string; authorized?: boolean } + 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: typedParams?.deviceId ?? '', + authorized: typedParams?.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..9bae8d2bd0 --- /dev/null +++ b/apps/sim/tools/tailscale/create_auth_key.ts @@ -0,0 +1,171 @@ +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}`, + '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) body.expirySeconds = params.expirySeconds + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { + id: '', + key: '', + description: '', + created: '', + expires: '', + revoked: '', + capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] }, + }, + error: data.message ?? 'Failed to create auth key', + } + } + + 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..3a1c1b9803 --- /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}`, + }), + }, + + transformResponse: async (response, _ctx, params) => { + 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 as TailscaleDeleteAuthKeyParams)?.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..583b901e4b --- /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}`, + }), + }, + + transformResponse: async (response, _ctx, params) => { + 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 as { deviceId?: string })?.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..d4e6d019cd --- /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}`, + 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..c8e045c181 --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { + id: '', + description: '', + created: '', + expires: '', + revoked: '', + capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] }, + }, + error: data.message ?? 'Failed to get auth key', + } + } + + 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..0b08b14e7c --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + 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.message ?? 'Failed to get device', + } + } + + 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..1a4303a83b --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { advertisedRoutes: [], enabledRoutes: [] }, + error: data.message ?? 'Failed to get device routes', + } + } + + 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..9afa63cd5a --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { magicDNS: false }, + error: data.message ?? 'Failed to get DNS preferences', + } + } + + 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..159e9c81a9 --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { searchPaths: [] }, + error: data.message ?? 'Failed to get DNS search paths', + } + } + + 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..bd1a6ba875 --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { keys: [], count: 0 }, + error: data.message ?? 'Failed to list auth keys', + } + } + + 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..0e0f7d11cf --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { devices: [], count: 0 }, + error: data.message ?? 'Failed to list devices', + } + } + + 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..c5f0f514ce --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { dns: [], magicDNS: false }, + error: data.message ?? 'Failed to list DNS nameservers', + } + } + + 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..0475ce313f --- /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}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { users: [], count: 0 }, + error: data.message ?? 'Failed to list users', + } + } + + 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..b32cc30a75 --- /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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + routes: params.routes + .split(',') + .map((r) => r.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { advertisedRoutes: [], enabledRoutes: [] }, + error: data.message ?? 'Failed to set device routes', + } + } + + 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..cf5dd58977 --- /dev/null +++ b/apps/sim/tools/tailscale/set_device_tags.ts @@ -0,0 +1,89 @@ +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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + tags: params.tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response, _ctx, params) => { + const typedParams = params as { deviceId?: string; tags?: string } + 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 = typedParams?.tags + ? typedParams.tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean) + : [] + + return { + success: true, + output: { + success: true, + deviceId: typedParams?.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..ac302e3214 --- /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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + dns: params.dns + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { dns: [], magicDNS: false }, + error: data.message ?? 'Failed to set DNS nameservers', + } + } + + 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..022bd46e28 --- /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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + magicDNS: params.magicDNS, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { magicDNS: false }, + error: data.message ?? 'Failed to set DNS preferences', + } + } + + 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..331d436342 --- /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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + searchPaths: params.searchPaths + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + output: { searchPaths: [] }, + error: data.message ?? 'Failed to set DNS search paths', + } + } + + 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..c09489da44 --- /dev/null +++ b/apps/sim/tools/tailscale/update_device_key.ts @@ -0,0 +1,79 @@ +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}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + keyExpiryDisabled: params.keyExpiryDisabled, + }), + }, + + transformResponse: async (response, _ctx, params) => { + const typedParams = params as { deviceId?: string; keyExpiryDisabled?: boolean } + 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: typedParams?.deviceId ?? '', + keyExpiryDisabled: typedParams?.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' }, + }, +} From f6c6c349b179cc7a17ceb76fd7cd470531893f59 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 31 Mar 2026 16:06:10 -0700 Subject: [PATCH 2/3] fix(tailscale): fix transformResponse signatures and block output types --- apps/sim/blocks/blocks/tailscale.ts | 77 +++++++++---------- apps/sim/tools/tailscale/authorize_device.ts | 7 +- apps/sim/tools/tailscale/delete_auth_key.ts | 4 +- apps/sim/tools/tailscale/delete_device.ts | 4 +- apps/sim/tools/tailscale/set_device_tags.ts | 9 +-- apps/sim/tools/tailscale/update_device_key.ts | 7 +- 6 files changed, 52 insertions(+), 56 deletions(-) diff --git a/apps/sim/blocks/blocks/tailscale.ts b/apps/sim/blocks/blocks/tailscale.ts index 81a5347b60..2a1f47dd0b 100644 --- a/apps/sim/blocks/blocks/tailscale.ts +++ b/apps/sim/blocks/blocks/tailscale.ts @@ -298,45 +298,44 @@ export const TailscaleBlock: BlockConfig = { }, outputs: { - response: { - type: { - devices: 'json', - count: 'number', - id: 'string', - name: 'string', - hostname: 'string', - user: 'string', - os: 'string', - clientVersion: 'string', - addresses: 'json', - tags: 'json', - authorized: 'boolean', - blocksIncomingConnections: 'boolean', - lastSeen: 'string', - created: 'string', - enabledRoutes: 'json', - advertisedRoutes: 'json', - isExternal: 'boolean', - updateAvailable: 'boolean', - machineKey: 'string', - nodeKey: 'string', - success: 'boolean', - deviceId: 'string', - keyExpiryDisabled: 'boolean', - dns: 'json', - magicDNS: 'boolean', - searchPaths: 'json', - users: 'json', - keys: 'json', - key: 'string', - keyId: 'string', - description: 'string', - expires: 'string', - revoked: 'string', - capabilities: 'json', - acl: 'string', - etag: 'string', - }, + 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/tools/tailscale/authorize_device.ts b/apps/sim/tools/tailscale/authorize_device.ts index 861336a064..88f86d33f0 100644 --- a/apps/sim/tools/tailscale/authorize_device.ts +++ b/apps/sim/tools/tailscale/authorize_device.ts @@ -50,8 +50,7 @@ export const tailscaleAuthorizeDeviceTool: ToolConfig< }), }, - transformResponse: async (response, _ctx, params) => { - const typedParams = params as { deviceId?: string; authorized?: boolean } + transformResponse: async (response: Response, params?: TailscaleAuthorizeDeviceParams) => { if (!response.ok) { const data = await response.json().catch(() => ({})) return { @@ -65,8 +64,8 @@ export const tailscaleAuthorizeDeviceTool: ToolConfig< success: true, output: { success: true, - deviceId: typedParams?.deviceId ?? '', - authorized: typedParams?.authorized ?? true, + deviceId: params?.deviceId ?? '', + authorized: params?.authorized ?? true, }, } }, diff --git a/apps/sim/tools/tailscale/delete_auth_key.ts b/apps/sim/tools/tailscale/delete_auth_key.ts index 3a1c1b9803..55ef0bf0f1 100644 --- a/apps/sim/tools/tailscale/delete_auth_key.ts +++ b/apps/sim/tools/tailscale/delete_auth_key.ts @@ -52,7 +52,7 @@ export const tailscaleDeleteAuthKeyTool: ToolConfig< }), }, - transformResponse: async (response, _ctx, params) => { + transformResponse: async (response: Response, params?: TailscaleDeleteAuthKeyParams) => { if (!response.ok) { const data = await response.json().catch(() => ({})) return { @@ -66,7 +66,7 @@ export const tailscaleDeleteAuthKeyTool: ToolConfig< success: true, output: { success: true, - keyId: (params as TailscaleDeleteAuthKeyParams)?.keyId ?? '', + keyId: params?.keyId ?? '', }, } }, diff --git a/apps/sim/tools/tailscale/delete_device.ts b/apps/sim/tools/tailscale/delete_device.ts index 583b901e4b..ef512e24a4 100644 --- a/apps/sim/tools/tailscale/delete_device.ts +++ b/apps/sim/tools/tailscale/delete_device.ts @@ -40,7 +40,7 @@ export const tailscaleDeleteDeviceTool: ToolConfig< }), }, - transformResponse: async (response, _ctx, params) => { + transformResponse: async (response: Response, params?: TailscaleDeviceParams) => { if (!response.ok) { const data = await response.json().catch(() => ({})) return { @@ -54,7 +54,7 @@ export const tailscaleDeleteDeviceTool: ToolConfig< success: true, output: { success: true, - deviceId: (params as { deviceId?: string })?.deviceId ?? '', + deviceId: params?.deviceId ?? '', }, } }, diff --git a/apps/sim/tools/tailscale/set_device_tags.ts b/apps/sim/tools/tailscale/set_device_tags.ts index cf5dd58977..558a686ef3 100644 --- a/apps/sim/tools/tailscale/set_device_tags.ts +++ b/apps/sim/tools/tailscale/set_device_tags.ts @@ -53,8 +53,7 @@ export const tailscaleSetDeviceTagsTool: ToolConfig< }), }, - transformResponse: async (response, _ctx, params) => { - const typedParams = params as { deviceId?: string; tags?: string } + transformResponse: async (response: Response, params?: TailscaleSetDeviceTagsParams) => { if (!response.ok) { const data = await response.json().catch(() => ({})) return { @@ -64,8 +63,8 @@ export const tailscaleSetDeviceTagsTool: ToolConfig< } } - const tags = typedParams?.tags - ? typedParams.tags + const tags = params?.tags + ? params.tags .split(',') .map((t) => t.trim()) .filter(Boolean) @@ -75,7 +74,7 @@ export const tailscaleSetDeviceTagsTool: ToolConfig< success: true, output: { success: true, - deviceId: typedParams?.deviceId ?? '', + deviceId: params?.deviceId ?? '', tags, }, } diff --git a/apps/sim/tools/tailscale/update_device_key.ts b/apps/sim/tools/tailscale/update_device_key.ts index c09489da44..fdb9aacea8 100644 --- a/apps/sim/tools/tailscale/update_device_key.ts +++ b/apps/sim/tools/tailscale/update_device_key.ts @@ -50,8 +50,7 @@ export const tailscaleUpdateDeviceKeyTool: ToolConfig< }), }, - transformResponse: async (response, _ctx, params) => { - const typedParams = params as { deviceId?: string; keyExpiryDisabled?: boolean } + transformResponse: async (response: Response, params?: TailscaleUpdateDeviceKeyParams) => { if (!response.ok) { const data = await response.json().catch(() => ({})) return { @@ -65,8 +64,8 @@ export const tailscaleUpdateDeviceKeyTool: ToolConfig< success: true, output: { success: true, - deviceId: typedParams?.deviceId ?? '', - keyExpiryDisabled: typedParams?.keyExpiryDisabled ?? true, + deviceId: params?.deviceId ?? '', + keyExpiryDisabled: params?.keyExpiryDisabled ?? true, }, } }, From 8c0616abfa51b4d5252c59b32c2606fd08f6c8c0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 31 Mar 2026 16:16:31 -0700 Subject: [PATCH 3/3] fix(tailscale): safe response.json() pattern, trim apiKey, guard expirySeconds --- apps/sim/tools/tailscale/authorize_device.ts | 2 +- apps/sim/tools/tailscale/create_auth_key.ts | 11 ++++++----- apps/sim/tools/tailscale/delete_auth_key.ts | 2 +- apps/sim/tools/tailscale/delete_device.ts | 2 +- apps/sim/tools/tailscale/get_acl.ts | 2 +- apps/sim/tools/tailscale/get_auth_key.ts | 8 ++++---- apps/sim/tools/tailscale/get_device.ts | 8 ++++---- apps/sim/tools/tailscale/get_device_routes.ts | 8 ++++---- apps/sim/tools/tailscale/get_dns_preferences.ts | 8 ++++---- apps/sim/tools/tailscale/get_dns_searchpaths.ts | 8 ++++---- apps/sim/tools/tailscale/list_auth_keys.ts | 8 ++++---- apps/sim/tools/tailscale/list_devices.ts | 8 ++++---- apps/sim/tools/tailscale/list_dns_nameservers.ts | 8 ++++---- apps/sim/tools/tailscale/list_users.ts | 8 ++++---- apps/sim/tools/tailscale/set_device_routes.ts | 8 ++++---- apps/sim/tools/tailscale/set_device_tags.ts | 2 +- apps/sim/tools/tailscale/set_dns_nameservers.ts | 8 ++++---- apps/sim/tools/tailscale/set_dns_preferences.ts | 8 ++++---- apps/sim/tools/tailscale/set_dns_searchpaths.ts | 8 ++++---- apps/sim/tools/tailscale/update_device_key.ts | 2 +- 20 files changed, 64 insertions(+), 63 deletions(-) diff --git a/apps/sim/tools/tailscale/authorize_device.ts b/apps/sim/tools/tailscale/authorize_device.ts index 88f86d33f0..f809fb95eb 100644 --- a/apps/sim/tools/tailscale/authorize_device.ts +++ b/apps/sim/tools/tailscale/authorize_device.ts @@ -42,7 +42,7 @@ export const tailscaleAuthorizeDeviceTool: ToolConfig< `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/authorized`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({ diff --git a/apps/sim/tools/tailscale/create_auth_key.ts b/apps/sim/tools/tailscale/create_auth_key.ts index 9bae8d2bd0..52ab595440 100644 --- a/apps/sim/tools/tailscale/create_auth_key.ts +++ b/apps/sim/tools/tailscale/create_auth_key.ts @@ -70,7 +70,7 @@ export const tailscaleCreateAuthKeyTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => { @@ -100,16 +100,16 @@ export const tailscaleCreateAuthKeyTool: ToolConfig< } if (params.description) body.description = params.description - if (params.expirySeconds) body.expirySeconds = params.expirySeconds + if (params.expirySeconds !== undefined && params.expirySeconds !== null) + body.expirySeconds = params.expirySeconds return body }, }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { @@ -121,10 +121,11 @@ export const tailscaleCreateAuthKeyTool: ToolConfig< revoked: '', capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] }, }, - error: data.message ?? 'Failed to create auth key', + error: (data as Record).message ?? 'Failed to create auth key', } } + const data = await response.json() const deviceCaps = data.capabilities?.devices?.create ?? {} return { diff --git a/apps/sim/tools/tailscale/delete_auth_key.ts b/apps/sim/tools/tailscale/delete_auth_key.ts index 55ef0bf0f1..d4f00c7539 100644 --- a/apps/sim/tools/tailscale/delete_auth_key.ts +++ b/apps/sim/tools/tailscale/delete_auth_key.ts @@ -48,7 +48,7 @@ export const tailscaleDeleteAuthKeyTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`, method: 'DELETE', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, diff --git a/apps/sim/tools/tailscale/delete_device.ts b/apps/sim/tools/tailscale/delete_device.ts index ef512e24a4..16671acec1 100644 --- a/apps/sim/tools/tailscale/delete_device.ts +++ b/apps/sim/tools/tailscale/delete_device.ts @@ -36,7 +36,7 @@ export const tailscaleDeleteDeviceTool: ToolConfig< `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`, method: 'DELETE', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, diff --git a/apps/sim/tools/tailscale/get_acl.ts b/apps/sim/tools/tailscale/get_acl.ts index d4e6d019cd..6d51ccbec6 100644 --- a/apps/sim/tools/tailscale/get_acl.ts +++ b/apps/sim/tools/tailscale/get_acl.ts @@ -34,7 +34,7 @@ export const tailscaleGetAclTool: ToolConfig ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, Accept: 'application/json', }), }, diff --git a/apps/sim/tools/tailscale/get_auth_key.ts b/apps/sim/tools/tailscale/get_auth_key.ts index c8e045c181..5b4d200c03 100644 --- a/apps/sim/tools/tailscale/get_auth_key.ts +++ b/apps/sim/tools/tailscale/get_auth_key.ts @@ -57,14 +57,13 @@ export const tailscaleGetAuthKeyTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { @@ -75,10 +74,11 @@ export const tailscaleGetAuthKeyTool: ToolConfig< revoked: '', capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] }, }, - error: data.message ?? 'Failed to get auth key', + error: (data as Record).message ?? 'Failed to get auth key', } } + const data = await response.json() const deviceCaps = data.capabilities?.devices?.create ?? {} return { diff --git a/apps/sim/tools/tailscale/get_device.ts b/apps/sim/tools/tailscale/get_device.ts index 0b08b14e7c..fe3ba670a7 100644 --- a/apps/sim/tools/tailscale/get_device.ts +++ b/apps/sim/tools/tailscale/get_device.ts @@ -34,14 +34,13 @@ export const tailscaleGetDeviceTool: ToolConfig ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { @@ -62,10 +61,11 @@ export const tailscaleGetDeviceTool: ToolConfig).message ?? 'Failed to get device', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/get_device_routes.ts b/apps/sim/tools/tailscale/get_device_routes.ts index 1a4303a83b..2d5b540750 100644 --- a/apps/sim/tools/tailscale/get_device_routes.ts +++ b/apps/sim/tools/tailscale/get_device_routes.ts @@ -36,21 +36,21 @@ export const tailscaleGetDeviceRoutesTool: ToolConfig< `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { advertisedRoutes: [], enabledRoutes: [] }, - error: data.message ?? 'Failed to get device routes', + error: (data as Record).message ?? 'Failed to get device routes', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/get_dns_preferences.ts b/apps/sim/tools/tailscale/get_dns_preferences.ts index 9afa63cd5a..248f962a19 100644 --- a/apps/sim/tools/tailscale/get_dns_preferences.ts +++ b/apps/sim/tools/tailscale/get_dns_preferences.ts @@ -36,21 +36,21 @@ export const tailscaleGetDnsPreferencesTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { magicDNS: false }, - error: data.message ?? 'Failed to get DNS preferences', + error: (data as Record).message ?? 'Failed to get DNS preferences', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/get_dns_searchpaths.ts b/apps/sim/tools/tailscale/get_dns_searchpaths.ts index 159e9c81a9..a5f8e54b02 100644 --- a/apps/sim/tools/tailscale/get_dns_searchpaths.ts +++ b/apps/sim/tools/tailscale/get_dns_searchpaths.ts @@ -36,21 +36,21 @@ export const tailscaleGetDnsSearchpathsTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { searchPaths: [] }, - error: data.message ?? 'Failed to get DNS search paths', + error: (data as Record).message ?? 'Failed to get DNS search paths', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/list_auth_keys.ts b/apps/sim/tools/tailscale/list_auth_keys.ts index bd1a6ba875..2e94308e89 100644 --- a/apps/sim/tools/tailscale/list_auth_keys.ts +++ b/apps/sim/tools/tailscale/list_auth_keys.ts @@ -51,21 +51,21 @@ export const tailscaleListAuthKeysTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { keys: [], count: 0 }, - error: data.message ?? 'Failed to list auth keys', + 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 diff --git a/apps/sim/tools/tailscale/list_devices.ts b/apps/sim/tools/tailscale/list_devices.ts index 0e0f7d11cf..b55835d482 100644 --- a/apps/sim/tools/tailscale/list_devices.ts +++ b/apps/sim/tools/tailscale/list_devices.ts @@ -30,21 +30,21 @@ export const tailscaleListDevicesTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/devices`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { devices: [], count: 0 }, - error: data.message ?? 'Failed to list devices', + 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, diff --git a/apps/sim/tools/tailscale/list_dns_nameservers.ts b/apps/sim/tools/tailscale/list_dns_nameservers.ts index c5f0f514ce..67b0ac6745 100644 --- a/apps/sim/tools/tailscale/list_dns_nameservers.ts +++ b/apps/sim/tools/tailscale/list_dns_nameservers.ts @@ -30,21 +30,21 @@ export const tailscaleListDnsNameserversTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`, method: 'GET', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { dns: [], magicDNS: false }, - error: data.message ?? 'Failed to list DNS nameservers', + error: (data as Record).message ?? 'Failed to list DNS nameservers', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/list_users.ts b/apps/sim/tools/tailscale/list_users.ts index 0475ce313f..100719d637 100644 --- a/apps/sim/tools/tailscale/list_users.ts +++ b/apps/sim/tools/tailscale/list_users.ts @@ -27,21 +27,21 @@ export const tailscaleListUsersTool: ToolConfig ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, }), }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { users: [], count: 0 }, - error: data.message ?? 'Failed to list users', + 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, diff --git a/apps/sim/tools/tailscale/set_device_routes.ts b/apps/sim/tools/tailscale/set_device_routes.ts index b32cc30a75..49b3ba3ca3 100644 --- a/apps/sim/tools/tailscale/set_device_routes.ts +++ b/apps/sim/tools/tailscale/set_device_routes.ts @@ -43,7 +43,7 @@ export const tailscaleSetDeviceRoutesTool: ToolConfig< `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({ @@ -55,16 +55,16 @@ export const tailscaleSetDeviceRoutesTool: ToolConfig< }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { advertisedRoutes: [], enabledRoutes: [] }, - error: data.message ?? 'Failed to set device routes', + error: (data as Record).message ?? 'Failed to set device routes', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/set_device_tags.ts b/apps/sim/tools/tailscale/set_device_tags.ts index 558a686ef3..b760a7b79c 100644 --- a/apps/sim/tools/tailscale/set_device_tags.ts +++ b/apps/sim/tools/tailscale/set_device_tags.ts @@ -42,7 +42,7 @@ export const tailscaleSetDeviceTagsTool: ToolConfig< `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/tags`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({ diff --git a/apps/sim/tools/tailscale/set_dns_nameservers.ts b/apps/sim/tools/tailscale/set_dns_nameservers.ts index ac302e3214..52ecbe7ece 100644 --- a/apps/sim/tools/tailscale/set_dns_nameservers.ts +++ b/apps/sim/tools/tailscale/set_dns_nameservers.ts @@ -48,7 +48,7 @@ export const tailscaleSetDnsNameserversTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({ @@ -60,16 +60,16 @@ export const tailscaleSetDnsNameserversTool: ToolConfig< }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { dns: [], magicDNS: false }, - error: data.message ?? 'Failed to set DNS nameservers', + error: (data as Record).message ?? 'Failed to set DNS nameservers', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/set_dns_preferences.ts b/apps/sim/tools/tailscale/set_dns_preferences.ts index 022bd46e28..d39bafbb82 100644 --- a/apps/sim/tools/tailscale/set_dns_preferences.ts +++ b/apps/sim/tools/tailscale/set_dns_preferences.ts @@ -47,7 +47,7 @@ export const tailscaleSetDnsPreferencesTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({ @@ -56,16 +56,16 @@ export const tailscaleSetDnsPreferencesTool: ToolConfig< }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { magicDNS: false }, - error: data.message ?? 'Failed to set DNS preferences', + error: (data as Record).message ?? 'Failed to set DNS preferences', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/set_dns_searchpaths.ts b/apps/sim/tools/tailscale/set_dns_searchpaths.ts index 331d436342..31d52f0328 100644 --- a/apps/sim/tools/tailscale/set_dns_searchpaths.ts +++ b/apps/sim/tools/tailscale/set_dns_searchpaths.ts @@ -48,7 +48,7 @@ export const tailscaleSetDnsSearchpathsTool: ToolConfig< `https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({ @@ -60,16 +60,16 @@ export const tailscaleSetDnsSearchpathsTool: ToolConfig< }, transformResponse: async (response) => { - const data = await response.json() - if (!response.ok) { + const data = await response.json().catch(() => ({})) return { success: false, output: { searchPaths: [] }, - error: data.message ?? 'Failed to set DNS search paths', + error: (data as Record).message ?? 'Failed to set DNS search paths', } } + const data = await response.json() return { success: true, output: { diff --git a/apps/sim/tools/tailscale/update_device_key.ts b/apps/sim/tools/tailscale/update_device_key.ts index fdb9aacea8..a26ff3b3ad 100644 --- a/apps/sim/tools/tailscale/update_device_key.ts +++ b/apps/sim/tools/tailscale/update_device_key.ts @@ -42,7 +42,7 @@ export const tailscaleUpdateDeviceKeyTool: ToolConfig< `https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/key`, method: 'POST', headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }), body: (params) => ({