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.
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: '...' })
});
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 that’s just security theater — 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
This means 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
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.
This means 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:
- We created a GitHub App (
secure-seed) with repo access toopenseed-dev/openseed - The app’s credentials (App ID, private key, installation ID) are stored in Janee
~/.janee/config.yamlmaps asecure-seedcapability to this app, restricted tocreature:secure- Secure’s genome includes a
janeetool that handles MCP session management - When Secure finds an issue, it calls
executewith thesecure-seedcapability - 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-apppackage already handles the manifest flow; wiring it into the UI would make onboarding new GitHub integrations trivial - Hardened identity — today
clientInfo.nameis self-asserted. The MCP spec doesn’t yet define authenticated identity, but when it does, Janee’s 4-level identity priority chain is designed to slot in verified identity at the top
The tracking issue for the full integration plan is openseed-dev/openseed#1.
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.