@auxiora/agent-protocol 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { getLogger } from '@auxiora/logger';
3
+ const logger = getLogger('agent-protocol:signing');
4
+ /**
5
+ * Ed25519-based message signing and verification for agent-to-agent communication.
6
+ */
7
+ export class MessageSigner {
8
+ publicKey;
9
+ privateKey;
10
+ constructor(keyPair) {
11
+ if (keyPair) {
12
+ this.importKeys(keyPair);
13
+ }
14
+ }
15
+ /** Generate a new Ed25519 key pair. */
16
+ static generateKeyPair() {
17
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
18
+ return {
19
+ publicKey: publicKey
20
+ .export({ type: 'spki', format: 'pem' })
21
+ .toString(),
22
+ privateKey: privateKey
23
+ .export({ type: 'pkcs8', format: 'pem' })
24
+ .toString(),
25
+ };
26
+ }
27
+ /** Import keys from PEM strings. */
28
+ importKeys(keyPair) {
29
+ this.publicKey = crypto.createPublicKey(keyPair.publicKey);
30
+ this.privateKey = crypto.createPrivateKey(keyPair.privateKey);
31
+ }
32
+ /** Import only a public key (for verification). */
33
+ importPublicKey(pem) {
34
+ this.publicKey = crypto.createPublicKey(pem);
35
+ }
36
+ /** Sign a message payload. Returns base64-encoded signature. */
37
+ sign(payload) {
38
+ if (!this.privateKey) {
39
+ throw new Error('Private key not available for signing');
40
+ }
41
+ const signature = crypto.sign(null, Buffer.from(payload, 'utf-8'), this.privateKey);
42
+ return signature.toString('base64');
43
+ }
44
+ /** Verify a signature against a payload. */
45
+ verify(payload, signature, publicKeyPem) {
46
+ const key = publicKeyPem ? crypto.createPublicKey(publicKeyPem) : this.publicKey;
47
+ if (!key) {
48
+ throw new Error('Public key not available for verification');
49
+ }
50
+ try {
51
+ return crypto.verify(null, Buffer.from(payload, 'utf-8'), key, Buffer.from(signature, 'base64'));
52
+ }
53
+ catch (error) {
54
+ logger.debug('Signature verification failed', { error: error });
55
+ return false;
56
+ }
57
+ }
58
+ /** Get the public key PEM string. */
59
+ getPublicKeyPem() {
60
+ return this.publicKey
61
+ ?.export({ type: 'spki', format: 'pem' })
62
+ .toString();
63
+ }
64
+ }
65
+ //# sourceMappingURL=signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,MAAM,GAAG,SAAS,CAAC,wBAAwB,CAAC,CAAC;AAOnD;;GAEG;AACH,MAAM,OAAO,aAAa;IAChB,SAAS,CAA+B;IACxC,UAAU,CAA+B;IAEjD,YAAY,OAAiB;QAC3B,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,uCAAuC;IACvC,MAAM,CAAC,eAAe;QACpB,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;QAExE,OAAO;YACL,SAAS,EAAE,SAAS;iBACjB,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;iBACvC,QAAQ,EAAE;YACb,UAAU,EAAE,UAAU;iBACnB,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;iBACxC,QAAQ,EAAE;SACd,CAAC;IACJ,CAAC;IAED,oCAAoC;IACpC,UAAU,CAAC,OAAgB;QACzB,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC3D,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChE,CAAC;IAED,mDAAmD;IACnD,eAAe,CAAC,GAAW;QACzB,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC;IAED,gEAAgE;IAChE,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC3D,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACpF,OAAO,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED,4CAA4C;IAC5C,MAAM,CAAC,OAAe,EAAE,SAAiB,EAAE,YAAqB;QAC9D,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QACjF,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QAED,IAAI,CAAC;YACH,OAAO,MAAM,CAAC,MAAM,CAClB,IAAI,EACJ,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAC7B,GAAG,EACH,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CACjC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,KAAK,EAAE,KAAc,EAAE,CAAC,CAAC;YACzE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,eAAe;QACb,OAAO,IAAI,CAAC,SAAS;YACnB,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;aACxC,QAAQ,EAAE,CAAC;IAChB,CAAC;CACF"}
@@ -0,0 +1,39 @@
1
+ /** Agent identifier in the form auxiora://user@host */
2
+ export interface AgentIdentifier {
3
+ user: string;
4
+ host: string;
5
+ }
6
+ export type AgentMessageType = 'text' | 'request' | 'response' | 'capability_query' | 'capability_response' | 'ping' | 'pong';
7
+ /** A message exchanged between agents. */
8
+ export interface AgentMessage {
9
+ id: string;
10
+ from: AgentIdentifier;
11
+ to: AgentIdentifier;
12
+ type: AgentMessageType;
13
+ payload: string;
14
+ timestamp: number;
15
+ signature?: string;
16
+ replyTo?: string;
17
+ }
18
+ /** A capability offered by an agent. */
19
+ export interface AgentCapability {
20
+ name: string;
21
+ description: string;
22
+ inputSchema?: Record<string, unknown>;
23
+ outputSchema?: Record<string, unknown>;
24
+ }
25
+ /** An entry in the agent directory. */
26
+ export interface AgentDirectoryEntry {
27
+ identifier: AgentIdentifier;
28
+ displayName: string;
29
+ capabilities: AgentCapability[];
30
+ publicKey: string;
31
+ endpoint: string;
32
+ lastSeen: number;
33
+ registeredAt: number;
34
+ }
35
+ /** Format an AgentIdentifier to its URI form. */
36
+ export declare function formatAgentId(id: AgentIdentifier): string;
37
+ /** Parse an agent URI string to an AgentIdentifier. */
38
+ export declare function parseAgentId(uri: string): AgentIdentifier | undefined;
39
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,SAAS,GACT,UAAU,GACV,kBAAkB,GAClB,qBAAqB,GACrB,MAAM,GACN,MAAM,CAAC;AAEX,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,eAAe,CAAC;IACtB,EAAE,EAAE,eAAe,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,uCAAuC;AACvC,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,EAAE,EAAE,eAAe,GAAG,MAAM,CAEzD;AAED,uDAAuD;AACvD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAIrE"}
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /** Format an AgentIdentifier to its URI form. */
2
+ export function formatAgentId(id) {
3
+ return `auxiora://${id.user}@${id.host}`;
4
+ }
5
+ /** Parse an agent URI string to an AgentIdentifier. */
6
+ export function parseAgentId(uri) {
7
+ const match = uri.match(/^auxiora:\/\/([^@]+)@(.+)$/);
8
+ if (!match)
9
+ return undefined;
10
+ return { user: match[1], host: match[2] };
11
+ }
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA8CA,iDAAiD;AACjD,MAAM,UAAU,aAAa,CAAC,EAAmB;IAC/C,OAAO,aAAa,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;IACtD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC5C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@auxiora/agent-protocol",
3
+ "version": "1.0.0",
4
+ "description": "Agent-to-agent communication protocol with Ed25519 signing and discovery",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@auxiora/core": "1.0.0",
16
+ "@auxiora/audit": "1.0.0",
17
+ "@auxiora/logger": "1.0.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "clean": "rm -rf dist",
25
+ "typecheck": "tsc --noEmit"
26
+ }
27
+ }
@@ -0,0 +1,112 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { getLogger } from '@auxiora/logger';
4
+ import { getAuxioraDir } from '@auxiora/core';
5
+ import type { AgentIdentifier, AgentDirectoryEntry, AgentCapability } from './types.js';
6
+ import { formatAgentId } from './types.js';
7
+
8
+ const logger = getLogger('agent-protocol:directory');
9
+
10
+ /**
11
+ * Directory of known agents for discovery and lookup.
12
+ */
13
+ export class AgentDirectory {
14
+ private filePath: string;
15
+
16
+ constructor(options?: { dir?: string }) {
17
+ const dir = options?.dir ?? path.join(getAuxioraDir(), 'agent-protocol');
18
+ this.filePath = path.join(dir, 'directory.json');
19
+ }
20
+
21
+ async register(
22
+ identifier: AgentIdentifier,
23
+ displayName: string,
24
+ publicKey: string,
25
+ endpoint: string,
26
+ capabilities?: AgentCapability[],
27
+ ): Promise<AgentDirectoryEntry> {
28
+ const entries = await this.readFile();
29
+ const uri = formatAgentId(identifier);
30
+
31
+ // Update if already registered
32
+ const existing = entries.find(e => formatAgentId(e.identifier) === uri);
33
+ if (existing) {
34
+ existing.displayName = displayName;
35
+ existing.publicKey = publicKey;
36
+ existing.endpoint = endpoint;
37
+ existing.capabilities = capabilities ?? existing.capabilities;
38
+ existing.lastSeen = Date.now();
39
+ await this.writeFile(entries);
40
+ logger.debug('Updated agent registration', { uri });
41
+ return existing;
42
+ }
43
+
44
+ const entry: AgentDirectoryEntry = {
45
+ identifier,
46
+ displayName,
47
+ capabilities: capabilities ?? [],
48
+ publicKey,
49
+ endpoint,
50
+ lastSeen: Date.now(),
51
+ registeredAt: Date.now(),
52
+ };
53
+
54
+ entries.push(entry);
55
+ await this.writeFile(entries);
56
+ logger.debug('Registered agent', { uri });
57
+ return entry;
58
+ }
59
+
60
+ async lookup(identifier: AgentIdentifier): Promise<AgentDirectoryEntry | undefined> {
61
+ const entries = await this.readFile();
62
+ const uri = formatAgentId(identifier);
63
+ return entries.find(e => formatAgentId(e.identifier) === uri);
64
+ }
65
+
66
+ async search(query: string): Promise<AgentDirectoryEntry[]> {
67
+ const entries = await this.readFile();
68
+ const lowerQuery = query.toLowerCase();
69
+
70
+ return entries.filter(e =>
71
+ e.displayName.toLowerCase().includes(lowerQuery) ||
72
+ formatAgentId(e.identifier).toLowerCase().includes(lowerQuery) ||
73
+ e.capabilities.some(c =>
74
+ c.name.toLowerCase().includes(lowerQuery) ||
75
+ c.description.toLowerCase().includes(lowerQuery),
76
+ ),
77
+ );
78
+ }
79
+
80
+ async remove(identifier: AgentIdentifier): Promise<boolean> {
81
+ const entries = await this.readFile();
82
+ const uri = formatAgentId(identifier);
83
+ const filtered = entries.filter(e => formatAgentId(e.identifier) !== uri);
84
+ if (filtered.length === entries.length) return false;
85
+
86
+ await this.writeFile(filtered);
87
+ logger.debug('Removed agent', { uri });
88
+ return true;
89
+ }
90
+
91
+ async listAll(): Promise<AgentDirectoryEntry[]> {
92
+ return this.readFile();
93
+ }
94
+
95
+ private async readFile(): Promise<AgentDirectoryEntry[]> {
96
+ try {
97
+ const content = await fs.readFile(this.filePath, 'utf-8');
98
+ return JSON.parse(content) as AgentDirectoryEntry[];
99
+ } catch (error) {
100
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
101
+ return [];
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ private async writeFile(entries: AgentDirectoryEntry[]): Promise<void> {
108
+ const dir = path.dirname(this.filePath);
109
+ await fs.mkdir(dir, { recursive: true });
110
+ await fs.writeFile(this.filePath, JSON.stringify(entries, null, 2), 'utf-8');
111
+ }
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type {
2
+ AgentIdentifier,
3
+ AgentMessage,
4
+ AgentMessageType,
5
+ AgentCapability,
6
+ AgentDirectoryEntry,
7
+ } from './types.js';
8
+ export { formatAgentId, parseAgentId } from './types.js';
9
+ export { AgentProtocol } from './protocol.js';
10
+ export type { MessageHandler } from './protocol.js';
11
+ export { MessageSigner } from './signing.js';
12
+ export type { KeyPair } from './signing.js';
13
+ export { AgentDirectory } from './directory.js';
14
+ export { ProtocolServer } from './server.js';
@@ -0,0 +1,145 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { getLogger } from '@auxiora/logger';
3
+ import { audit } from '@auxiora/audit';
4
+ import type { AgentIdentifier, AgentMessage, AgentMessageType } from './types.js';
5
+ import { formatAgentId } from './types.js';
6
+ import type { MessageSigner } from './signing.js';
7
+ import type { AgentDirectory } from './directory.js';
8
+
9
+ const logger = getLogger('agent-protocol:protocol');
10
+
11
+ export interface MessageHandler {
12
+ (message: AgentMessage): Promise<AgentMessage | void>;
13
+ }
14
+
15
+ /**
16
+ * Core agent-to-agent protocol. JSON-over-HTTPS messaging
17
+ * with Ed25519 signature verification.
18
+ */
19
+ export class AgentProtocol {
20
+ private identity: AgentIdentifier;
21
+ private signer: MessageSigner;
22
+ private directory: AgentDirectory;
23
+ private handlers = new Map<AgentMessageType, MessageHandler>();
24
+ private inbox: AgentMessage[] = [];
25
+ private maxInboxSize = 1000;
26
+
27
+ constructor(
28
+ identity: AgentIdentifier,
29
+ signer: MessageSigner,
30
+ directory: AgentDirectory,
31
+ ) {
32
+ this.identity = identity;
33
+ this.signer = signer;
34
+ this.directory = directory;
35
+ }
36
+
37
+ /** Send a message to another agent. */
38
+ async send(
39
+ to: AgentIdentifier,
40
+ type: AgentMessageType,
41
+ payload: string,
42
+ replyTo?: string,
43
+ ): Promise<AgentMessage> {
44
+ const message: AgentMessage = {
45
+ id: `msg-${crypto.randomUUID().slice(0, 8)}`,
46
+ from: this.identity,
47
+ to,
48
+ type,
49
+ payload,
50
+ timestamp: Date.now(),
51
+ replyTo,
52
+ };
53
+
54
+ // Sign the message
55
+ const signaturePayload = `${message.id}:${message.timestamp}:${message.payload}`;
56
+ message.signature = this.signer.sign(signaturePayload);
57
+
58
+ // Look up the target agent's endpoint
59
+ const entry = await this.directory.lookup(to);
60
+ if (!entry) {
61
+ throw new Error(`Agent not found: ${formatAgentId(to)}`);
62
+ }
63
+
64
+ // Send via HTTPS (in production would use fetch)
65
+ // For now, store in local outbox for testing
66
+ logger.debug('Message sent', {
67
+ to: formatAgentId(to),
68
+ type,
69
+ id: message.id,
70
+ });
71
+ void audit('agent_protocol.message_sent', {
72
+ to: formatAgentId(to),
73
+ type,
74
+ id: message.id,
75
+ });
76
+
77
+ return message;
78
+ }
79
+
80
+ /** Receive and process an incoming message. */
81
+ async receive(message: AgentMessage): Promise<AgentMessage | void> {
82
+ // Verify signature if present
83
+ if (message.signature) {
84
+ const senderEntry = await this.directory.lookup(message.from);
85
+ if (senderEntry) {
86
+ const signaturePayload = `${message.id}:${message.timestamp}:${message.payload}`;
87
+ const valid = this.signer.verify(signaturePayload, message.signature, senderEntry.publicKey);
88
+ if (!valid) {
89
+ logger.debug('Invalid message signature', { from: formatAgentId(message.from) });
90
+ throw new Error('Invalid message signature');
91
+ }
92
+ }
93
+ }
94
+
95
+ // Store in inbox
96
+ this.inbox.push(message);
97
+ if (this.inbox.length > this.maxInboxSize) {
98
+ this.inbox = this.inbox.slice(-this.maxInboxSize);
99
+ }
100
+
101
+ logger.debug('Message received', {
102
+ from: formatAgentId(message.from),
103
+ type: message.type,
104
+ id: message.id,
105
+ });
106
+ void audit('agent_protocol.message_received', {
107
+ from: formatAgentId(message.from),
108
+ type: message.type,
109
+ id: message.id,
110
+ });
111
+
112
+ // Dispatch to handler
113
+ const handler = this.handlers.get(message.type);
114
+ if (handler) {
115
+ return handler(message);
116
+ }
117
+ }
118
+
119
+ /** Register a handler for a message type. */
120
+ onMessage(type: AgentMessageType, handler: MessageHandler): void {
121
+ this.handlers.set(type, handler);
122
+ }
123
+
124
+ /** Discover agents with specific capabilities. */
125
+ async discover(query: string): Promise<AgentIdentifier[]> {
126
+ const entries = await this.directory.search(query);
127
+ return entries.map(e => e.identifier);
128
+ }
129
+
130
+ /** Query another agent's capabilities. */
131
+ async negotiate(target: AgentIdentifier): Promise<AgentMessage> {
132
+ return this.send(target, 'capability_query', '');
133
+ }
134
+
135
+ /** Get messages from the inbox. */
136
+ getInbox(limit?: number): AgentMessage[] {
137
+ const max = limit ?? 50;
138
+ return this.inbox.slice(-max);
139
+ }
140
+
141
+ /** Get this agent's identity. */
142
+ getIdentity(): AgentIdentifier {
143
+ return this.identity;
144
+ }
145
+ }
package/src/server.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ import type { AgentMessage } from './types.js';
3
+ import type { AgentProtocol } from './protocol.js';
4
+
5
+ const logger = getLogger('agent-protocol:server');
6
+
7
+ /**
8
+ * HTTP endpoint handler for receiving agent messages.
9
+ * Designed to be mounted on an Express router.
10
+ */
11
+ export class ProtocolServer {
12
+ private protocol: AgentProtocol;
13
+
14
+ constructor(protocol: AgentProtocol) {
15
+ this.protocol = protocol;
16
+ }
17
+
18
+ /**
19
+ * Handle an incoming HTTP request with an agent message.
20
+ * Returns the response message (if any) or an error.
21
+ */
22
+ async handleRequest(body: unknown): Promise<{
23
+ status: number;
24
+ body: { success: boolean; response?: AgentMessage; error?: string };
25
+ }> {
26
+ if (!body || typeof body !== 'object') {
27
+ return {
28
+ status: 400,
29
+ body: { success: false, error: 'Invalid request body' },
30
+ };
31
+ }
32
+
33
+ const message = body as AgentMessage;
34
+
35
+ if (!message.id || !message.from || !message.to || !message.type || message.payload === undefined) {
36
+ return {
37
+ status: 400,
38
+ body: { success: false, error: 'Missing required message fields' },
39
+ };
40
+ }
41
+
42
+ try {
43
+ const response = await this.protocol.receive(message);
44
+
45
+ return {
46
+ status: 200,
47
+ body: {
48
+ success: true,
49
+ ...(response ? { response } : {}),
50
+ },
51
+ };
52
+ } catch (error) {
53
+ const msg = error instanceof Error ? error.message : 'Unknown error';
54
+ logger.debug('Failed to handle agent message', { error: error as Error });
55
+ return {
56
+ status: 400,
57
+ body: { success: false, error: msg },
58
+ };
59
+ }
60
+ }
61
+
62
+ /** Get the protocol instance. */
63
+ getProtocol(): AgentProtocol {
64
+ return this.protocol;
65
+ }
66
+ }
package/src/signing.ts ADDED
@@ -0,0 +1,85 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { getLogger } from '@auxiora/logger';
3
+
4
+ const logger = getLogger('agent-protocol:signing');
5
+
6
+ export interface KeyPair {
7
+ publicKey: string;
8
+ privateKey: string;
9
+ }
10
+
11
+ /**
12
+ * Ed25519-based message signing and verification for agent-to-agent communication.
13
+ */
14
+ export class MessageSigner {
15
+ private publicKey: crypto.KeyObject | undefined;
16
+ private privateKey: crypto.KeyObject | undefined;
17
+
18
+ constructor(keyPair?: KeyPair) {
19
+ if (keyPair) {
20
+ this.importKeys(keyPair);
21
+ }
22
+ }
23
+
24
+ /** Generate a new Ed25519 key pair. */
25
+ static generateKeyPair(): KeyPair {
26
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
27
+
28
+ return {
29
+ publicKey: publicKey
30
+ .export({ type: 'spki', format: 'pem' })
31
+ .toString(),
32
+ privateKey: privateKey
33
+ .export({ type: 'pkcs8', format: 'pem' })
34
+ .toString(),
35
+ };
36
+ }
37
+
38
+ /** Import keys from PEM strings. */
39
+ importKeys(keyPair: KeyPair): void {
40
+ this.publicKey = crypto.createPublicKey(keyPair.publicKey);
41
+ this.privateKey = crypto.createPrivateKey(keyPair.privateKey);
42
+ }
43
+
44
+ /** Import only a public key (for verification). */
45
+ importPublicKey(pem: string): void {
46
+ this.publicKey = crypto.createPublicKey(pem);
47
+ }
48
+
49
+ /** Sign a message payload. Returns base64-encoded signature. */
50
+ sign(payload: string): string {
51
+ if (!this.privateKey) {
52
+ throw new Error('Private key not available for signing');
53
+ }
54
+
55
+ const signature = crypto.sign(null, Buffer.from(payload, 'utf-8'), this.privateKey);
56
+ return signature.toString('base64');
57
+ }
58
+
59
+ /** Verify a signature against a payload. */
60
+ verify(payload: string, signature: string, publicKeyPem?: string): boolean {
61
+ const key = publicKeyPem ? crypto.createPublicKey(publicKeyPem) : this.publicKey;
62
+ if (!key) {
63
+ throw new Error('Public key not available for verification');
64
+ }
65
+
66
+ try {
67
+ return crypto.verify(
68
+ null,
69
+ Buffer.from(payload, 'utf-8'),
70
+ key,
71
+ Buffer.from(signature, 'base64'),
72
+ );
73
+ } catch (error) {
74
+ logger.debug('Signature verification failed', { error: error as Error });
75
+ return false;
76
+ }
77
+ }
78
+
79
+ /** Get the public key PEM string. */
80
+ getPublicKeyPem(): string | undefined {
81
+ return this.publicKey
82
+ ?.export({ type: 'spki', format: 'pem' })
83
+ .toString();
84
+ }
85
+ }
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ /** Agent identifier in the form auxiora://user@host */
2
+ export interface AgentIdentifier {
3
+ user: string;
4
+ host: string;
5
+ }
6
+
7
+ export type AgentMessageType =
8
+ | 'text'
9
+ | 'request'
10
+ | 'response'
11
+ | 'capability_query'
12
+ | 'capability_response'
13
+ | 'ping'
14
+ | 'pong';
15
+
16
+ /** A message exchanged between agents. */
17
+ export interface AgentMessage {
18
+ id: string;
19
+ from: AgentIdentifier;
20
+ to: AgentIdentifier;
21
+ type: AgentMessageType;
22
+ payload: string;
23
+ timestamp: number;
24
+ signature?: string;
25
+ replyTo?: string;
26
+ }
27
+
28
+ /** A capability offered by an agent. */
29
+ export interface AgentCapability {
30
+ name: string;
31
+ description: string;
32
+ inputSchema?: Record<string, unknown>;
33
+ outputSchema?: Record<string, unknown>;
34
+ }
35
+
36
+ /** An entry in the agent directory. */
37
+ export interface AgentDirectoryEntry {
38
+ identifier: AgentIdentifier;
39
+ displayName: string;
40
+ capabilities: AgentCapability[];
41
+ publicKey: string;
42
+ endpoint: string;
43
+ lastSeen: number;
44
+ registeredAt: number;
45
+ }
46
+
47
+ /** Format an AgentIdentifier to its URI form. */
48
+ export function formatAgentId(id: AgentIdentifier): string {
49
+ return `auxiora://${id.user}@${id.host}`;
50
+ }
51
+
52
+ /** Parse an agent URI string to an AgentIdentifier. */
53
+ export function parseAgentId(uri: string): AgentIdentifier | undefined {
54
+ const match = uri.match(/^auxiora:\/\/([^@]+)@(.+)$/);
55
+ if (!match) return undefined;
56
+ return { user: match[1], host: match[2] };
57
+ }