Skip to content
API Reference

Grant Agent Access to APIs

Build an autonomous agent that authenticates to Snowflake using Workload Identity Federation. No human approvals, no stored secrets.

Ask Claude to build an autonomous agent for you. By the end of this guide you’ll have a standalone agent that authenticates to Snowflake using Workload Identity Federation (WIF). No human approves each request, and no API keys live on disk. Other agents and clients talk to it over the A2A protocol.

Keycard
Agent
Snowflake
  • Keycard. You need a Keycard URL (e.g. https://<zone-id>.keycard.cloud).

  • Snowflake account with permission to run CREATE USER ... TYPE = SERVICE. The agent will walk you through creating a Workload Identity Federation service user that trusts tokens from your Keycard zone.

  • Keycard session active. Your keycard run -- claude session should still be running.

  • Tailscale with Funnel enabled. The agent serves its identity (public key and OAuth client metadata) and A2A agent card at /.well-known/ endpoints. Keycard pulls these from the public internet to check the agent’s signed assertions, so localhost won’t work.

  1. Ask Claude to create your agent

    Inside your Keycard session, send a prompt like:

    Create a Snowflake autonomous AI agent with its own identity so I can grant it access to tools

    The Keycard skill drafts an action plan and shows it to you before doing anything.

  2. Expose the agent with Tailscale Funnel

    Before scaffolding, start a Tailscale Funnel on the port the agent will use (default 9000). Claude needs the public URL when it registers the agent with Keycard:

    Terminal window
    tailscale funnel --bg 9000

    Tailscale prints a public URL like https://<host>.<tailnet>.ts.net. Copy it; Claude will ask for it as AGENT_BASE_URL. If you already have a funnel running, check with tailscale funnel status.

  3. Claude scaffolds the project and bootstraps the agent’s identity

    Claude generates the code, configures it for your Keycard zone, installs dependencies, and creates the agent’s keypair (an RSA key stored in agent_keys/).

    The agent serves three documents at well-known URLs that Keycard and A2A clients consume:

    EndpointPurpose
    /.well-known/jwks.jsonPublic key Keycard uses to verify the agent’s signed client assertions
    /.well-known/oauth-client-metadataOAuth Client ID Metadata Document. This URL is the agent’s client_id
    /.well-known/agent-card.jsonA2A agent card advertising the agent’s capabilities and auth requirements

    These documents are fetched from the public internet, so the agent has to be reachable through your Tailscale Funnel URL. Claude uses the URL you copied in step 2 when it registers the agent with Keycard.

    Here’s how the generated code handles identity, token exchange, and A2A:

    // src/identity.ts: bootstrap the keypair and publish the CIMD
    const identity = new WebIdentity({
    storageDir: "./agent_keys",
    keyId: process.env.AGENT_NAME,
    });
    await identity.bootstrap();
    router.get("/.well-known/oauth-client-metadata", (_req, res) => {
    res.json({
    client_id: getClientId(),
    token_endpoint_auth_method: "private_key_jwt",
    grant_types: ["urn:ietf:params:oauth:grant-type:token-exchange"],
    jwks: identity.getPublicJwks(),
    });
    });
  4. Configure your .env

    Claude generates a .env file with placeholders. Fill in the values for your environment. You can find your Snowflake account identifier, warehouse, and database from Snowflake’s connection settings.

    AGENT_BASE_URL must be the Tailscale Funnel URL from step 2, and PORT must match the port you funneled. SNOWFLAKE_USER defaults to autonomous_agent, which is the service user you’ll create in the next step.

  5. Create the Snowflake WIF service user

    Claude prints a CREATE USER statement with your Keycard zone URL and the agent’s application ID filled in. Run it in a Snowflake worksheet (or via SnowSQL) using whichever role your Snowflake admin tells you to use:

    CREATE USER autonomous_agent
    WORKLOAD_IDENTITY = (
    TYPE = OIDC
    ISSUER = '<keycard-url>'
    SUBJECT = '<agent-application-id>'
    )
    TYPE = SERVICE;

    That creates a Snowflake service user that trusts OIDC tokens from your Keycard zone for this specific agent. Grant the user a role and warehouse access based on what your Snowflake admin tells you.

  6. Start the agent

    With the funnel running and the WIF user created, start the agent through keycard run so the LLM key comes from your zone:

    Terminal window
    cd <project-name>
    keycard run -- npm start

    At startup the agent:

    1. Loads or generates its RSA keypair
    2. Starts an Express server with the identity and A2A endpoints on PORT
    3. Trades a signed JWT at Keycard’s token endpoint for an access token scoped to snowflakecomputing.com
    4. Connects to Snowflake with that token using the WORKLOAD_IDENTITY authenticator

    No secrets get passed on the command line or stored in env vars. The agent proves who it is with a signed JWT and gets a short-lived token back.

  7. Send a request via A2A

    The agent exposes an A2A JSON-RPC endpoint at /a2a/jsonrpc and an agent card at /.well-known/agent-card.json. Any A2A-compatible client can send tasks to it:

    Terminal window
    curl -X POST http://localhost:9000/a2a/jsonrpc \
    -H "Content-Type: application/json" \
    -d '{"jsonrpc":"2.0","method":"tasks/send","id":"1","params":{"id":"task-1","message":{"role":"user","parts":[{"kind":"text","text":"Query Snowflake for the available sample datasets"}]}}}'

    The agent checks the inbound request with Keycard, runs the query against Snowflake using its short-lived OIDC token, and streams the result back over A2A.

  8. Verify in Keycard Console

    Open Keycard ConsoleAudit Log. You should see entries for:

    credentials:issueAccess token issued for snowflakecomputing.com

    That entry is the agent’s token exchange. The agent presented a signed JWT, and Keycard returned a short-lived access token scoped to Snowflake.

Your agent is running. Here’s what Claude set up, and why it matters when you build your own agents or come back to debug this one.

The agent owns its own keypair instead of using an injected secret. On first startup it generates an RSA keypair and saves it under agent_keys/. The private key never leaves the machine. The public key gets published at /.well-known/jwks.json, and an OAuth Client ID Metadata Document at /.well-known/oauth-client-metadata tells Keycard how to authenticate the agent. That metadata URL is the agent’s client_id.

When the agent needs an access token, it signs a JWT with its private key and sends it to Keycard’s token endpoint. Keycard fetches the metadata document, finds the public key, checks the signature, and issues a scoped access token. That’s the private_key_jwt method from RFC 7523. No shared secret ever lives on disk or in env vars.

To rotate the identity, delete agent_keys/ and restart. The agent makes a new keypair, and Keycard picks up the new public key the next time it fetches the metadata.

See Applications for the full identity and credential model.

Instead of storing Snowflake credentials, the agent uses Keycard as a token broker for Workload Identity Federation. The flow:

  1. The agent signs a JWT with its private key and posts it to Keycard’s /oauth/2/token endpoint, asking for a token scoped to snowflakecomputing.com.
  2. Keycard verifies the JWT against the agent’s published public key.
  3. Keycard returns an OIDC access token whose iss is your zone URL and whose sub is the agent’s application ID. Those are the same values you put into the Snowflake CREATE USER ... WORKLOAD_IDENTITY statement.
  4. The agent connects to Snowflake with the WORKLOAD_IDENTITY authenticator, presenting the OIDC token.

Tokens are cached per resource and refreshed before they expire. If a Snowflake connection drops mid-query, the client throws away its cached token, gets a fresh one, and retries with exponential backoff. You don’t rotate anything by hand, and nothing gets persisted.

For the broader authorization model, see How Keycard Works.

The agent exposes the Agent-to-Agent (A2A) protocol so other agents or clients can send it tasks. The @keycardai/a2a package provides the agent card handler, the JSON-RPC handler, and a keycardUserBuilder that checks inbound caller tokens against your Keycard zone before any task runs.

The agent card at /.well-known/agent-card.json lists the agent’s capabilities and how to authenticate to it, so A2A clients can discover and connect on their own.

Keycard returns invalid_client when the agent requests a token

Keycard couldn’t fetch or verify the agent’s metadata document.

  • Run tailscale funnel status and confirm it points at the same port the agent listens on.
  • Check curl $AGENT_BASE_URL/.well-known/oauth-client-metadata returns JSON.
  • Ask Claude to re-check the application credential’s identifier and jwks_uri match the current Funnel URL.
Token exchange fails with invalid_target or “unknown resource”

The snowflakecomputing.com resource isn’t wired as a dependency of the agent application.

  • Open Keycard ConsoleApplications → your agent → Dependencies and confirm Snowflake is listed.
  • If it’s missing, ask Claude to re-run the dependency wiring step.
Snowflake rejects the OIDC token with an authentication error

The Snowflake WIF user’s ISSUER or SUBJECT don’t match the agent’s token.

  • ISSUER must equal your KEYCARD_URL; SUBJECT must equal the agent’s application ID from Keycard Console.
  • Fix with ALTER USER autonomous_agent SET WORKLOAD_IDENTITY = (TYPE = OIDC ISSUER = '<keycard-url>' SUBJECT = '<agent-application-id>');.

Your agent is running locally with autonomous access to Snowflake. From here you can: