Agent authentication runtime primitives
npm install @lucid-dreams/agent-auth@lucid-dreams/agent-auth runtime around the challenge → exchange → refresh loop while hiding protocol details from consumers.packages/sdk/src/index.ts so every network call stays in sync with the server OpenAPI contract.
src/
runtime/
agent-runtime.ts
agent-auth-client.ts
agent-api-client.ts
token-manager.ts
storage/
memory-adapter.ts
file-adapter.ts
types.ts
wallet/
base-connector.ts
coinbase-connector.ts
privy-connector.ts
local-key-connector.ts
transport/
http-transport.ts
undici-transport.ts
fetch-transport.ts
events.ts
errors.ts
logger.ts
config-loader.ts
`
- The runtime owns orchestration and abstractions; all HTTP contracts flow through the generated SDK under @lucid-dreams/sdk.
- CLI and agent-kit consumers import only the public surface inside src/runtime.Generated SDK Integration
- packages/sdk/src/index.ts re-exports sdk.gen (request builders) and types.gen (request/response types), so the runtime always imports from @lucid-dreams/sdk root.
- The runtime currently relies on the following request builders:
- Auth: postV1AuthAgentsByAgentRefChallenge, postV1AuthAgentsByAgentRefExchange, postV1AuthAgentsByAgentRefRefresh.
- Agents: getV1Agents, getV1AgentsByAgentRef.
- Wallet: postV1AgentsByAgentRefWalletSignMessage, postV1AgentsByAgentRefWalletSignTypedData, postV1AgentsByAgentRefWalletSignTransaction, postV1AgentsByAgentRefWalletSendTransaction.
- See src/runtime/__tests__/sdk-contract.test.ts for a regression test that asserts these helpers remain available and keep their expected request shapes.
- To regenerate the SDK after OpenAPI changes:
1. pnpm --filter @lucid-dreams/sdk run openapi-ts
2. pnpm --filter @lucid-dreams/sdk run build
3. Re-run bun test (at minimum packages/agent-auth) to ensure the runtime still compiles.Module Specifications
$3
- Purpose: High-level façade that bootstraps config, authenticates on demand, and keeps tokens fresh.
- Public Surface:
- static fromConfig(options?: FromConfigOptions): Promise
- static load(options?: LoadOptions): Promise
- authenticate(): Promise
- ensureAccessToken(): Promise
- on(event: AgentAuthEvent, handler: EventHandler): () => void
- shutdown(): Promise (optional lifecycle hook to stop background refresh).
- Collaborators:
- ConfigLoader for config hydration.
- AgentAuthClient for challenge/exchange/refresh.
- AgentApiClient for authenticated API calls.
- TokenManager for caching and refresh scheduling.
- Logger + events.ts for observability.
- Generated SDK Touchpoints: Delegates to AgentAuthClient, which is the only module talking to @lucid-dreams/sdk/auth.
- Implementation Notes:
- Start background refresh on first successful authenticate.
- Emit Authenticated, TokenRefreshed, RefreshFailed, CredentialRevoked, ReauthenticationRequired events.
- Provide escape hatches to override storage, transport, wallet connector, and logger via constructor options.
- Status: Implemented AgentRuntime.fromConfig with configurable transport/auth/token wiring, background refresh, and event hooks, plus AgentRuntime.load convenience wrapper that composes the config loader (src/runtime/agent-runtime.ts, src/runtime/__tests__/agent-runtime.test.ts).$3
- Purpose: Typed wrapper over /challenge, /exchange, /refresh endpoints.
- Public Surface:
- requestChallenge(payload: ChallengeRequest): Promise
- exchange(payload: ExchangeRequest): Promise
- refresh(payload: RefreshRequest): Promise
- Collaborators:
- Accepts an HttpTransport instance for network execution.
- Uses WalletConnector to sign challenge payloads (if runtime delegates signing here).
- Returns typed data using interfaces from @lucid-dreams/sdk/types.
- Generated SDK Touchpoints:
- Calls auth.challenge, auth.exchange, auth.refresh exported from sdk.gen.
- Re-exports relevant TypeScript types (ChallengePayload, etc.) so consumers do not import the generated files directly.
- Implementation Notes:
- Inject baseUrl, clientId, agentId, and telemetry metadata through constructor options.
- Translate HTTP errors into discriminated union errors defined in errors.ts.$3
- Purpose: Authenticated convenience layer over protected agent APIs.
- Public Surface (held behind typed namespaces):
- listAgents(params?): Promise
- sendTransaction(payload): Promise
- Additional generated calls as coverage grows.
- Collaborators:
- Consumes the generated SDK groups (agents., transactions., etc.).
- Accepts a callback getAccessToken(): Promise to attach bearer tokens lazily.
- Uses shared HttpTransport for retries/telemetry.
- Generated SDK Touchpoints:
- Import relevant groups from @lucid-dreams/sdk rather than reimplementing fetch.
- Provide thin wrappers that enrich errors/events before re-throwing.
- Implementation Notes:
- Surfacing coverage: optionally track which generated endpoints remain unwrapped.
- Expose only stable APIs; experimental ones live under experimental/ namespace.
- Status: Agent API client scaffolded with listAgents, getAgent, signMessage, and sendTransaction, leveraging the shared transport + access-token callback with unit coverage (src/runtime/agent-api-client.ts, src/runtime/__tests__/agent-api-client.test.ts).$3
- Purpose: Persist access + refresh tokens, schedule refresh, dedupe concurrent refresh requests.
- Public Surface:
- getCachedAccessToken(): CachedToken | null
- setTokens(tokens: TokenBundle): void
- scheduleRefresh(trigger: () => Promise
- withRefreshLock (single-flight helper).
- Collaborators:
- StorageAdapter for persistence.
- Logger + events for refresh telemetry.
- Implementation Notes:
- Default refresh window: 30s prior to exp.
- Detect nonce/jti changes to guard against replayed refresh handles.
- Provide serialization format for disk persistence and document it.$3
- Purpose: Abstract persistence for refresh handles and metadata.
- Interfaces:
- StorageAdapter#get(key: string): Promise
- StorageAdapter#set(key: string, value: StoredValue): Promise
- StorageAdapter#clear(key: string): Promise
- Default Implementations:
- MemoryStorageAdapter — in-process map.
- FileStorageAdapter — JSON file with advisory locking to survive restarts.
- Extensibility: Document how to supply custom adapters (Redis, AWS Secrets Manager) via AgentRuntime.fromConfig options.$3
- Purpose: Provide signing capabilities for different execution environments.
- Interface (base-connector.ts):
- Extends the runtime ChallengeSigner interface with metadata helpers (getWalletMetadata(), getAddress(), supportsCaip2()).
- Offers shared utilities for challenge normalization, stable JSON stringification, payload encoding detection, and signature extraction.
- Concrete Connectors:
- ServerOrchestratorWalletConnector — delegates signing to the hosted server orchestrator via the agent wallet endpoints.
- LocalEoaWalletConnector — wraps local EOA signers (Node or browser) and supports message + typed data flows.
- CoinbaseManagedWalletConnector (stub) — placeholder for Coinbase managed wallet orchestration.
- PrivyEip191Connector (stub) — placeholder for Privy managed wallet signing.
- Generated SDK Touchpoints: Connectors interact with @lucid-dreams/sdk wallet endpoints when delegating signature requests.
- Implementation Notes: Guardrails for CAIP-2 chain matching, payload normalization, and metadata propagation are covered by dedicated unit tests.$3
- Purpose: Normalise HTTP execution across runtimes.
- Interface:
- request
- Default Implementations:
- undici-transport.ts for Node/Bun.
- fetch-transport.ts for browser/edge environments.
- Status: Fetch-based transport implemented with timeout handling, JSON helpers, and a fetch-compatible façade (asFetch()); Undici factory reuses the fetch transport while allowing optional dispatcher injection.
- Responsibilities:
- Handle retries, backoff, timeout, and instrumentation hooks.
- Provide hook for injecting headers (user agent, telemetry metadata).
- Generated SDK Touchpoints: The generated SDK accepts a fetch implementation; we should pass our wrapped transport to ensure consistent behaviour.$3
- Logger: Accept Pino-compatible logger; default to lightweight structured logger with redacted secrets.
- Events:
- Define AgentAuthEvent enum and payload contracts.
- Provide subscription helpers used by AgentRuntime.
- Emit events from AgentAuthClient, TokenManager, and AgentApiClient as relevant.$3
- Purpose: Merge configuration sources for the runtime.
- Responsibilities:
- Load .lucid-agent.json generated by CLI.
- Overlay environment variables (LUCID_AGENT_*).
- Accept runtime overrides (custom transport, storage adapter, wallet connector, logger).
- Validate schema using zod/types exported from @lucid-dreams/sdk where available.
- Status: Loader implemented with file/env/override precedence, sensible defaults, unit coverage, and now exposed through AgentRuntime.load for a one-call bootstrap that returns the resolved config and source metadata (src/runtime/config-loader.ts, src/runtime/agent-runtime.ts, src/runtime/__tests__/config-loader.test.ts, src/runtime/__tests__/agent-runtime.test.ts).$3
- Purpose: Centralised discriminated union for runtime failures.
- Shape:
- AgentRuntimeError union (CredentialRevoked, RefreshFailed, ChallengeFailed, TransportError, ConfigurationError, CoverageMismatch).
- Include helper type guards (isCredentialRevoked(error) etc.).
- Map generated SDK error responses into these unions.
- Status: Client-facing error taxonomy extended for auth + agent API flows (AgentAuthClientError, AgentApiClientError, TokenManagerError) with contextual metadata surfaced to callers.Cross-Cutting Concerns
- Telemetry: Optional OpenTelemetry spans named agent.auth.challenge, agent.auth.exchange, agent.auth.refresh; attach HTTP status, retry count, latency.
- Debug Mode: Toggle via LUCID_AGENT_DEBUG=1 to emit verbose logs without leaking tokens.
- Edge Compatibility: Document required polyfills (crypto.subtle, TextEncoder) and how transports select runtime-specific implementations.
- Testing:
- Contract tests: mock generated SDK responses to verify token lifecycle scenarios.
- Storage adapter tests: concurrency + persistence.
- Integration smoke: run against local stack using the generated SDK to guarantee parity.
- CLI Alignment: CLI should boot the runtime via AgentRuntime.fromConfig() and rely on AgentApiClient for admin operations.Usage Examples
$3
`ts
import { createWallet } from "@lucid-dreams/agent-auth";const wallet = await createWallet({
// Omitting
config lets createWallet read standard LUCID_* environment variables
events: {
authenticated: (event) => {
console.log("agent authenticated", event.accessToken);
},
},
});await wallet.ensureAccessToken();
const signed = await wallet.signMessage({
message: "gm",
});
console.log("signature", signed.signed);
await wallet.shutdown();
`$3
`ts
import {
createWallet,
createViemAccount,
} from "@lucid-dreams/agent-auth";
import {
createPublicClient,
createWalletClient,
http,
parseEther,
} from "viem";
import { base } from "viem/chains";const lucidWallet = await createWallet();
// Ensure the wallet has an address (authenticate once if using the server orchestrator).
const account = await createViemAccount(lucidWallet, { chainId: base.id });
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const hash = await walletClient.sendTransaction({
account,
to: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
value: parseEther("0.01"),
});
console.log("submitted transaction", hash);
`> Want a runnable script? Check
example/viem.ts for a full workflow that wires
> environment-driven configuration into a viem wallet client, signs messages,
> typed data, and demonstrates optional transaction signing.$3
`ts
import {
AgentRuntime,
MemoryStorageAdapter,
} from "@lucid-dreams/agent-auth";const runtime = await AgentRuntime.fromConfig({
config: {
baseUrl: "https://lucid.daydream.systems",
agentRef: process.env.LUCID_AGENT_REF!,
credentialId: process.env.LUCID_AGENT_CREDENTIAL!,
refreshToken: process.env.LUCID_AGENT_REFRESH_TOKEN,
scopes: ["agents.read"],
},
wallet: {
signer: myWalletConnector, // e.g. new ServerOrchestratorWalletConnector({...})
},
storage: new MemoryStorageAdapter(), // swap in a durable adapter in production
});
runtime.on("tokenRefreshed", (event) => {
console.log("tokens rotated", event.expiresAt.toISOString());
});
const agents = await runtime.api.listAgents();
console.log("agent count", agents.items.length);
await runtime.shutdown();
`$3
`ts
import { AgentRuntime } from "@lucid-dreams/agent-auth";const { runtime, config, sourcePath } = await AgentRuntime.load({
wallet: { signer: myWalletConnector },
loader: {
// optional overrides; defaults align with CLI output
cwd: process.cwd(),
},
});
console.log("loaded agent config from", sourcePath ?? "environment only");
console.log("authenticated agent ref", config.agentRef);
const session = await runtime.ensureAccessToken();
console.log("active scopes", session.scopes);
`$3
`ts
import {
AgentRuntime,
MemoryStorageAdapter,
ServerOrchestratorWalletConnector,
} from "@lucid-dreams/agent-auth";const serverWallet = new ServerOrchestratorWalletConnector({
baseUrl:
process.env.LUCID_SERVER_URL ??
process.env.LUCID_BASE_URL ??
"https://lucid.daydream.systems",
agentRef: process.env.LUCID_AGENT_REF!,
});
if (process.env.LUCID_SERVER_ACCESS_TOKEN) {
serverWallet.setAccessToken(process.env.LUCID_SERVER_ACCESS_TOKEN);
}
const runtime = await AgentRuntime.fromConfig({
config: {
baseUrl:
process.env.LUCID_BASE_URL ?? "https://lucid.daydream.systems",
agentRef: process.env.LUCID_AGENT_REF!,
credentialId: process.env.LUCID_AGENT_CREDENTIAL!,
refreshToken: process.env.LUCID_AGENT_REFRESH_TOKEN,
},
wallet: { signer: serverWallet },
storage: new MemoryStorageAdapter(),
});
// Keep the orchestrator token in sync with runtime refresh events
runtime.on("authenticated", (event) => {
serverWallet.setAccessToken(event.accessToken);
});
runtime.on("tokenRefreshed", (event) => {
serverWallet.setAccessToken(event.accessToken);
});
`$3
`ts
const agentRef = process.env.LUCID_AGENT_REF ?? config.agentRef; // reuse the agent ref from your loaded configconst signed = await runtime.api.signTransaction(
{
caip2: "eip155:8453",
params: {
transaction: {
to: "0x0000000000000000000000000000000000000000",
value: "0x0",
data: "0x",
},
},
authorization_context: {
reason: "lucid.example.sign-transaction",
},
},
{ agentRef }
);
console.log("signed transaction", signed.signed);
`$3
`ts
import {
AgentRuntime,
createFetchTransport,
MemoryStorageAdapter,
} from "@lucid-dreams/agent-auth";const resolvedConfig = {
baseUrl: "https://lucid.daydream.systems",
agentRef: "agent-123",
credentialId: "cred-123",
};
const runtime = await AgentRuntime.fromConfig({
config: resolvedConfig,
wallet: { signer: myWalletConnector },
transport: createFetchTransport({
baseUrl: resolvedConfig.baseUrl,
fetch: myInstrumentedFetch,
defaultHeaders: {
"x-lucid-sdk-version": "agent-auth/0.1.0",
},
}),
storage: new MemoryStorageAdapter(),
});
`Implementation Checklist
- [x] Implemented AgentAuthClient via generated SDK helpers with unit coverage (src/runtime/agent-auth-client.ts, src/runtime/__tests__/agent-auth-client.test.ts).
- [x] Added AgentApiClient coverage for initial agent + wallet calls with unit tests (src/runtime/agent-api-client.ts, src/runtime/__tests__/agent-api-client.test.ts).
- [ ] Finalise generated SDK namespace mappings (auth., agents., etc.) and document them here.
- [ ] Create runtime module files with exported interfaces matching the above specs. (TokenManager + storage adapters + HTTP transport + AgentApiClient + AgentRuntime + ConfigLoader scaffolded with tests.)
- [x] Implemented config loader with env/file/override precedence and tests (src/runtime/config-loader.ts, src/runtime/__tests__/config-loader.test.ts).
- [x] Implement AgentRuntime bootstrap that wires ConfigLoader, TokenManager, AgentAuthClient, and AgentApiClient together.
- [x] Added AgentRuntime.load convenience entrypoint that composes the config loader and surfaces resolved metadata (src/runtime/agent-runtime.ts, src/runtime/__tests__/agent-runtime.test.ts).
- [ ] Provide default storage, transport, wallet connector, and logger implementations with the documented extension points.
- [x] Add DX examples demonstrating how to call the runtime (const runtime = await AgentRuntime.fromConfig();`).