Local agent identity
Persist real Ed25519 keys, explicitly trust public identities and refuse replay and revoked peers.
OpenAgentWorkspace is the Node-only, single-host identity path in SDK 0.7.2. It needs no account or network identity service. The signer holds a real private Ed25519 key; a separate receiver explicitly pins the exported public document.
Provision and prove
Start in a new application directory with Node 20+:
npm install @openagentid/sdk@0.7.2Save this complete first-run program as provision.mjs and run node provision.mjs. The example chooses a unique receiver audience and persists it in the workspaces; its urn is an exact recipient identifier, not a hosted endpoint.
import { randomUUID } from 'node:crypto';
import { OpenAgentWorkspace } from '@openagentid/sdk/local';
const audience = 'urn:example:receiver:' + randomUUID();
const signer = await OpenAgentWorkspace.initialize('./agent-keys', { audience });
const receiver = await OpenAgentWorkspace.initialize('./app-trust', { audience });
const agent = await signer.createAgent('Research assistant');
// An operator or onboarding policy explicitly approves this public identity.
await receiver.trustIdentity(await signer.exportIdentity(agent.did));
const challenge = await receiver.issueChallenge(agent.did);
const proof = await signer.prove(agent.did, challenge);
const identity = await receiver.verify(proof, { nonce: challenge.nonce });
console.log(identity.did);createAgent also trusts the identity within its own workspace. That does not authorize it at the separate receiver. Public export contains no private key; importing an unknown caller's key automatically from its proof would bypass onboarding policy.
Local DIDs use did:oas:openagent:agent:<identifier> by default. The identifier is Base58 of the first 26 bytes of BLAKE3 over the raw 32-byte Ed25519 public key. Names are labels; changing a label is not a DID derivation rule. An explicit namespace can be supplied at initialization, without implying organization membership.
Open existing state and refuse replay
Run this second program from the same directory. It opens the original workspaces, issues a new challenge, verifies a new proof once and confirms that reopening the receiver does not reset consumption:
import assert from 'node:assert/strict';
import { OpenAgentWorkspace } from '@openagentid/sdk/local';
const signer = await OpenAgentWorkspace.open('./agent-keys');
const receiver = await OpenAgentWorkspace.open('./app-trust');
const agents = await signer.listAgents();
const agent = agents.find(value => value.hasPrivateKey && !value.revoked);
if (!agent) throw new Error('No active signing identity');
const challenge = await receiver.issueChallenge(agent.did);
const proof = await signer.prove(agent.did, challenge);
await receiver.verify(proof, { nonce: challenge.nonce });
const reopened = await OpenAgentWorkspace.open('./app-trust');
await assert.rejects(() => reopened.verify(proof, { nonce: challenge.nonce }));
console.log('The original identity works; a consumed proof is refused.');The generated starter tests exercise this behavior through actual HTTP and server restart. The receiver verifies current trust, signature, exact audience and its issued nonce before durably consuming both challenge and proof ID. Local proofs have empty delegations and 60-second lifetimes; issue a fresh challenge for each authentication attempt.
Revoke this receiver's trust
import { OpenAgentWorkspace } from '@openagentid/sdk/local';
const signer = await OpenAgentWorkspace.open('./agent-keys');
const receiver = await OpenAgentWorkspace.open('./app-trust');
const agent = (await signer.listAgents()).find(value => value.hasPrivateKey);
if (!agent) throw new Error('No signing identity');
await receiver.revoke(agent.did);
console.log('Receiver trust withdrawn.');Opening the receiver or importing the same public identity does not undo revocation. The signing key remains, and other receivers retain their independent policies.
Root authority and persistence
The returned local identity has owner: null and an explicit root.kind: 'self'. This is an authorized local self root, not a fabricated human or LifeID enrollment. It can receive normal application permissions under the receiver's policy. Identity authentication itself does not grant those permissions.
Keep each workspace on a private persistent filesystem, backed up and outside Git. Initialization adds a workspace-local .gitignore. Unsafe permissions, symlink paths, corrupt state and abandoned locks fail closed; opening never repairs state or generates replacement keys. Do not put this store on ephemeral serverless storage or synchronize it between hosts. For shared deployment, use IdentityVerifier with authoritative root policy and durable atomic replay storage.
Creating an identity does not start an agent worker, provision a provider credential, register an IAM principal or publish a public profile. Those are separate authorized integrations; see production integration.