feat(apps): add Tailscale connector with OAuth client health check

Add the Tailscale catalog entry (rest_api, api_key) that stores an OAuth
client in the vault, a server-side API client for token exchange, device
listing, and auth key mint/delete, and a health check that proves the
client scopes and tag ownership with one ephemeral test key. Store a
redacted summary on the connection, map provider failures to actionable
codes without echoing provider bodies, refuse credential export, and add
brand assets, review registry entry, docs, and fixture tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
devinfoleyandClaude Fable 5.1 committed 2026-10-09 04:42:25 +00:00
1 parent 1c4ce44e13
commit 47bbe1aba4
18 files changed
+1414 -75

No files matched your search

+23
View File
@@ -922,6 +922,29 @@ const apps = [
}),
{ docsUrl: "https://docs.browser-use.com/cloud/api-v4-overview" },
],
["tailscale", "Tailscale", "Reach tailnet machines and private services from agent environments.", "developer", "tailscale.com", ["https://login.tailscale.com/*"],
method("oauth-client", "rest_api", "api_key", { serverUrl: "https://api.tailscale.com/api/v2" }, "S3",
"Create an OAuth client in [Tailscale trust credentials](https://login.tailscale.com/admin/settings/oauth) with the `auth_keys` scope (tags `tag:paperclip-agent` and `tag:paperclip-control`) and the `devices:core` scope, then paste its client ID and secret. Paperclip verifies the client server-side by listing devices and minting one short-lived ephemeral test key that it deletes immediately. The client never leaves the server.", {
label: "Tailscale OAuth client",
whenToUse: "Connect the tailnet whose machines and services agents should reach.",
grantKinds: ["organization"],
credentialFields: [
{ ...field("oauthClientId", "OAuth client ID", "k1234567CNTRL"), helperMd: "Copy the client ID shown after you create the OAuth client." },
{ ...field("oauthClientSecret", "OAuth client secret", "tskey-client-…"), helperMd: "The secret is shown once. It needs the `auth_keys` scope with the tags agents will carry and the `devices:core` scope. Paperclip cannot add scopes or tags to an existing client; create a new client and reconnect when access is insufficient." },
],
// The client is exchanged for a token in the form body of the token
// request; it is never a request header. A non-header placement keeps
// the two fields as vault secret refs only, without header projections.
keyPlacement: { location: "body_json", name: "client_secret" },
tenantFields: [
{ key: "tailnet", label: "Tailnet", type: "text", required: false, defaultValue: "-", placeholder: "-", helperMd: "Keep `-` for the tailnet that owns the OAuth client, or enter the tailnet name such as `example.com` or `tail1234.ts.net`.", validation: { pattern: "^(-|[A-Za-z0-9.@_-]+)$", maxLength: 253 } },
{ key: "agentTag", label: "Agent tag", type: "text", required: true, advanced: true, defaultValue: "tag:paperclip-agent", placeholder: "tag:paperclip-agent", helperMd: "Tag assigned to machines and sandboxes that join the tailnet for agents. The OAuth client must be allowed to assign it.", validation: { pattern: "^tag:[A-Za-z0-9_-]+$", maxLength: 128 } },
],
consoleLinks: { keys: "https://login.tailscale.com/admin/settings/oauth", docs: "https://tailscale.com/kb/1215/oauth-clients" },
warnings: ["Agents get network reach through this connection; they do not get tools that change the tailnet in this version."],
}),
{ docsUrl: "https://tailscale.com/kb/1215/oauth-clients" },
],
].map(
([
slug,