chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleAuthorizeDeviceParams, TailscaleAuthorizeDeviceResponse } from './types'
|
||||
|
||||
export const tailscaleAuthorizeDeviceTool: ToolConfig<
|
||||
TailscaleAuthorizeDeviceParams,
|
||||
TailscaleAuthorizeDeviceResponse
|
||||
> = {
|
||||
id: 'tailscale_authorize_device',
|
||||
name: 'Tailscale Authorize Device',
|
||||
description: 'Authorize or deauthorize a device on the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to authorize',
|
||||
},
|
||||
authorized: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to authorize (true) or deauthorize (false) the device',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/authorized`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
authorized: params.authorized,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleAuthorizeDeviceParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, deviceId: '', authorized: false },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to authorize device',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deviceId: params?.deviceId ?? '',
|
||||
authorized: params?.authorized ?? true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded' },
|
||||
deviceId: { type: 'string', description: 'Device ID' },
|
||||
authorized: { type: 'boolean', description: 'Authorization status after the operation' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleCreateAuthKeyParams, TailscaleCreateAuthKeyResponse } from './types'
|
||||
|
||||
export const tailscaleCreateAuthKeyTool: ToolConfig<
|
||||
TailscaleCreateAuthKeyParams,
|
||||
TailscaleCreateAuthKeyResponse
|
||||
> = {
|
||||
id: 'tailscale_create_auth_key',
|
||||
name: 'Tailscale Create Auth Key',
|
||||
description: 'Create a new auth key for the tailnet to pre-authorize devices',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
reusable: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the key can be used more than once',
|
||||
default: false,
|
||||
},
|
||||
ephemeral: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether devices authenticated with this key are ephemeral',
|
||||
default: false,
|
||||
},
|
||||
preauthorized: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether devices are pre-authorized (skip manual approval)',
|
||||
default: true,
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated list of tags for devices using this key (e.g., "tag:server,tag:prod")',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Description for the auth key',
|
||||
},
|
||||
expirySeconds: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Key expiry time in seconds (default: 90 days)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const tags = params.tags
|
||||
? params.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
const createCaps: Record<string, unknown> = {
|
||||
reusable: params.reusable ?? false,
|
||||
ephemeral: params.ephemeral ?? false,
|
||||
preauthorized: params.preauthorized ?? true,
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
createCaps.tags = tags
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
capabilities: {
|
||||
devices: {
|
||||
create: createCaps,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (params.description) body.description = params.description
|
||||
if (params.expirySeconds !== undefined && params.expirySeconds !== null)
|
||||
body.expirySeconds = params.expirySeconds
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
key: '',
|
||||
description: '',
|
||||
created: '',
|
||||
expires: '',
|
||||
revoked: '',
|
||||
capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] },
|
||||
},
|
||||
error: (data as Record<string, string>).message ?? 'Failed to create auth key',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const deviceCaps = data.capabilities?.devices?.create ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
key: data.key ?? null,
|
||||
description: data.description ?? null,
|
||||
created: data.created ?? null,
|
||||
expires: data.expires ?? null,
|
||||
revoked: data.revoked ?? null,
|
||||
capabilities: {
|
||||
reusable: deviceCaps.reusable ?? false,
|
||||
ephemeral: deviceCaps.ephemeral ?? false,
|
||||
preauthorized: deviceCaps.preauthorized ?? false,
|
||||
tags: deviceCaps.tags ?? [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Auth key ID' },
|
||||
key: { type: 'string', description: 'The auth key value (only shown once at creation)' },
|
||||
description: { type: 'string', description: 'Key description', optional: true },
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
expires: { type: 'string', description: 'Expiration timestamp' },
|
||||
revoked: {
|
||||
type: 'string',
|
||||
description: 'Revocation timestamp (empty if not revoked)',
|
||||
optional: true,
|
||||
},
|
||||
capabilities: {
|
||||
type: 'object',
|
||||
description: 'Key capabilities',
|
||||
properties: {
|
||||
reusable: { type: 'boolean', description: 'Whether the key is reusable' },
|
||||
ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' },
|
||||
preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' },
|
||||
tags: { type: 'array', description: 'Tags applied to devices using this key' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface TailscaleDeleteAuthKeyParams {
|
||||
apiKey: string
|
||||
tailnet: string
|
||||
keyId: string
|
||||
}
|
||||
|
||||
interface TailscaleDeleteAuthKeyResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
keyId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleDeleteAuthKeyTool: ToolConfig<
|
||||
TailscaleDeleteAuthKeyParams,
|
||||
TailscaleDeleteAuthKeyResponse
|
||||
> = {
|
||||
id: 'tailscale_delete_auth_key',
|
||||
name: 'Tailscale Delete Auth Key',
|
||||
description: 'Revoke and delete an auth key',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
keyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Auth key ID to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleDeleteAuthKeyParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, keyId: '' },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to delete auth key',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
keyId: params?.keyId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the auth key was successfully deleted' },
|
||||
keyId: { type: 'string', description: 'ID of the deleted auth key' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleDeleteDeviceResponse, TailscaleDeviceParams } from './types'
|
||||
|
||||
export const tailscaleDeleteDeviceTool: ToolConfig<
|
||||
TailscaleDeviceParams,
|
||||
TailscaleDeleteDeviceResponse
|
||||
> = {
|
||||
id: 'tailscale_delete_device',
|
||||
name: 'Tailscale Delete Device',
|
||||
description: 'Remove a device from the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleDeviceParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, deviceId: '' },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to delete device',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deviceId: params?.deviceId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the device was successfully deleted' },
|
||||
deviceId: { type: 'string', description: 'ID of the deleted device' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { TailscaleBaseParams } from './types'
|
||||
|
||||
interface TailscaleDeleteUserParams extends TailscaleBaseParams {
|
||||
userId: string
|
||||
}
|
||||
|
||||
interface TailscaleDeleteUserResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleDeleteUserTool: ToolConfig<
|
||||
TailscaleDeleteUserParams,
|
||||
TailscaleDeleteUserResponse
|
||||
> = {
|
||||
id: 'tailscale_delete_user',
|
||||
name: 'Tailscale Delete User',
|
||||
description: 'Delete a user 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',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'User ID to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/users/${encodeURIComponent(params.userId.trim())}/delete`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleDeleteUserParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, userId: '' },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to delete user',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
userId: params?.userId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the user was successfully deleted' },
|
||||
userId: { type: 'string', description: 'ID of the deleted user' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { TailscaleDeviceParams } from './types'
|
||||
|
||||
interface TailscaleExpireDeviceKeyResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
deviceId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleExpireDeviceKeyTool: ToolConfig<
|
||||
TailscaleDeviceParams,
|
||||
TailscaleExpireDeviceKeyResponse
|
||||
> = {
|
||||
id: 'tailscale_expire_device_key',
|
||||
name: 'Tailscale Expire Device Key',
|
||||
description:
|
||||
"Immediately expire a device's node key, requiring it to re-authenticate before it can reconnect to 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 expire the key for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/expire`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleDeviceParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, deviceId: '' },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to expire device key',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deviceId: params?.deviceId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: "Whether the device's key was successfully expired" },
|
||||
deviceId: { type: 'string', description: 'Device ID' },
|
||||
},
|
||||
}
|
||||
@@ -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<TailscaleBaseParams, TailscaleGetAclResponse> = {
|
||||
id: 'tailscale_get_acl',
|
||||
name: 'Tailscale Get ACL',
|
||||
description: 'Get the current ACL policy for the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/acl`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { acl: '', etag: '' },
|
||||
error: (data as Record<string, string>).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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface TailscaleGetAuthKeyParams {
|
||||
apiKey: string
|
||||
tailnet: string
|
||||
keyId: string
|
||||
}
|
||||
|
||||
interface TailscaleGetAuthKeyResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
description: string
|
||||
created: string
|
||||
expires: string
|
||||
revoked: string
|
||||
capabilities: {
|
||||
reusable: boolean
|
||||
ephemeral: boolean
|
||||
preauthorized: boolean
|
||||
tags: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleGetAuthKeyTool: ToolConfig<
|
||||
TailscaleGetAuthKeyParams,
|
||||
TailscaleGetAuthKeyResponse
|
||||
> = {
|
||||
id: 'tailscale_get_auth_key',
|
||||
name: 'Tailscale Get Auth Key',
|
||||
description: 'Get details of a specific auth key',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
keyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Auth key ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
description: '',
|
||||
created: '',
|
||||
expires: '',
|
||||
revoked: '',
|
||||
capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] },
|
||||
},
|
||||
error: (data as Record<string, string>).message ?? 'Failed to get auth key',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const deviceCaps = data.capabilities?.devices?.create ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
description: data.description ?? null,
|
||||
created: data.created ?? null,
|
||||
expires: data.expires ?? null,
|
||||
revoked: data.revoked ?? null,
|
||||
capabilities: {
|
||||
reusable: deviceCaps.reusable ?? false,
|
||||
ephemeral: deviceCaps.ephemeral ?? false,
|
||||
preauthorized: deviceCaps.preauthorized ?? false,
|
||||
tags: deviceCaps.tags ?? [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Auth key ID' },
|
||||
description: { type: 'string', description: 'Key description', optional: true },
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
expires: { type: 'string', description: 'Expiration timestamp' },
|
||||
revoked: { type: 'string', description: 'Revocation timestamp', optional: true },
|
||||
capabilities: {
|
||||
type: 'object',
|
||||
description: 'Key capabilities',
|
||||
properties: {
|
||||
reusable: { type: 'boolean', description: 'Whether the key is reusable' },
|
||||
ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' },
|
||||
preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' },
|
||||
tags: { type: 'array', description: 'Tags applied to devices using this key' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleDeviceParams, TailscaleGetDeviceResponse } from './types'
|
||||
|
||||
export const tailscaleGetDeviceTool: ToolConfig<TailscaleDeviceParams, TailscaleGetDeviceResponse> =
|
||||
{
|
||||
id: 'tailscale_get_device',
|
||||
name: 'Tailscale Get Device',
|
||||
description: 'Get details of a specific device by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
nodeId: '',
|
||||
name: '',
|
||||
hostname: '',
|
||||
user: '',
|
||||
os: '',
|
||||
clientVersion: '',
|
||||
addresses: [],
|
||||
tags: [],
|
||||
authorized: false,
|
||||
blocksIncomingConnections: false,
|
||||
keyExpiryDisabled: false,
|
||||
expires: '',
|
||||
lastSeen: '',
|
||||
created: '',
|
||||
isExternal: false,
|
||||
updateAvailable: false,
|
||||
machineKey: '',
|
||||
nodeKey: '',
|
||||
},
|
||||
error: (data as Record<string, string>).message ?? 'Failed to get device',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
nodeId: data.nodeId ?? 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,
|
||||
keyExpiryDisabled: data.keyExpiryDisabled ?? false,
|
||||
expires: data.expires ?? null,
|
||||
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: 'Legacy device ID' },
|
||||
nodeId: { type: 'string', description: 'Preferred device ID', optional: true },
|
||||
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',
|
||||
},
|
||||
keyExpiryDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the device key is exempt from expiring',
|
||||
optional: true,
|
||||
},
|
||||
expires: {
|
||||
type: 'string',
|
||||
description: "The device's auth key expiration timestamp",
|
||||
optional: true,
|
||||
},
|
||||
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 },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleDeviceParams, TailscaleGetDeviceRoutesResponse } from './types'
|
||||
|
||||
export const tailscaleGetDeviceRoutesTool: ToolConfig<
|
||||
TailscaleDeviceParams,
|
||||
TailscaleGetDeviceRoutesResponse
|
||||
> = {
|
||||
id: 'tailscale_get_device_routes',
|
||||
name: 'Tailscale Get Device Routes',
|
||||
description: 'Get the subnet routes for a device',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { advertisedRoutes: [], enabledRoutes: [] },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to get device routes',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
advertisedRoutes: data.advertisedRoutes ?? [],
|
||||
enabledRoutes: data.enabledRoutes ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
advertisedRoutes: { type: 'array', description: 'Subnet routes the device is advertising' },
|
||||
enabledRoutes: { type: 'array', description: 'Subnet routes that are approved/enabled' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { TailscaleBaseParams } from './types'
|
||||
|
||||
interface TailscaleGetDnsPreferencesResponse extends ToolResponse {
|
||||
output: {
|
||||
magicDNS: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleGetDnsPreferencesTool: ToolConfig<
|
||||
TailscaleBaseParams,
|
||||
TailscaleGetDnsPreferencesResponse
|
||||
> = {
|
||||
id: 'tailscale_get_dns_preferences',
|
||||
name: 'Tailscale Get DNS Preferences',
|
||||
description: 'Get the DNS preferences for the tailnet including MagicDNS status',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { magicDNS: false },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to get DNS preferences',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
magicDNS: data.magicDNS ?? false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { TailscaleBaseParams } from './types'
|
||||
|
||||
interface TailscaleGetDnsSearchpathsResponse extends ToolResponse {
|
||||
output: {
|
||||
searchPaths: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleGetDnsSearchpathsTool: ToolConfig<
|
||||
TailscaleBaseParams,
|
||||
TailscaleGetDnsSearchpathsResponse
|
||||
> = {
|
||||
id: 'tailscale_get_dns_searchpaths',
|
||||
name: 'Tailscale Get DNS Search Paths',
|
||||
description: 'Get the DNS search paths configured for the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { searchPaths: [] },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to get DNS search paths',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
searchPaths: data.searchPaths ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
searchPaths: { type: 'array', description: 'List of DNS search path domains' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export { tailscaleAuthorizeDeviceTool } from './authorize_device'
|
||||
export { tailscaleCreateAuthKeyTool } from './create_auth_key'
|
||||
export { tailscaleDeleteAuthKeyTool } from './delete_auth_key'
|
||||
export { tailscaleDeleteDeviceTool } from './delete_device'
|
||||
export { tailscaleDeleteUserTool } from './delete_user'
|
||||
export { tailscaleExpireDeviceKeyTool } from './expire_device_key'
|
||||
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 { tailscaleSetAclTool } from './set_acl'
|
||||
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 { tailscaleSuspendUserTool } from './suspend_user'
|
||||
export * from './types'
|
||||
export { tailscaleUpdateDeviceKeyTool } from './update_device_key'
|
||||
@@ -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?all=true`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { keys: [], count: 0 },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to list auth keys',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const keys = (data.keys ?? []).map((key: Record<string, unknown>) => {
|
||||
const caps = (key.capabilities as Record<string, unknown>)?.devices as Record<string, unknown>
|
||||
const create = caps?.create as Record<string, unknown>
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleBaseParams, TailscaleListDevicesResponse } from './types'
|
||||
|
||||
export const tailscaleListDevicesTool: ToolConfig<
|
||||
TailscaleBaseParams,
|
||||
TailscaleListDevicesResponse
|
||||
> = {
|
||||
id: 'tailscale_list_devices',
|
||||
name: 'Tailscale List Devices',
|
||||
description: 'List all devices in the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/devices`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { devices: [], count: 0 },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to list devices',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const devices = (data.devices ?? []).map((device: Record<string, unknown>) => ({
|
||||
id: (device.id as string) ?? null,
|
||||
nodeId: (device.nodeId 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,
|
||||
keyExpiryDisabled: (device.keyExpiryDisabled as boolean) ?? false,
|
||||
expires: (device.expires as string) ?? null,
|
||||
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: 'Legacy device ID' },
|
||||
nodeId: { type: 'string', description: 'Preferred 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',
|
||||
},
|
||||
keyExpiryDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the device key is exempt from expiring',
|
||||
},
|
||||
expires: { type: 'string', description: "The device's auth key expiration timestamp" },
|
||||
lastSeen: { type: 'string', description: 'Last seen timestamp' },
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Total number of devices',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleBaseParams, TailscaleListDnsNameserversResponse } from './types'
|
||||
|
||||
export const tailscaleListDnsNameserversTool: ToolConfig<
|
||||
TailscaleBaseParams,
|
||||
TailscaleListDnsNameserversResponse
|
||||
> = {
|
||||
id: 'tailscale_list_dns_nameservers',
|
||||
name: 'Tailscale List DNS Nameservers',
|
||||
description: 'Get the DNS nameservers configured for the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { dns: [] },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to list DNS nameservers',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dns: data.dns ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dns: { type: 'array', description: 'List of DNS nameserver addresses' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleBaseParams, TailscaleListUsersResponse } from './types'
|
||||
|
||||
export const tailscaleListUsersTool: ToolConfig<TailscaleBaseParams, TailscaleListUsersResponse> = {
|
||||
id: 'tailscale_list_users',
|
||||
name: 'Tailscale List Users',
|
||||
description: 'List all users in the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/users`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { users: [], count: 0 },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to list users',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const users = (data.users ?? []).map((user: Record<string, unknown>) => ({
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface TailscaleSetAclParams {
|
||||
apiKey: string
|
||||
tailnet: string
|
||||
acl: string
|
||||
ifMatch?: string
|
||||
}
|
||||
|
||||
interface TailscaleSetAclResponse extends ToolResponse {
|
||||
output: {
|
||||
acl: string
|
||||
etag: string
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleSetAclTool: ToolConfig<TailscaleSetAclParams, TailscaleSetAclResponse> = {
|
||||
id: 'tailscale_set_acl',
|
||||
name: 'Tailscale Set ACL',
|
||||
description: 'Replace the ACL policy file 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',
|
||||
},
|
||||
acl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The new ACL policy file, as a JSON string',
|
||||
},
|
||||
ifMatch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'ETag from a prior Get ACL call to avoid overwriting concurrent updates. Use "ts-default" to only replace an untouched default policy file.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/acl`,
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
if (params.ifMatch) headers['If-Match'] = `"${params.ifMatch.trim().replace(/^"|"$/g, '')}"`
|
||||
return headers
|
||||
},
|
||||
body: (params) => params.acl.trim(),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { acl: '', etag: '' },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to set 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: 'Updated ACL policy as JSON string' },
|
||||
etag: {
|
||||
type: 'string',
|
||||
description: 'ETag for the new ACL version (use with If-Match header for future updates)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleSetDeviceRoutesParams, TailscaleSetDeviceRoutesResponse } from './types'
|
||||
|
||||
export const tailscaleSetDeviceRoutesTool: ToolConfig<
|
||||
TailscaleSetDeviceRoutesParams,
|
||||
TailscaleSetDeviceRoutesResponse
|
||||
> = {
|
||||
id: 'tailscale_set_device_routes',
|
||||
name: 'Tailscale Set Device Routes',
|
||||
description: 'Set the enabled subnet routes for a device',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID',
|
||||
},
|
||||
routes: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated list of subnet routes to enable (e.g., "10.0.0.0/24,192.168.1.0/24")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
routes: params.routes
|
||||
.split(',')
|
||||
.map((r) => r.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { advertisedRoutes: [], enabledRoutes: [] },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to set device routes',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
advertisedRoutes: data.advertisedRoutes ?? [],
|
||||
enabledRoutes: data.enabledRoutes ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
advertisedRoutes: { type: 'array', description: 'Subnet routes the device is advertising' },
|
||||
enabledRoutes: { type: 'array', description: 'Subnet routes that are now enabled' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleSetDeviceTagsParams, TailscaleSetDeviceTagsResponse } from './types'
|
||||
|
||||
export const tailscaleSetDeviceTagsTool: ToolConfig<
|
||||
TailscaleSetDeviceTagsParams,
|
||||
TailscaleSetDeviceTagsResponse
|
||||
> = {
|
||||
id: 'tailscale_set_device_tags',
|
||||
name: 'Tailscale Set Device Tags',
|
||||
description: 'Set tags on a device in the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of tags (e.g., "tag:server,tag:production")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/tags`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
tags: params.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleSetDeviceTagsParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, deviceId: '', tags: [] },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to set device tags',
|
||||
}
|
||||
}
|
||||
|
||||
const tags = params?.tags
|
||||
? params.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deviceId: params?.deviceId ?? '',
|
||||
tags,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the tags were successfully set' },
|
||||
deviceId: { type: 'string', description: 'Device ID' },
|
||||
tags: { type: 'array', description: 'Tags set on the device' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface TailscaleSetDnsNameserversParams {
|
||||
apiKey: string
|
||||
tailnet: string
|
||||
dns: string
|
||||
}
|
||||
|
||||
interface TailscaleSetDnsNameserversResponse extends ToolResponse {
|
||||
output: {
|
||||
dns: string[]
|
||||
magicDNS: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleSetDnsNameserversTool: ToolConfig<
|
||||
TailscaleSetDnsNameserversParams,
|
||||
TailscaleSetDnsNameserversResponse
|
||||
> = {
|
||||
id: 'tailscale_set_dns_nameservers',
|
||||
name: 'Tailscale Set DNS Nameservers',
|
||||
description: 'Set the DNS nameservers for the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
dns: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of DNS nameserver IP addresses (e.g., "8.8.8.8,8.8.4.4")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
dns: params.dns
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { dns: [], magicDNS: false },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to set DNS nameservers',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
dns: data.dns ?? [],
|
||||
magicDNS: data.magicDNS ?? false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dns: { type: 'array', description: 'Updated list of DNS nameserver addresses' },
|
||||
magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface TailscaleSetDnsPreferencesParams {
|
||||
apiKey: string
|
||||
tailnet: string
|
||||
magicDNS: boolean
|
||||
}
|
||||
|
||||
interface TailscaleSetDnsPreferencesResponse extends ToolResponse {
|
||||
output: {
|
||||
magicDNS: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleSetDnsPreferencesTool: ToolConfig<
|
||||
TailscaleSetDnsPreferencesParams,
|
||||
TailscaleSetDnsPreferencesResponse
|
||||
> = {
|
||||
id: 'tailscale_set_dns_preferences',
|
||||
name: 'Tailscale Set DNS Preferences',
|
||||
description: 'Set DNS preferences for the tailnet (enable/disable MagicDNS)',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
magicDNS: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to enable (true) or disable (false) MagicDNS',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
magicDNS: params.magicDNS,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { magicDNS: false },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to set DNS preferences',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
magicDNS: data.magicDNS ?? false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
magicDNS: { type: 'boolean', description: 'Updated MagicDNS status' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface TailscaleSetDnsSearchpathsParams {
|
||||
apiKey: string
|
||||
tailnet: string
|
||||
searchPaths: string
|
||||
}
|
||||
|
||||
interface TailscaleSetDnsSearchpathsResponse extends ToolResponse {
|
||||
output: {
|
||||
searchPaths: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleSetDnsSearchpathsTool: ToolConfig<
|
||||
TailscaleSetDnsSearchpathsParams,
|
||||
TailscaleSetDnsSearchpathsResponse
|
||||
> = {
|
||||
id: 'tailscale_set_dns_searchpaths',
|
||||
name: 'Tailscale Set DNS Search Paths',
|
||||
description: 'Set the DNS search paths for the tailnet',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
searchPaths: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated list of DNS search path domains (e.g., "corp.example.com,internal.example.com")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
searchPaths: params.searchPaths
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { searchPaths: [] },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to set DNS search paths',
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
searchPaths: data.searchPaths ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
searchPaths: { type: 'array', description: 'Updated list of DNS search path domains' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import type { TailscaleBaseParams } from './types'
|
||||
|
||||
interface TailscaleSuspendUserParams extends TailscaleBaseParams {
|
||||
userId: string
|
||||
}
|
||||
|
||||
interface TailscaleSuspendUserResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const tailscaleSuspendUserTool: ToolConfig<
|
||||
TailscaleSuspendUserParams,
|
||||
TailscaleSuspendUserResponse
|
||||
> = {
|
||||
id: 'tailscale_suspend_user',
|
||||
name: 'Tailscale Suspend User',
|
||||
description: "Suspend a user's access to 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',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'User ID to suspend',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/users/${encodeURIComponent(params.userId.trim())}/suspend`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleSuspendUserParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, userId: '' },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to suspend user',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
userId: params?.userId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the user was successfully suspended' },
|
||||
userId: { type: 'string', description: 'ID of the suspended user' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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
|
||||
}
|
||||
|
||||
interface TailscaleDeviceOutput {
|
||||
id: string
|
||||
nodeId: string
|
||||
name: string
|
||||
hostname: string
|
||||
user: string
|
||||
os: string
|
||||
clientVersion: string
|
||||
addresses: string[]
|
||||
tags: string[]
|
||||
authorized: boolean
|
||||
blocksIncomingConnections: boolean
|
||||
keyExpiryDisabled: boolean
|
||||
expires: string
|
||||
lastSeen: string
|
||||
created: string
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { TailscaleUpdateDeviceKeyParams, TailscaleUpdateDeviceKeyResponse } from './types'
|
||||
|
||||
export const tailscaleUpdateDeviceKeyTool: ToolConfig<
|
||||
TailscaleUpdateDeviceKeyParams,
|
||||
TailscaleUpdateDeviceKeyResponse
|
||||
> = {
|
||||
id: 'tailscale_update_device_key',
|
||||
name: 'Tailscale Update Device Key',
|
||||
description: 'Enable or disable key expiry on a device',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Tailscale API key',
|
||||
},
|
||||
tailnet: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tailnet name (e.g., example.com) or "-" for default',
|
||||
},
|
||||
deviceId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID',
|
||||
},
|
||||
keyExpiryDisabled: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to disable key expiry (true) or enable it (false)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/key`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey.trim()}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
keyExpiryDisabled: params.keyExpiryDisabled,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: TailscaleUpdateDeviceKeyParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, deviceId: '', keyExpiryDisabled: false },
|
||||
error: (data as Record<string, string>).message ?? 'Failed to update device key',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deviceId: params?.deviceId ?? '',
|
||||
keyExpiryDisabled: params?.keyExpiryDisabled ?? true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded' },
|
||||
deviceId: { type: 'string', description: 'Device ID' },
|
||||
keyExpiryDisabled: { type: 'boolean', description: 'Whether key expiry is now disabled' },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user