@agentlair/openai-agents
v0.2.0
Published
AgentLair adapter for the OpenAI Agents SDK — issue per-agent AATs, attach Bearer tokens to tool calls, emit audit envelopes, and sign outbound HTTP requests with RFC 9421 / Web Bot Auth.
Maintainers
Readme
@agentlair/openai-agents
AgentLair adapter for the OpenAI Agents SDK. Issue an Agent Authentication Token (AAT) per agent run, attach it to outbound tool calls, and emit a signed audit envelope per invocation — without changing how you write agents.
Install
npm install @agentlair/openai-agents @openai/agents
# or
bun add @agentlair/openai-agents @openai/agents@openai/agents is a (optional) peer dependency. Zero runtime dependencies otherwise. Works in Node 18+, Bun, and edge runtimes (Cloudflare Workers, Deno Deploy).
Get a free AgentLair API key at agentlair.dev — no card required.
Usage
import { Agent, run, tool } from '@openai/agents';
import { withAgentLair } from '@agentlair/openai-agents';
import { z } from 'zod';
const echo = tool({
name: 'echo',
description: 'Echo a string back to the caller',
parameters: z.object({ msg: z.string() }),
execute: async ({ msg }) => `you said: ${msg}`,
});
const myAgent = new Agent({
name: 'demo',
instructions: 'Echo what the user says.',
tools: [echo],
});
// Wrap once at startup. The original agent is unchanged.
const governed = withAgentLair(myAgent, {
apiKey: process.env.AGENTLAIR_API_KEY!,
audience: 'https://my-mcp.example.com',
scopes: ['mcp:tools:read'],
agentName: 'demo',
});
const result = await run(governed, 'echo "hello"');
// On every tool call, AgentLair issued (or reused) an AAT and recorded
// an audit envelope. Inspect at https://agentlair.dev/v1/audit/<jti>.That's it. No changes to your tools or Agent shape — withAgentLair returns a shallow clone with each tool's execute wrapped to issue an AAT and emit an audit envelope.
What it does
- Issues an AAT before each tool invocation (cached and reused until expiry by default — one issue per run, not one per tool call).
- Records an audit envelope for every tool call — args, result, duration, jti, agent name, audience. Best-effort; failures never block the tool.
- Returns a verifiable did:web — each AAT embeds
did:web:agentlair.dev:agents:<account_id>, resolvable against AgentLair's JWKS. - Signs outbound HTTP requests with RFC 9421 / Web Bot Auth (v0.2.0+) —
signRequest()producesSignature/Signature-Input/Signature-Agentheaders that Google Cloud Fraud Defense and any Web Bot Auth–aware origin can verify.
Web Bot Auth (RFC 9421 HTTP Message Signatures)
Sign outbound requests inside a tool so the receiving origin can prove the request came from your AgentLair-anchored agent — no shared secrets, no Bearer rotation:
import { signRequest } from '@agentlair/openai-agents';
// 32-byte Ed25519 keypair — generate once, register the public half at
// POST https://agentlair.dev/v1/agents/signing-keys, then keep the private
// half (and its 32-byte raw seed) wherever your agent stores secrets.
const req = new Request('https://api.example.com/resource');
const signed = await signRequest(req, { privateKey, publicKey });
const res = await fetch(signed);signRequest adds three headers:
Signature-Input— covered components,created,expires,keyid(the RFC 8037 JWK thumbprint), andtag="web-bot-auth".Signature— Ed25519 over the canonical signature base.Signature-Agent—https://agentlair.dev/agents/<thumbprint>— verifiers fetch the JWK there.
The same Ed25519 keypair you register at /v1/agents/signing-keys is the one used here. AgentLair's directory endpoint at GET /agents/<thumbprint> resolves the public JWK with no auth required, so any verifier (Google Fraud Defense, a custom origin) can complete the loop.
See docs/web-bot-auth for the full architecture.
Explicit Agent Self-Attestation
Agents can post L3 self-attestations directly — no tool wrapping required. Use this when your agent wants to declare intent, record a decision, or attest to a constraint before acting:
import { logAuditEvent } from '@agentlair/openai-agents';
// Agent declares it will not exceed budget before starting work
const entry = await logAuditEvent(
{
category: 'budget',
action: 'budget.no_exceed',
details: { limit_usd: 10, projected_usd: 2.5, model: 'gpt-4o-mini' },
},
{ apiKey: process.env.AGENTLAIR_API_KEY! },
);
console.log(entry.id); // e.g. "WSZqkmVNIzXrxwqnIwbF"
console.log(entry.signature); // Ed25519 sig — independently verifiable
console.log(entry.prev_hash); // SHA-256 of previous chain entryThe entry is hash-chained and Ed25519-signed server-side. Verify the chain at GET /v1/audit/log.
Valid categories: task, tool_call, observation, reasoning, output, budget, memory, session, and more — see ALLOWED_AUDIT_CATEGORIES.
Action format: lowercase dot-separated, e.g. "task.complete", "budget.no_exceed". Validates against AUDIT_ACTION_REGEX client-side before the network call.
Throws AuditLogError with typed code on validation failures or non-2xx responses.
Lower-level API
If you want fine-grained control:
import { issueAATForAgent, recordAuditEvent, wrapTool } from '@agentlair/openai-agents';
// Issue a token by hand
const aat = await issueAATForAgent({
apiKey: process.env.AGENTLAIR_API_KEY!,
audience: 'https://my-mcp.example.com',
scopes: ['mcp:tools:read', 'mcp:tools:execute'],
ttl: 3600,
});
console.log(aat.jti); // aat_xxxxxxxxxxxxxxxx
console.log(aat.did); // did:web:agentlair.dev:agents:acc_...
console.log(aat.token); // eyJhbGciOiJFZERTQSIsImtpZCI6...
// Wrap a single tool with a custom audit sink
const wrapped = wrapTool(myTool, {
apiKey: process.env.AGENTLAIR_API_KEY!,
audience: 'https://my-mcp.example.com',
preIssuedAAT: aat,
onAuditEvent: (e) => myObservabilityPipeline.send(e),
});Options
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| apiKey | string | — | Required. al_live_* or al_pod_* from agentlair.dev. |
| audience | string | — | Required. Target service URL the AAT will be presented to. |
| scopes | string[] | ['mcp:tools:read'] | Each must match ^[a-z][a-z0-9._:-]*$. |
| ttl | number | 3600 | Lifetime in seconds. Max 86400. |
| agentName | string | — | al_name claim in the AAT. |
| agentEmail | string | — | al_email claim in the AAT. |
| agentLairBaseUrl | string | https://agentlair.dev | Override for staging/self-host. |
| cacheAAT | boolean | true | Reuse the AAT across tool calls until expiry. |
| preIssuedAAT | AAT | — | Skip the issue call entirely. Useful in tests. |
| onAuditEvent | function | best-effort POST | Custom audit sink. Failures never block the tool. |
| fetchImpl | typeof fetch | global fetch | For testing or edge runtimes. |
Verification
Audit envelopes are signed with AgentLair's Ed25519 audit key. To verify a token or audit entry independently:
# Token — verify against JWKS
curl https://agentlair.dev/.well-known/jwks.json
# Per-token metadata
curl https://agentlair.dev/v1/audit/<jti>Errors
issueAATForAgent throws AgentLairError with a typed code:
| Code | Meaning |
| --- | --- |
| invalid_options | apiKey or audience missing/malformed |
| network_error | fetch threw (DNS, timeout, etc.) |
| http_error | non-2xx response from /v1/tokens/issue (check .status) |
| invalid_response | unparseable JSON or unexpected shape |
Tool wrapping (wrapTool, withAgentLair) never throws on AgentLair-side failures — the original tool error (if any) is rethrown unchanged.
logAuditEvent throws AuditLogError with a typed code:
| Code | Meaning |
| --- | --- |
| invalid_category | category not in ALLOWED_AUDIT_CATEGORIES |
| invalid_action | action violates AUDIT_ACTION_REGEX or length limit (1–128 chars) |
| details_too_large | details JSON-serialised size exceeds 4 KB |
| network_error | fetch threw (DNS, timeout, etc.) |
| http_error | non-2xx response from /v1/audit/log (check .status) |
| invalid_response | unparseable JSON or unexpected response shape |
Reference
- AgentLair: https://agentlair.dev
- OpenAI Agents SDK (TypeScript): https://github.com/openai/openai-agents-js
- Source: https://github.com/piiiico/agentlair-primitives
License
Apache-2.0
