import fs from "node:fs/promises"; import crypto from "node:crypto"; function canonicalJson(value) { if (value === null) return "null"; if (Array.isArray(value)) { return `[${value.map((item) => canonicalJson(item)).join(", ")}]`; } if (typeof value === "object") { const keys = Object.keys(value).sort(); return `{${keys .map((key) => `${JSON.stringify(key)}: ${canonicalJson(value[key])}`) .join(", ")}}`; } return JSON.stringify(value); } function stripSecurityFields(payload) { const { hash, signature, ...rest } = payload; return rest; } export class A2SPAClient { constructor({ apiBase, apiKey, agentId, privateKeyPath, timeoutMs = 20000 }) { this.apiBase = apiBase.replace(/\/$/, ""); this.apiKey = apiKey; this.agentId = agentId; this.privateKeyPath = privateKeyPath; this.timeoutMs = timeoutMs; this.privateKeyPem = null; } static fromEnv() { return new A2SPAClient({ apiBase: process.env.A2SPA_API_BASE || "https://aimodularity.com/A2SPA", apiKey: process.env.A2SPA_API_KEY, agentId: process.env.A2SPA_AGENT_ID, privateKeyPath: process.env.A2SPA_PRIVATE_KEY_PATH, }); } async loadPrivateKeyPem() { if (!this.privateKeyPem) { this.privateKeyPem = await fs.readFile(this.privateKeyPath, "utf8"); } return this.privateKeyPem; } sha256Hex(value) { return crypto.createHash("sha256").update(canonicalJson(stripSecurityFields(value))).digest("hex"); } async sign(payload) { const signer = crypto.createSign("RSA-SHA256"); signer.update(canonicalJson(stripSecurityFields(payload))); signer.end(); return signer.sign(await this.loadPrivateKeyPem(), "hex"); } buildPayload(targetAgentId, input, output, alertThreshold = 10, extra = {}) { const payload = { agent_id: this.agentId, target_agent_id: targetAgentId, timestamp: new Date().toISOString(), nonce: crypto.randomUUID(), input, output, alert_threshold: alertThreshold, ...extra, }; payload.hash = this.sha256Hex(payload); return payload; } async sendPayload(targetAgentId, input, output, alertThreshold = 10, extra = {}) { const payload = this.buildPayload(targetAgentId, input, output, alertThreshold, extra); const signature = await this.sign(payload); const response = await fetch(`${this.apiBase}/api/verify_payload`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": this.apiKey, }, body: JSON.stringify({ payload, signature }), }); if (!response.ok) { throw new Error(await response.text()); } return response.json(); } async fetchLogs(agentId = this.agentId) { const params = new URLSearchParams({ agent_id: agentId }); const response = await fetch(`${this.apiBase}/api/logs_for_agent?${params.toString()}`, { headers: { "x-api-key": this.apiKey }, }); if (!response.ok) { throw new Error(await response.text()); } const body = await response.json(); return body.logs || []; } async fetchInbox(agentId = this.agentId, after = "", limit = 50) { const params = new URLSearchParams({ agent_id: agentId, limit: String(limit) }); if (after) params.set("after", after); const response = await fetch(`${this.apiBase}/api/inbox_for_agent?${params.toString()}`, { headers: { "x-api-key": this.apiKey }, }); if (!response.ok) { throw new Error(await response.text()); } const body = await response.json(); return body.messages || []; } async runAndSend(targetAgentId, input, runner, alertThreshold = 10) { const output = await runner(input); return this.sendPayload(targetAgentId, input, output, alertThreshold); } } async function existingAgent(input) { return { status: "ready", summary: `Processed: ${input.message ?? "no message"}`, }; } if (import.meta.url === `file://${process.argv[1]}`) { const client = A2SPAClient.fromEnv(); const result = await client.runAndSend( process.env.A2SPA_TARGET_AGENT_ID, { message: "Hello from my existing Node agent" }, existingAgent, ); console.log(result); }