DEV Community

Ross Douglas
Ross Douglas

Posted on • Edited on • Originally published at openseed.dev

How we stopped giving our AI agents raw API keys

Autonomous agents need API access to do useful work. Our creature Secure files security issues on GitHub. The voyager genome commits code. Future creatures will need Stripe, analytics, whatever.

The naive solution is to inject API keys as environment variables. Every container runtime supports it, every SDK can read from process.env, and it works on day one. It also means every creature has every key, there's no audit trail, and a prompt injection can exfiltrate credentials in a single tool call.

We needed something better.

Janee: a credential proxy for agents

Janee is an MCP server that sits between agents and APIs. You store your credentials in Janee (encrypted at rest with AES-256-GCM), define capabilities with access policies, and agents call APIs by capability name. They never see raw keys.

┌──────────┐     MCP/HTTP    ┌────────┐    real creds   ┌──────────┐
│ Creature │ ──────────────> │ Janee  │ ──────────────> │ External │
│          │                 │        │   proxied req   │   API    │
└──────────┘                 └────────┘                 └──────────┘
   no keys              encrypted at rest               GitHub, etc.
Enter fullscreen mode Exit fullscreen mode

A creature that needs to create a GitHub issue calls:

await janee({
  action: 'execute',
  capability: 'secure-seed',
  method: 'POST',
  path: '/repos/openseed-dev/openseed/issues',
  body: JSON.stringify({ title: 'Security finding', body: '...' })
});
Enter fullscreen mode Exit fullscreen mode

Janee looks up the secure-seed capability, decrypts the GitHub App private key, mints a short-lived installation token, injects it into the request, and proxies to GitHub. The creature never touches the key. Janee logs the request. If something goes wrong, you revoke access in one place.

Identity without custom plumbing

The tricky part with multiple agents is identity. Which creature is making the request? Early prototypes used custom HTTP headers (X-Agent-ID), but any client can set any header.

We landed on something simpler: the MCP protocol already has an initialize handshake where clients send clientInfo.name. Each creature sets this to creature:{name} when it opens a session. Janee captures it from the transport layer, not from tool arguments the client controls.

const transport = new StreamableHTTPClientTransport(url);
await client.connect(transport);
// clientInfo.name = "creature:secure" sent during initialize
Enter fullscreen mode Exit fullscreen mode

Identity resolution uses the same mechanism regardless of transport: stdio, HTTP, in-memory. No extra headers, no extra arguments. Just MCP.

Access control: least privilege by default

With identity sorted, access control is straightforward. In ~/.janee/config.yaml:

server:
  defaultAccess: restricted

capabilities:
  secure-seed:
    service: secure-seed
    allowedAgents: ["creature:secure"]
    autoApprove: true
Enter fullscreen mode Exit fullscreen mode

defaultAccess: restricted means capabilities without an explicit allowedAgents list are hidden from all agents. The secure-seed capability (backed by a GitHub App with repo access to openseed-dev/openseed) is only visible to creature:secure. Other creatures calling list_services won't even know it exists.

If a creature creates a credential at runtime (via the manage_credential tool), it defaults to agent-only. Only the creating creature can use it. It can explicitly grant access to other creatures, but the default is isolation.

Multiple creatures, isolated sessions

OpenSeed runs multiple creatures concurrently. The orchestrator spawns Janee once as a child process in HTTP mode. Each creature gets its own MCP session. Janee creates a fresh Server and Transport instance per initialize handshake, following the official MCP SDK pattern.

Creature A's session state, identity, and access decisions are completely isolated from creature B's. No shared state, no last-writer-wins, no cross-talk.

The real example: Secure files a GitHub issue

Our creature Secure runs the dreamer genome. Its job is to audit OpenSeed for security issues. When it finds something, it needs to create a GitHub issue, which requires authenticating as a GitHub App installation.

The flow:

  1. We created a GitHub App (secure-seed) with repo access to openseed-dev/openseed
  2. The app's credentials (App ID, private key, installation ID) are stored in Janee
  3. ~/.janee/config.yaml maps a secure-seed capability to this app, restricted to creature:secure
  4. Secure's genome includes a janee tool that handles MCP session management
  5. When Secure finds an issue, it calls execute with the secure-seed capability
  6. Janee mints a short-lived GitHub installation token (1hr TTL) and proxies the request

Secure never sees the private key. It can't mint tokens for repos it shouldn't access. If we need to rotate the key, we update Janee. No creature code changes.

What's next

This is the foundation. The obvious next steps:

  • Web UI for secret management: manage Janee credentials from the OpenSeed dashboard instead of editing YAML
  • GitHub App creation from the UI: the create-gh-app package already handles the manifest flow; wiring it into the UI would make onboarding new GitHub integrations trivial
  • Hardened identity: today clientInfo.name is self-asserted. The MCP spec doesn't yet define authenticated identity, but when it does, Janee's identity priority chain is designed to slot in verified identity at the top

If you're building autonomous agents that need API access, consider putting a proxy in front of your keys. Your agents don't need them. They just need the responses.

Janee on GitHub · Janee on npm · OpenSeed

Top comments (4)

Collapse
 
xwero profile image
david duymelinck

Why would agents need API keys? Isn't the simplest way to write a function they can call that accesses the API key.

I agree adding API keys to the environment variables can be dangerous. An option is to never let agents read environment variables. When they try kill the agent.

Instead of environment variables have a encrypted file or files and a decrypt function. And that decrypt function can only be called by other functions.

What if there are multiple keys for a service?

Collapse
 
rsdouglas profile image
Ross Douglas

Yes, you could write a function call that accesses the API without exposing the key! That's what Janee is basically! Except that instead of writing the function you install the package and load up the credentials - no function writing needed.

You can define multiple services so if you have cloudflare keys, for example, with different permissions you could define cloudflare-read-dns and cloudflare-write-pages or whatever.

Collapse
 
xwero profile image
david duymelinck

While I think it is a nice tool. It does too much for me. It is a secrets manager and an http client. I rather keep concerns separated.

I think it is good to create more security content when it comes to AI, because that seems to be the first thing that got thrown overboard.

Thread Thread
 
rsdouglas profile image
Ross Douglas

Thanks! It works great as a local-only tool if you just ignore the HTTP features :) It doesn't open any ports or make anything available over HTTP by default, its just there if you're running agents in docker and want to centralise secrets