Build a Node vault client
In this tutorial you build a Node.js script that talks to a vault from outside the platform. By the end it opens a vault, connects an agent, publishes a message, reads the agent’s reply, reads the agent’s cubby, and prints the run’s task logs.
The agent here handles user_message events, writes each message to a
history cubby with a messages table, and publishes a reply event. See
Write an agent to build one. Any agent works the same
way; change the event types and the SQL.
Prerequisites
- Node.js with ES module support, and a project with
"type": "module". - A 32-byte ed25519 seed for the wallet the script acts as.
- The vault API, agreement registry, and chain URLs for your environment.
- The agent’s id,
<agentServicePubkey>:<alias>, from ROC.
Step 1: Install
npm install @cef-ai/vault-sdk@5.5.0Step 2: Construct the SDK with a server signer
import { VaultSDK, KeypairWallet } from "@cef-ai/vault-sdk";
const seed = Uint8Array.from(Buffer.from(process.env.SEED_HEX!, "hex"));
const sdk = new VaultSDK({ endpoint: process.env.VAULT_URL!, garEndpoint: process.env.GAR_URL!, chainUrl: process.env.CHAIN_URL!, signer: await KeypairWallet.fromSeed(seed),});KeypairWallet signs every request directly, with no prompt. garEndpoint and
the signer together let the script sign the connect agreement in Step 4.
Step 3: Open the vault
const vault = await sdk.vault.ensure({ name: "My Vault", onProgress: (e) => console.log("vault:", e.kind),});console.log("vault", vault.id);The first run claims the vault; later runs return it. If you see
OnboardingRequiredError, authorize the storage gateway with @cef-ai/account’s
provisioning.ensure, then run again.
Step 4: Connect the agent
import { VaultRequestError } from "@cef-ai/vault-sdk";
const agentId = process.env.AGENT_ID!;
try { await vault.agents.connect({ agentId, scope: "default" });} catch (e) { if (!(e instanceof VaultRequestError && e.code === "AGENT_ALREADY_CONNECTED")) throw e;}
let connection = await vault.agents.get(agentId);while (connection.status === "provisioning") { await new Promise((r) => setTimeout(r, 1000)); connection = await vault.agents.get(agentId);}console.log("connection", connection.status);connect signs one agreement with your key, submits it, and the vault
provisions the agent’s cubbies. The connection moves from provisioning to
active. A second run finds it already connected.
Step 5: Send a message and wait for the reply
const scope = vault.scope("default");const context = `thread-${Date.now()}`;
const reply = new Promise<string>((resolve) => { const sub = scope.subscribe<{ text: string }>( { context, types: ["reply"] }, (event) => { sub.unsubscribe(); resolve(event.payload.text); }, );});
await scope.publish({ type: "user_message", context, payload: { text: "hi" } });console.log("agent said:", await reply);Subscribe first, then publish, so the reply cannot slip past. The subscription polls the stream every second.
Step 6: Read the agent’s cubby
const asPubKey = agentId.split(":")[0]!;const { columns, rows } = await vault .service(asPubKey) .cubby("history") .query("SELECT text, ts FROM messages ORDER BY ts DESC LIMIT 5");console.log(columns, rows);A service’s cubbies are global within the vault, addressed by the service’s public key and the cubby alias.
Step 7: Print the run’s logs
const jobs = await connection.jobs.list({ limit: 1 });const job = vault.jobs.get(jobs.items[0]!.jobId);const tasks = await job.tasks.list();for (const t of tasks.items) { const { logs } = await job.tasks.logs(t.taskId); for (const line of logs) console.log(t.status, line.message);}Each event your script published became a Task in the agent’s Job; the log lines
are what the agent wrote with console.*.
Run it
SEED_HEX=… VAULT_URL=… GAR_URL=… CHAIN_URL=… AGENT_ID=… npx tsx client.tsWhat you built
A client that does, from outside the platform, what ROC does for a person: open a vault, connect an agent under a signed agreement, talk to it through events, and read what it stored and logged.