adp-agent 0.2.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.
Files changed (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/config.example.json +32 -0
  4. package/dist/agent-id.d.ts +9 -0
  5. package/dist/agent-id.js +118 -0
  6. package/dist/agent-id.js.map +1 -0
  7. package/dist/agent-id.test.d.ts +1 -0
  8. package/dist/agent-id.test.js +36 -0
  9. package/dist/agent-id.test.js.map +1 -0
  10. package/dist/canonical.d.ts +5 -0
  11. package/dist/canonical.js +35 -0
  12. package/dist/canonical.js.map +1 -0
  13. package/dist/canonical.test.d.ts +1 -0
  14. package/dist/canonical.test.js +33 -0
  15. package/dist/canonical.test.js.map +1 -0
  16. package/dist/capability-test.d.ts +1 -0
  17. package/dist/capability-test.js +108 -0
  18. package/dist/capability-test.js.map +1 -0
  19. package/dist/chat.d.ts +1 -0
  20. package/dist/chat.js +166 -0
  21. package/dist/chat.js.map +1 -0
  22. package/dist/contacts-test.d.ts +1 -0
  23. package/dist/contacts-test.js +225 -0
  24. package/dist/contacts-test.js.map +1 -0
  25. package/dist/crypto.d.ts +11 -0
  26. package/dist/crypto.js +103 -0
  27. package/dist/crypto.js.map +1 -0
  28. package/dist/crypto.test.d.ts +1 -0
  29. package/dist/crypto.test.js +40 -0
  30. package/dist/crypto.test.js.map +1 -0
  31. package/dist/discovery.d.ts +36 -0
  32. package/dist/discovery.js +291 -0
  33. package/dist/discovery.js.map +1 -0
  34. package/dist/envelope.d.ts +42 -0
  35. package/dist/envelope.js +58 -0
  36. package/dist/envelope.js.map +1 -0
  37. package/dist/gateway.d.ts +44 -0
  38. package/dist/gateway.js +255 -0
  39. package/dist/gateway.js.map +1 -0
  40. package/dist/index.d.ts +33 -0
  41. package/dist/index.js +75 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/integration-test.d.ts +1 -0
  44. package/dist/integration-test.js +206 -0
  45. package/dist/integration-test.js.map +1 -0
  46. package/dist/key-store.d.ts +10 -0
  47. package/dist/key-store.js +81 -0
  48. package/dist/key-store.js.map +1 -0
  49. package/dist/manifest.d.ts +35 -0
  50. package/dist/manifest.js +24 -0
  51. package/dist/manifest.js.map +1 -0
  52. package/dist/mdns-test.d.ts +1 -0
  53. package/dist/mdns-test.js +93 -0
  54. package/dist/mdns-test.js.map +1 -0
  55. package/dist/relay-peer-test.d.ts +1 -0
  56. package/dist/relay-peer-test.js +220 -0
  57. package/dist/relay-peer-test.js.map +1 -0
  58. package/dist/relay-test.d.ts +1 -0
  59. package/dist/relay-test.js +92 -0
  60. package/dist/relay-test.js.map +1 -0
  61. package/dist/relay.d.ts +60 -0
  62. package/dist/relay.js +277 -0
  63. package/dist/relay.js.map +1 -0
  64. package/dist/src/agent-id.d.ts +9 -0
  65. package/dist/src/agent-id.js +44 -0
  66. package/dist/src/agent-id.js.map +1 -0
  67. package/dist/src/agent-id.test.d.ts +1 -0
  68. package/dist/src/agent-id.test.js +36 -0
  69. package/dist/src/agent-id.test.js.map +1 -0
  70. package/dist/src/canonical.d.ts +5 -0
  71. package/dist/src/canonical.js +37 -0
  72. package/dist/src/canonical.js.map +1 -0
  73. package/dist/src/canonical.test.d.ts +1 -0
  74. package/dist/src/canonical.test.js +33 -0
  75. package/dist/src/canonical.test.js.map +1 -0
  76. package/dist/src/capabilities.d.ts +3 -0
  77. package/dist/src/capabilities.js +39 -0
  78. package/dist/src/capabilities.js.map +1 -0
  79. package/dist/src/config.d.ts +35 -0
  80. package/dist/src/config.js +3 -0
  81. package/dist/src/config.js.map +1 -0
  82. package/dist/src/contacts.d.ts +28 -0
  83. package/dist/src/contacts.js +118 -0
  84. package/dist/src/contacts.js.map +1 -0
  85. package/dist/src/crypto.d.ts +11 -0
  86. package/dist/src/crypto.js +83 -0
  87. package/dist/src/crypto.js.map +1 -0
  88. package/dist/src/crypto.test.d.ts +1 -0
  89. package/dist/src/crypto.test.js +40 -0
  90. package/dist/src/crypto.test.js.map +1 -0
  91. package/dist/src/discovery.d.ts +39 -0
  92. package/dist/src/discovery.js +317 -0
  93. package/dist/src/discovery.js.map +1 -0
  94. package/dist/src/envelope.d.ts +55 -0
  95. package/dist/src/envelope.js +95 -0
  96. package/dist/src/envelope.js.map +1 -0
  97. package/dist/src/gateway.d.ts +78 -0
  98. package/dist/src/gateway.js +540 -0
  99. package/dist/src/gateway.js.map +1 -0
  100. package/dist/src/index.d.ts +22 -0
  101. package/dist/src/index.js +81 -0
  102. package/dist/src/index.js.map +1 -0
  103. package/dist/src/key-rotation.d.ts +27 -0
  104. package/dist/src/key-rotation.js +41 -0
  105. package/dist/src/key-rotation.js.map +1 -0
  106. package/dist/src/key-store.d.ts +10 -0
  107. package/dist/src/key-store.js +81 -0
  108. package/dist/src/key-store.js.map +1 -0
  109. package/dist/src/logger.d.ts +9 -0
  110. package/dist/src/logger.js +18 -0
  111. package/dist/src/logger.js.map +1 -0
  112. package/dist/src/manifest.d.ts +37 -0
  113. package/dist/src/manifest.js +24 -0
  114. package/dist/src/manifest.js.map +1 -0
  115. package/dist/src/mcp-server.d.ts +38 -0
  116. package/dist/src/mcp-server.js +408 -0
  117. package/dist/src/mcp-server.js.map +1 -0
  118. package/dist/src/net-utils.d.ts +3 -0
  119. package/dist/src/net-utils.js +71 -0
  120. package/dist/src/net-utils.js.map +1 -0
  121. package/dist/src/registry/cache.d.ts +12 -0
  122. package/dist/src/registry/cache.js +45 -0
  123. package/dist/src/registry/cache.js.map +1 -0
  124. package/dist/src/registry/client.d.ts +43 -0
  125. package/dist/src/registry/client.js +245 -0
  126. package/dist/src/registry/client.js.map +1 -0
  127. package/dist/src/registry/config.d.ts +32 -0
  128. package/dist/src/registry/config.js +79 -0
  129. package/dist/src/registry/config.js.map +1 -0
  130. package/dist/src/registry/db.d.ts +10 -0
  131. package/dist/src/registry/db.js +97 -0
  132. package/dist/src/registry/db.js.map +1 -0
  133. package/dist/src/registry/index.d.ts +5 -0
  134. package/dist/src/registry/index.js +22 -0
  135. package/dist/src/registry/index.js.map +1 -0
  136. package/dist/src/registry/service.d.ts +45 -0
  137. package/dist/src/registry/service.js +802 -0
  138. package/dist/src/registry/service.js.map +1 -0
  139. package/dist/src/relay.d.ts +69 -0
  140. package/dist/src/relay.js +399 -0
  141. package/dist/src/relay.js.map +1 -0
  142. package/dist/src/task-manager.d.ts +55 -0
  143. package/dist/src/task-manager.js +150 -0
  144. package/dist/src/task-manager.js.map +1 -0
  145. package/dist/src/trust-store.d.ts +24 -0
  146. package/dist/src/trust-store.js +144 -0
  147. package/dist/src/trust-store.js.map +1 -0
  148. package/dist/src/webhook-client.d.ts +30 -0
  149. package/dist/src/webhook-client.js +78 -0
  150. package/dist/src/webhook-client.js.map +1 -0
  151. package/dist/start-mcp.d.ts +2 -0
  152. package/dist/start-mcp.js +126 -0
  153. package/dist/start-mcp.js.map +1 -0
  154. package/dist/start-registry.d.ts +2 -0
  155. package/dist/start-registry.js +33 -0
  156. package/dist/start-registry.js.map +1 -0
  157. package/dist/start-relay.d.ts +1 -0
  158. package/dist/start-relay.js +35 -0
  159. package/dist/start-relay.js.map +1 -0
  160. package/dist/start.d.ts +1 -0
  161. package/dist/start.js +364 -0
  162. package/dist/start.js.map +1 -0
  163. package/dist/task-manager.d.ts +55 -0
  164. package/dist/task-manager.js +145 -0
  165. package/dist/task-manager.js.map +1 -0
  166. package/dist/task-test.d.ts +1 -0
  167. package/dist/task-test.js +188 -0
  168. package/dist/task-test.js.map +1 -0
  169. package/dist/test-auth.d.ts +1 -0
  170. package/dist/test-auth.js +166 -0
  171. package/dist/test-auth.js.map +1 -0
  172. package/dist/test-key-rotation.d.ts +2 -0
  173. package/dist/test-key-rotation.js +114 -0
  174. package/dist/test-key-rotation.js.map +1 -0
  175. package/dist/test-registry.d.ts +2 -0
  176. package/dist/test-registry.js +123 -0
  177. package/dist/test-registry.js.map +1 -0
  178. package/dist/trust-store.d.ts +23 -0
  179. package/dist/trust-store.js +111 -0
  180. package/dist/trust-store.js.map +1 -0
  181. package/package.json +96 -0
  182. package/schema.sql +54 -0
@@ -0,0 +1,802 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RegistryService = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const cors_1 = __importDefault(require("cors"));
9
+ const crypto_1 = require("../crypto");
10
+ const agent_id_1 = require("../agent-id");
11
+ const canonical_1 = require("../canonical");
12
+ class RegistryService {
13
+ constructor(config, db, cache) {
14
+ this.heartbeatQueue = new Set();
15
+ this.heartbeatDrainTimer = null;
16
+ this.heartbeatBatchSize = 500;
17
+ this.rateLimitMap = new Map();
18
+ this.rateLimitMax = 100;
19
+ this.rateLimitWindowMs = 60000;
20
+ this.rateLimitCleanupTimer = null;
21
+ this.config = config;
22
+ this.db = db;
23
+ this.cache = cache;
24
+ this.app = (0, express_1.default)();
25
+ this.setupMiddleware();
26
+ this.setupRoutes();
27
+ this.startHeartbeatDrain();
28
+ this.startRateLimitCleanup();
29
+ }
30
+ startRateLimitCleanup() {
31
+ this.rateLimitCleanupTimer = setInterval(() => {
32
+ const now = Date.now();
33
+ for (const [ip, entry] of this.rateLimitMap) {
34
+ if (now > entry.resetAt) {
35
+ this.rateLimitMap.delete(ip);
36
+ }
37
+ }
38
+ }, this.rateLimitWindowMs);
39
+ }
40
+ startHeartbeatDrain() {
41
+ this.heartbeatDrainTimer = setInterval(() => {
42
+ this.drainHeartbeats().catch(err => {
43
+ console.error('Heartbeat drain error:', err);
44
+ });
45
+ }, 5000);
46
+ }
47
+ setupMiddleware() {
48
+ if (this.config.cors.enabled) {
49
+ this.app.use((0, cors_1.default)({
50
+ origin: this.config.cors.origins
51
+ }));
52
+ }
53
+ this.app.use(express_1.default.json({ limit: '1mb' }));
54
+ this.app.use((req, res, next) => {
55
+ console.log(`${req.method} ${req.path}`);
56
+ next();
57
+ });
58
+ this.app.use((req, res, next) => {
59
+ if (!this.checkRateLimit(req, res))
60
+ return;
61
+ next();
62
+ });
63
+ }
64
+ checkRateLimit(req, res) {
65
+ const ip = req.ip || req.socket.remoteAddress || 'unknown';
66
+ const now = Date.now();
67
+ const entry = this.rateLimitMap.get(ip);
68
+ if (!entry || now > entry.resetAt) {
69
+ this.rateLimitMap.set(ip, { count: 1, resetAt: now + this.rateLimitWindowMs });
70
+ }
71
+ else if (entry.count >= this.rateLimitMax) {
72
+ res.status(429).json({
73
+ error: { code: 'RATE_LIMITED', message: 'Too many requests' }
74
+ });
75
+ return false;
76
+ }
77
+ else {
78
+ entry.count++;
79
+ }
80
+ return true;
81
+ }
82
+ tokenAuth(req, res, next) {
83
+ if (!this.config.token.enabled) {
84
+ next();
85
+ return;
86
+ }
87
+ const token = req.body?.token || req.headers.authorization?.replace(/^Bearer\s+/i, '');
88
+ if (!token) {
89
+ res.status(401).json({
90
+ error: {
91
+ code: 'UNAUTHORIZED',
92
+ message: 'Token is required'
93
+ }
94
+ });
95
+ return;
96
+ }
97
+ const tokenEntries = this.config.token.tokens || {};
98
+ const tokenEntry = tokenEntries[token];
99
+ if (!tokenEntry) {
100
+ res.status(401).json({
101
+ error: {
102
+ code: 'UNAUTHORIZED',
103
+ message: 'Invalid token'
104
+ }
105
+ });
106
+ return;
107
+ }
108
+ req.tokenNamespace =
109
+ tokenEntry.namespace;
110
+ req.tokenCapabilities =
111
+ tokenEntry.capabilities;
112
+ next();
113
+ }
114
+ signatureAuth(req, res, next) {
115
+ const signatureHeader = req.headers['x-adp-signature'];
116
+ if (!signatureHeader) {
117
+ res.status(401).json({
118
+ error: {
119
+ code: 'UNAUTHORIZED',
120
+ message: 'X-ADP-Signature header is required for this operation'
121
+ }
122
+ });
123
+ return;
124
+ }
125
+ const body = req.body;
126
+ if (!body?.agent_id) {
127
+ res.status(400).json({
128
+ error: {
129
+ code: 'INVALID_PARAMS',
130
+ message: 'agent_id is required in request body for signature verification'
131
+ }
132
+ });
133
+ return;
134
+ }
135
+ try {
136
+ const sigBytes = (0, crypto_1.decodeBase64URL)(signatureHeader);
137
+ if (sigBytes.length !== 64) {
138
+ res.status(401).json({
139
+ error: {
140
+ code: 'UNAUTHORIZED',
141
+ message: 'Invalid signature'
142
+ }
143
+ });
144
+ return;
145
+ }
146
+ const publicKey = (() => {
147
+ try {
148
+ return (0, agent_id_1.extractPublicKey)(body.agent_id);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ })();
154
+ if (!publicKey) {
155
+ res.status(401).json({
156
+ error: {
157
+ code: 'INVALID_PARAMS',
158
+ message: 'Invalid agent_id format'
159
+ }
160
+ });
161
+ return;
162
+ }
163
+ const signedPayload = {};
164
+ signedPayload.agent_id = body.agent_id;
165
+ signedPayload.manifest = body.manifest;
166
+ signedPayload.routes = body.routes;
167
+ if (body.rotation)
168
+ signedPayload.rotation = body.rotation;
169
+ signedPayload.timestamp = req.headers['x-adp-timestamp'] || body.timestamp;
170
+ const canonical = (0, canonical_1.canonicalize)(signedPayload);
171
+ const messageBytes = new TextEncoder().encode(canonical);
172
+ const isValid = (0, crypto_1.verify)(publicKey, messageBytes, sigBytes);
173
+ if (!isValid) {
174
+ res.status(401).json({
175
+ error: {
176
+ code: 'UNAUTHORIZED',
177
+ message: 'X-ADP-Signature verification failed'
178
+ }
179
+ });
180
+ return;
181
+ }
182
+ }
183
+ catch {
184
+ res.status(401).json({
185
+ error: {
186
+ code: 'UNAUTHORIZED',
187
+ message: 'Invalid signature format'
188
+ }
189
+ });
190
+ return;
191
+ }
192
+ next();
193
+ }
194
+ setupRoutes() {
195
+ this.app.get('/health', this.healthCheck.bind(this));
196
+ this.app.post('/v1/agents', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.registerAgent.bind(this));
197
+ this.app.put('/v1/agents/:initialId', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.updateAgent.bind(this));
198
+ this.app.post('/v1/agents/:initialId/heartbeat', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.heartbeat.bind(this));
199
+ this.app.get('/v1/agents/:initialId', this.getAgent.bind(this));
200
+ this.app.delete('/v1/agents/:initialId', this.tokenAuth.bind(this), this.signatureAuth.bind(this), this.deleteAgent.bind(this));
201
+ this.app.get('/v1/agents', this.searchAgents.bind(this));
202
+ }
203
+ normalizeInitialId(raw) {
204
+ const decoded = decodeURIComponent(Array.isArray(raw) ? raw[0] : raw);
205
+ if (decoded.startsWith('adp://')) {
206
+ try {
207
+ return (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(decoded));
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
213
+ return decoded;
214
+ }
215
+ healthCheck(req, res) {
216
+ res.json({
217
+ status: 'ok',
218
+ version: '0.2.0',
219
+ timestamp: new Date().toISOString()
220
+ });
221
+ }
222
+ async registerAgent(req, res) {
223
+ try {
224
+ const request = req.body;
225
+ const validationError = this.validateRegistrationRequest(request);
226
+ if (validationError) {
227
+ res.status(400).json({
228
+ error: {
229
+ code: 'INVALID_PARAMS',
230
+ message: validationError
231
+ }
232
+ });
233
+ return;
234
+ }
235
+ const initialId = (() => {
236
+ try {
237
+ return (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(request.agent_id));
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ })();
243
+ if (!initialId) {
244
+ res.status(400).json({
245
+ error: {
246
+ code: 'INVALID_PARAMS',
247
+ message: 'Invalid agent_id format'
248
+ }
249
+ });
250
+ return;
251
+ }
252
+ const parsed = (() => {
253
+ try {
254
+ return (0, agent_id_1.parseAgentId)(request.agent_id);
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ })();
260
+ if (!parsed) {
261
+ res.status(400).json({
262
+ error: {
263
+ code: 'INVALID_PARAMS',
264
+ message: 'Invalid agent_id format'
265
+ }
266
+ });
267
+ return;
268
+ }
269
+ const connection = await this.db.getConnection();
270
+ try {
271
+ const [existing] = await connection.execute('SELECT expires_at FROM agents WHERE initial_id = ?', [initialId]);
272
+ const rows = existing;
273
+ if (rows.length > 0) {
274
+ const row = rows[0];
275
+ const isExpired = new Date(row.expires_at) <= new Date();
276
+ if (!isExpired) {
277
+ res.status(409).json({
278
+ error: {
279
+ code: 'AGENT_ALREADY_EXISTS',
280
+ message: 'Agent already registered, use PUT to update'
281
+ }
282
+ });
283
+ return;
284
+ }
285
+ }
286
+ const expiresAt = new Date(Date.now() + this.config.registration.ttlSeconds * 1000);
287
+ await connection.execute(`INSERT INTO agents (initial_id, current_agent_id, namespace, manifest, routes, last_seen, expires_at)
288
+ VALUES (?, ?, ?, ?, ?, NOW(), ?)
289
+ ON DUPLICATE KEY UPDATE
290
+ current_agent_id = VALUES(current_agent_id),
291
+ namespace = VALUES(namespace),
292
+ manifest = VALUES(manifest),
293
+ routes = VALUES(routes),
294
+ last_seen = NOW(),
295
+ expires_at = VALUES(expires_at)`, [
296
+ initialId,
297
+ request.agent_id,
298
+ parsed.namespace,
299
+ JSON.stringify(request.manifest),
300
+ JSON.stringify(request.routes),
301
+ expiresAt
302
+ ]);
303
+ await connection.execute('DELETE FROM agent_capabilities WHERE initial_id = ?', [initialId]);
304
+ const capabilities = request.manifest.capabilities || [];
305
+ for (const cap of capabilities) {
306
+ const capName = typeof cap === 'string' ? cap : cap.capability;
307
+ await connection.execute('INSERT IGNORE INTO agent_capabilities (initial_id, capability) VALUES (?, ?)', [initialId, capName]);
308
+ }
309
+ const agentData = {
310
+ initial_id: initialId,
311
+ current_agent_id: request.agent_id,
312
+ manifest: request.manifest,
313
+ routes: request.routes,
314
+ last_seen: new Date().toISOString(),
315
+ expires_at: expiresAt.toISOString(),
316
+ rotation_chain: []
317
+ };
318
+ await this.cache.setAgent(initialId, agentData, this.config.registration.ttlSeconds);
319
+ const alreadyExisted = rows.length > 0;
320
+ res.status(alreadyExisted ? 200 : 201).json({
321
+ initial_id: initialId,
322
+ current_agent_id: request.agent_id,
323
+ status: 'ok',
324
+ expires_at: expiresAt.toISOString()
325
+ });
326
+ }
327
+ finally {
328
+ connection.release();
329
+ }
330
+ }
331
+ catch (error) {
332
+ console.error('Registration error:', error);
333
+ res.status(500).json({
334
+ error: {
335
+ code: 'INTERNAL_ERROR',
336
+ message: 'Internal server error'
337
+ }
338
+ });
339
+ }
340
+ }
341
+ async updateAgent(req, res) {
342
+ try {
343
+ const initialId = this.normalizeInitialId(req.params.initialId);
344
+ if (!initialId) {
345
+ res.status(400).json({
346
+ error: {
347
+ code: 'INVALID_PARAMS',
348
+ message: 'Invalid agent_id format in URL'
349
+ }
350
+ });
351
+ return;
352
+ }
353
+ const request = req.body;
354
+ // Validate request
355
+ const validationError = this.validateRegistrationRequest(request);
356
+ if (validationError) {
357
+ res.status(400).json({
358
+ error: {
359
+ code: 'INVALID_PARAMS',
360
+ message: validationError
361
+ }
362
+ });
363
+ return;
364
+ }
365
+ const connection = await this.db.getConnection();
366
+ try {
367
+ // Check if agent exists
368
+ const [agents] = await connection.execute('SELECT current_agent_id FROM agents WHERE initial_id = ?', [initialId]);
369
+ if (agents.length === 0) {
370
+ res.status(404).json({
371
+ error: {
372
+ code: 'AGENT_NOT_FOUND',
373
+ message: 'Agent not found'
374
+ }
375
+ });
376
+ return;
377
+ }
378
+ const currentAgentId = agents[0].current_agent_id;
379
+ const currentPublicKey = (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(currentAgentId));
380
+ const newPublicKey = (() => {
381
+ try {
382
+ return (0, crypto_1.encodeBase64URL)((0, agent_id_1.extractPublicKey)(request.agent_id));
383
+ }
384
+ catch {
385
+ return null;
386
+ }
387
+ })();
388
+ if (!newPublicKey) {
389
+ res.status(400).json({
390
+ error: {
391
+ code: 'INVALID_PARAMS',
392
+ message: 'Invalid agent_id format'
393
+ }
394
+ });
395
+ return;
396
+ }
397
+ const isRotation = newPublicKey !== currentPublicKey;
398
+ if (isRotation && !request.rotation) {
399
+ res.status(400).json({
400
+ error: {
401
+ code: 'INVALID_PARAMS',
402
+ message: 'Key rotation requires rotation envelope'
403
+ }
404
+ });
405
+ return;
406
+ }
407
+ if (isRotation && request.rotation) {
408
+ // Add to rotation chain
409
+ const [maxSeq] = await connection.execute('SELECT MAX(sequence) as max_seq FROM rotation_chain WHERE initial_id = ?', [initialId]);
410
+ const nextSequence = (maxSeq[0].max_seq ?? -1) + 1;
411
+ await connection.execute(`INSERT INTO rotation_chain (initial_id, sequence, from_agent_id, to_agent_id, envelope)
412
+ VALUES (?, ?, ?, ?, ?)`, [
413
+ initialId,
414
+ nextSequence,
415
+ currentAgentId,
416
+ request.agent_id,
417
+ JSON.stringify(request.rotation)
418
+ ]);
419
+ // Clear rotation cache
420
+ await this.cache.setRotationChain(initialId, []);
421
+ }
422
+ // Calculate new expires_at
423
+ const expiresAt = new Date(Date.now() + this.config.registration.ttlSeconds * 1000);
424
+ // Update agent
425
+ const parsed = (() => {
426
+ try {
427
+ return (0, agent_id_1.parseAgentId)(request.agent_id);
428
+ }
429
+ catch {
430
+ return null;
431
+ }
432
+ })();
433
+ if (!parsed) {
434
+ res.status(400).json({
435
+ error: {
436
+ code: 'INVALID_PARAMS',
437
+ message: 'Invalid agent_id format'
438
+ }
439
+ });
440
+ return;
441
+ }
442
+ await connection.execute(`UPDATE agents
443
+ SET current_agent_id = ?, namespace = ?, manifest = ?, routes = ?, last_seen = NOW(), expires_at = ?
444
+ WHERE initial_id = ?`, [
445
+ request.agent_id,
446
+ parsed.namespace,
447
+ JSON.stringify(request.manifest),
448
+ JSON.stringify(request.routes),
449
+ expiresAt,
450
+ initialId
451
+ ]);
452
+ // Update capabilities
453
+ await connection.execute('DELETE FROM agent_capabilities WHERE initial_id = ?', [initialId]);
454
+ const capabilities = request.manifest.capabilities || [];
455
+ for (const cap of capabilities) {
456
+ const capName = typeof cap === 'string' ? cap : cap.capability;
457
+ await connection.execute('INSERT IGNORE INTO agent_capabilities (initial_id, capability) VALUES (?, ?)', [initialId, capName]);
458
+ }
459
+ // Get updated rotation chain
460
+ const [chainResult] = await connection.execute('SELECT envelope FROM rotation_chain WHERE initial_id = ? ORDER BY sequence ASC', [initialId]);
461
+ const rotationChain = chainResult.map(row => ({
462
+ envelope: row.envelope
463
+ }));
464
+ // Update cache
465
+ const agentData = {
466
+ initial_id: initialId,
467
+ current_agent_id: request.agent_id,
468
+ manifest: request.manifest,
469
+ routes: request.routes,
470
+ last_seen: new Date().toISOString(),
471
+ expires_at: expiresAt.toISOString(),
472
+ rotation_chain: rotationChain
473
+ };
474
+ await this.cache.setAgent(initialId, agentData, this.config.registration.ttlSeconds);
475
+ await this.cache.setRotationChain(initialId, rotationChain, this.config.registration.ttlSeconds);
476
+ res.json({
477
+ initial_id: initialId,
478
+ current_agent_id: request.agent_id,
479
+ status: 'ok',
480
+ expires_at: expiresAt.toISOString()
481
+ });
482
+ }
483
+ finally {
484
+ connection.release();
485
+ }
486
+ }
487
+ catch (error) {
488
+ console.error('Update error:', error);
489
+ res.status(500).json({
490
+ error: {
491
+ code: 'INTERNAL_ERROR',
492
+ message: 'Internal server error'
493
+ }
494
+ });
495
+ }
496
+ }
497
+ async getAgent(req, res) {
498
+ try {
499
+ const initialId = this.normalizeInitialId(req.params.initialId);
500
+ if (!initialId) {
501
+ res.status(400).json({
502
+ error: { code: 'INVALID_PARAMS', message: 'Invalid agent_id format in URL' }
503
+ });
504
+ return;
505
+ }
506
+ // Check cache first
507
+ const cached = await this.cache.getAgent(initialId);
508
+ if (cached) {
509
+ const now = new Date();
510
+ const expiresAt = new Date(cached.expires_at);
511
+ const online = now < expiresAt;
512
+ if (online) {
513
+ res.json({
514
+ ...cached,
515
+ online: true
516
+ });
517
+ return;
518
+ }
519
+ }
520
+ const connection = await this.db.getConnection();
521
+ try {
522
+ const [agents] = await connection.execute('SELECT * FROM agents WHERE initial_id = ?', [initialId]);
523
+ if (agents.length === 0) {
524
+ res.status(404).json({
525
+ initial_id: initialId,
526
+ online: false,
527
+ error: {
528
+ code: 'AGENT_NOT_FOUND',
529
+ message: '未注册或注册已过期'
530
+ }
531
+ });
532
+ return;
533
+ }
534
+ const agent = agents[0];
535
+ const now = new Date();
536
+ const expiresAt = new Date(agent.expires_at);
537
+ const online = now < expiresAt;
538
+ // Get rotation chain
539
+ const [chainResult] = await connection.execute('SELECT envelope FROM rotation_chain WHERE initial_id = ? ORDER BY sequence ASC', [initialId]);
540
+ const rotationChain = chainResult.map(row => ({
541
+ envelope: row.envelope
542
+ }));
543
+ const agentData = {
544
+ initial_id: agent.initial_id,
545
+ current_agent_id: agent.current_agent_id,
546
+ online,
547
+ manifest: JSON.parse(agent.manifest),
548
+ routes: JSON.parse(agent.routes),
549
+ rotation_chain: rotationChain,
550
+ last_seen: agent.last_seen.toISOString()
551
+ };
552
+ // Cache the result
553
+ if (online) {
554
+ const ttl = Math.max(1, Math.floor((expiresAt.getTime() - now.getTime()) / 1000));
555
+ await this.cache.setAgent(initialId, agentData, ttl);
556
+ await this.cache.setRotationChain(initialId, rotationChain, ttl);
557
+ }
558
+ if (!online) {
559
+ res.status(404).json({
560
+ initial_id: initialId,
561
+ online: false,
562
+ error: {
563
+ code: 'AGENT_NOT_FOUND',
564
+ message: '未注册或注册已过期'
565
+ }
566
+ });
567
+ return;
568
+ }
569
+ res.json(agentData);
570
+ }
571
+ finally {
572
+ connection.release();
573
+ }
574
+ }
575
+ catch (error) {
576
+ console.error('Get agent error:', error);
577
+ res.status(500).json({
578
+ error: {
579
+ code: 'INTERNAL_ERROR',
580
+ message: 'Internal server error'
581
+ }
582
+ });
583
+ }
584
+ }
585
+ async deleteAgent(req, res) {
586
+ try {
587
+ const initialId = this.normalizeInitialId(req.params.initialId);
588
+ if (!initialId) {
589
+ res.status(400).json({
590
+ error: { code: 'INVALID_PARAMS', message: 'Invalid agent_id format in URL' }
591
+ });
592
+ return;
593
+ }
594
+ const connection = await this.db.getConnection();
595
+ try {
596
+ await connection.execute('DELETE FROM agents WHERE initial_id = ?', [initialId]);
597
+ await this.cache.deleteAgent(initialId);
598
+ res.json({ status: 'ok' });
599
+ }
600
+ finally {
601
+ connection.release();
602
+ }
603
+ }
604
+ catch (error) {
605
+ console.error('Delete agent error:', error);
606
+ res.status(500).json({
607
+ error: {
608
+ code: 'INTERNAL_ERROR',
609
+ message: 'Internal server error'
610
+ }
611
+ });
612
+ }
613
+ }
614
+ async heartbeat(req, res) {
615
+ try {
616
+ const initialId = this.normalizeInitialId(req.params.initialId);
617
+ if (!initialId) {
618
+ res.status(400).json({
619
+ error: { code: 'INVALID_PARAMS', message: 'Invalid agent_id format in URL' }
620
+ });
621
+ return;
622
+ }
623
+ const cached = await this.cache.getAgent(initialId);
624
+ if (!cached) {
625
+ const connection = await this.db.getConnection();
626
+ try {
627
+ const [agents] = await connection.execute('SELECT 1 FROM agents WHERE initial_id = ? AND expires_at > NOW()', [initialId]);
628
+ if (agents.length === 0) {
629
+ res.status(404).json({
630
+ error: {
631
+ code: 'AGENT_NOT_FOUND',
632
+ message: 'Agent not registered'
633
+ }
634
+ });
635
+ return;
636
+ }
637
+ }
638
+ finally {
639
+ connection.release();
640
+ }
641
+ }
642
+ this.heartbeatQueue.add(initialId);
643
+ res.json({
644
+ status: 'ok',
645
+ expires_at: new Date(Date.now() + this.config.registration.ttlSeconds * 1000).toISOString()
646
+ });
647
+ }
648
+ catch (error) {
649
+ console.error('Heartbeat error:', error);
650
+ res.status(500).json({
651
+ error: {
652
+ code: 'INTERNAL_ERROR',
653
+ message: 'Internal server error'
654
+ }
655
+ });
656
+ }
657
+ }
658
+ async drainHeartbeats() {
659
+ if (this.heartbeatQueue.size === 0)
660
+ return;
661
+ const oldQueue = this.heartbeatQueue;
662
+ this.heartbeatQueue = new Set();
663
+ const allIds = Array.from(oldQueue);
664
+ const expiresAt = new Date(Date.now() + this.config.registration.ttlSeconds * 1000);
665
+ for (let i = 0; i < allIds.length; i += this.heartbeatBatchSize) {
666
+ const batch = allIds.slice(i, i + this.heartbeatBatchSize);
667
+ const connection = await this.db.getConnection();
668
+ try {
669
+ const placeholders = batch.map(() => '?').join(',');
670
+ await connection.execute(`UPDATE agents SET last_seen = NOW(), expires_at = DATE_ADD(NOW(), INTERVAL ? SECOND) WHERE initial_id IN (${placeholders})`, [this.config.registration.ttlSeconds, ...batch]);
671
+ }
672
+ finally {
673
+ connection.release();
674
+ }
675
+ for (const id of batch) {
676
+ const cached = await this.cache.getAgent(id);
677
+ if (cached) {
678
+ cached.last_seen = new Date().toISOString();
679
+ cached.expires_at = expiresAt.toISOString();
680
+ await this.cache.setAgent(id, cached, this.config.registration.ttlSeconds);
681
+ }
682
+ }
683
+ }
684
+ }
685
+ async searchAgents(req, res) {
686
+ try {
687
+ const namespace = req.query.namespace;
688
+ const capability = req.query.capability;
689
+ const cursor = req.query.cursor;
690
+ const limit = Math.min(100, parseInt(req.query.limit || '20'));
691
+ const offset = cursor ? parseInt(Buffer.from(cursor, 'base64').toString(), 10) : 0;
692
+ const connection = await this.db.getConnection();
693
+ try {
694
+ let query = `
695
+ SELECT DISTINCT a.*
696
+ FROM agents a
697
+ WHERE a.expires_at > NOW()
698
+ `;
699
+ const params = [];
700
+ if (namespace) {
701
+ query += ' AND a.namespace = ?';
702
+ params.push(namespace);
703
+ }
704
+ if (capability) {
705
+ query += `
706
+ AND EXISTS (
707
+ SELECT 1 FROM agent_capabilities ac
708
+ WHERE ac.initial_id = a.initial_id
709
+ AND ac.capability = ?
710
+ )
711
+ `;
712
+ params.push(capability);
713
+ }
714
+ query += ` ORDER BY a.last_seen DESC LIMIT ? OFFSET ?`;
715
+ params.push(limit, offset);
716
+ const [agents] = await connection.execute(query, params);
717
+ const agentList = agents;
718
+ const results = [];
719
+ if (agentList.length > 0) {
720
+ const initialIds = agentList.map((a) => a.initial_id);
721
+ const placeholders = initialIds.map(() => '?').join(',');
722
+ const [chainResults] = await connection.execute(`SELECT initial_id, envelope FROM rotation_chain WHERE initial_id IN (${placeholders}) ORDER BY sequence ASC`, initialIds);
723
+ const chainMap = new Map();
724
+ for (const row of chainResults) {
725
+ if (!chainMap.has(row.initial_id)) {
726
+ chainMap.set(row.initial_id, []);
727
+ }
728
+ chainMap.get(row.initial_id).push({ envelope: row.envelope });
729
+ }
730
+ for (const agent of agentList) {
731
+ results.push({
732
+ initial_id: agent.initial_id,
733
+ current_agent_id: agent.current_agent_id,
734
+ manifest: JSON.parse(agent.manifest),
735
+ routes: JSON.parse(agent.routes),
736
+ rotation_chain: chainMap.get(agent.initial_id) || [],
737
+ last_seen: agent.last_seen.toISOString()
738
+ });
739
+ }
740
+ }
741
+ const nextCursor = agentList.length < limit ? null :
742
+ Buffer.from(String(offset + limit)).toString('base64');
743
+ res.json({
744
+ agents: results,
745
+ next_cursor: nextCursor
746
+ });
747
+ }
748
+ finally {
749
+ connection.release();
750
+ }
751
+ }
752
+ catch (error) {
753
+ console.error('Search agents error:', error);
754
+ res.status(500).json({
755
+ error: {
756
+ code: 'INTERNAL_ERROR',
757
+ message: 'Internal server error'
758
+ }
759
+ });
760
+ }
761
+ }
762
+ validateRegistrationRequest(request) {
763
+ if (!request.agent_id) {
764
+ return 'agent_id is required';
765
+ }
766
+ if (!request.manifest) {
767
+ return 'manifest is required';
768
+ }
769
+ if (!request.routes || !Array.isArray(request.routes) || request.routes.length === 0) {
770
+ return 'routes is required and must be a non-empty array';
771
+ }
772
+ if (request.manifest.agent_id !== request.agent_id) {
773
+ return 'manifest.agent_id must match request.agent_id';
774
+ }
775
+ if (!request.manifest.protocol || request.manifest.protocol !== 'adp/0.2') {
776
+ return 'manifest.protocol must be adp/0.2';
777
+ }
778
+ // Validate agent_id format (basic check)
779
+ if (!request.agent_id.startsWith('adp://')) {
780
+ return 'agent_id must start with adp://';
781
+ }
782
+ return null;
783
+ }
784
+ start() {
785
+ this.app.listen(this.config.port, this.config.host, () => {
786
+ console.log(`Registry server started on ${this.config.host}:${this.config.port}`);
787
+ });
788
+ }
789
+ async stop() {
790
+ if (this.heartbeatDrainTimer) {
791
+ clearInterval(this.heartbeatDrainTimer);
792
+ this.heartbeatDrainTimer = null;
793
+ }
794
+ if (this.rateLimitCleanupTimer) {
795
+ clearInterval(this.rateLimitCleanupTimer);
796
+ this.rateLimitCleanupTimer = null;
797
+ }
798
+ await this.drainHeartbeats();
799
+ }
800
+ }
801
+ exports.RegistryService = RegistryService;
802
+ //# sourceMappingURL=service.js.map