society-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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/adapters.d.ts +101 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +764 -0
- package/dist/adapters.js.map +1 -0
- package/dist/agents-md.d.ts +59 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +204 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/autoconfig.d.ts +137 -0
- package/dist/autoconfig.d.ts.map +1 -0
- package/dist/autoconfig.js +452 -0
- package/dist/autoconfig.js.map +1 -0
- package/dist/bootstrap.d.ts +68 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +304 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bridges/a2a-bridge.d.ts +156 -0
- package/dist/bridges/a2a-bridge.d.ts.map +1 -0
- package/dist/bridges/a2a-bridge.js +337 -0
- package/dist/bridges/a2a-bridge.js.map +1 -0
- package/dist/bridges/mcp-bridge.d.ts +87 -0
- package/dist/bridges/mcp-bridge.d.ts.map +1 -0
- package/dist/bridges/mcp-bridge.js +332 -0
- package/dist/bridges/mcp-bridge.js.map +1 -0
- package/dist/cache.d.ts +130 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +257 -0
- package/dist/cache.js.map +1 -0
- package/dist/capsules.d.ts +23 -0
- package/dist/capsules.d.ts.map +1 -0
- package/dist/capsules.js +75 -0
- package/dist/capsules.js.map +1 -0
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +263 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/coc.d.ts +121 -0
- package/dist/coc.d.ts.map +1 -0
- package/dist/coc.js +629 -0
- package/dist/coc.js.map +1 -0
- package/dist/coc.test.d.ts +2 -0
- package/dist/coc.test.d.ts.map +1 -0
- package/dist/coc.test.js +80 -0
- package/dist/coc.test.js.map +1 -0
- package/dist/compression.d.ts +125 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +573 -0
- package/dist/compression.js.map +1 -0
- package/dist/cot-stream.d.ts +220 -0
- package/dist/cot-stream.d.ts.map +1 -0
- package/dist/cot-stream.js +673 -0
- package/dist/cot-stream.js.map +1 -0
- package/dist/crypto-wasm.d.ts +100 -0
- package/dist/crypto-wasm.d.ts.map +1 -0
- package/dist/crypto-wasm.js +229 -0
- package/dist/crypto-wasm.js.map +1 -0
- package/dist/federation.d.ts +200 -0
- package/dist/federation.d.ts.map +1 -0
- package/dist/federation.js +691 -0
- package/dist/federation.js.map +1 -0
- package/dist/federation.test.d.ts +2 -0
- package/dist/federation.test.d.ts.map +1 -0
- package/dist/federation.test.js +71 -0
- package/dist/federation.test.js.map +1 -0
- package/dist/gateway/capability-router.d.ts +77 -0
- package/dist/gateway/capability-router.d.ts.map +1 -0
- package/dist/gateway/capability-router.js +222 -0
- package/dist/gateway/capability-router.js.map +1 -0
- package/dist/gateway/demand-spawner.d.ts +155 -0
- package/dist/gateway/demand-spawner.d.ts.map +1 -0
- package/dist/gateway/demand-spawner.js +426 -0
- package/dist/gateway/demand-spawner.js.map +1 -0
- package/dist/identity.d.ts +46 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +102 -0
- package/dist/identity.js.map +1 -0
- package/dist/identity.test.d.ts +2 -0
- package/dist/identity.test.d.ts.map +1 -0
- package/dist/identity.test.js +45 -0
- package/dist/identity.test.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1572 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +210 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +1105 -0
- package/dist/integration.js.map +1 -0
- package/dist/integration.test.d.ts +2 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +155 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/knowledge.d.ts +219 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +543 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/knowledge.test.d.ts +2 -0
- package/dist/knowledge.test.d.ts.map +1 -0
- package/dist/knowledge.test.js +72 -0
- package/dist/knowledge.test.js.map +1 -0
- package/dist/latent-space.d.ts +178 -0
- package/dist/latent-space.d.ts.map +1 -0
- package/dist/latent-space.js +385 -0
- package/dist/latent-space.js.map +1 -0
- package/dist/lib.d.ts +30 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +30 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp/server.d.ts +74 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1392 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/metrics.d.ts +98 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +222 -0
- package/dist/metrics.js.map +1 -0
- package/dist/p2p.d.ts +87 -0
- package/dist/p2p.d.ts.map +1 -0
- package/dist/p2p.js +606 -0
- package/dist/p2p.js.map +1 -0
- package/dist/persona/capabilities.d.ts +17 -0
- package/dist/persona/capabilities.d.ts.map +1 -0
- package/dist/persona/capabilities.js +224 -0
- package/dist/persona/capabilities.js.map +1 -0
- package/dist/persona/domains.d.ts +22 -0
- package/dist/persona/domains.d.ts.map +1 -0
- package/dist/persona/domains.js +176 -0
- package/dist/persona/domains.js.map +1 -0
- package/dist/persona/embeddings.d.ts +40 -0
- package/dist/persona/embeddings.d.ts.map +1 -0
- package/dist/persona/embeddings.js +265 -0
- package/dist/persona/embeddings.js.map +1 -0
- package/dist/persona/engine.d.ts +79 -0
- package/dist/persona/engine.d.ts.map +1 -0
- package/dist/persona/engine.js +1087 -0
- package/dist/persona/engine.js.map +1 -0
- package/dist/persona/index.d.ts +11 -0
- package/dist/persona/index.d.ts.map +1 -0
- package/dist/persona/index.js +11 -0
- package/dist/persona/index.js.map +1 -0
- package/dist/persona/lifecycle.d.ts +17 -0
- package/dist/persona/lifecycle.d.ts.map +1 -0
- package/dist/persona/lifecycle.js +36 -0
- package/dist/persona/lifecycle.js.map +1 -0
- package/dist/persona/retrieval.d.ts +6 -0
- package/dist/persona/retrieval.d.ts.map +1 -0
- package/dist/persona/retrieval.js +122 -0
- package/dist/persona/retrieval.js.map +1 -0
- package/dist/persona/sync.d.ts +15 -0
- package/dist/persona/sync.d.ts.map +1 -0
- package/dist/persona/sync.js +92 -0
- package/dist/persona/sync.js.map +1 -0
- package/dist/persona/types.d.ts +283 -0
- package/dist/persona/types.d.ts.map +1 -0
- package/dist/persona/types.js +2 -0
- package/dist/persona/types.js.map +1 -0
- package/dist/persona/zkp/engine.d.ts +26 -0
- package/dist/persona/zkp/engine.d.ts.map +1 -0
- package/dist/persona/zkp/engine.js +370 -0
- package/dist/persona/zkp/engine.js.map +1 -0
- package/dist/persona/zkp/types.d.ts +39 -0
- package/dist/persona/zkp/types.d.ts.map +1 -0
- package/dist/persona/zkp/types.js +2 -0
- package/dist/persona/zkp/types.js.map +1 -0
- package/dist/planner.d.ts +114 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +522 -0
- package/dist/planner.js.map +1 -0
- package/dist/proactive/checkpoints.d.ts +9 -0
- package/dist/proactive/checkpoints.d.ts.map +1 -0
- package/dist/proactive/checkpoints.js +20 -0
- package/dist/proactive/checkpoints.js.map +1 -0
- package/dist/proactive/engine.d.ts +59 -0
- package/dist/proactive/engine.d.ts.map +1 -0
- package/dist/proactive/engine.js +406 -0
- package/dist/proactive/engine.js.map +1 -0
- package/dist/proactive/scheduler.d.ts +11 -0
- package/dist/proactive/scheduler.d.ts.map +1 -0
- package/dist/proactive/scheduler.js +45 -0
- package/dist/proactive/scheduler.js.map +1 -0
- package/dist/proactive/swarm-controller.d.ts +189 -0
- package/dist/proactive/swarm-controller.d.ts.map +1 -0
- package/dist/proactive/swarm-controller.js +477 -0
- package/dist/proactive/swarm-controller.js.map +1 -0
- package/dist/proactive/swarm-registry.d.ts +13 -0
- package/dist/proactive/swarm-registry.d.ts.map +1 -0
- package/dist/proactive/swarm-registry.js +122 -0
- package/dist/proactive/swarm-registry.js.map +1 -0
- package/dist/proactive/types.d.ts +145 -0
- package/dist/proactive/types.d.ts.map +1 -0
- package/dist/proactive/types.js +25 -0
- package/dist/proactive/types.js.map +1 -0
- package/dist/registry.d.ts +35 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +88 -0
- package/dist/registry.js.map +1 -0
- package/dist/reputation.d.ts +123 -0
- package/dist/reputation.d.ts.map +1 -0
- package/dist/reputation.js +366 -0
- package/dist/reputation.js.map +1 -0
- package/dist/reputation.test.d.ts +5 -0
- package/dist/reputation.test.d.ts.map +1 -0
- package/dist/reputation.test.js +265 -0
- package/dist/reputation.test.js.map +1 -0
- package/dist/rooms.d.ts +96 -0
- package/dist/rooms.d.ts.map +1 -0
- package/dist/rooms.js +410 -0
- package/dist/rooms.js.map +1 -0
- package/dist/sdk/client.d.ts +290 -0
- package/dist/sdk/client.d.ts.map +1 -0
- package/dist/sdk/client.js +1287 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/sdk/index.d.ts +32 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +70 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/security.d.ts +230 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +652 -0
- package/dist/security.js.map +1 -0
- package/dist/skills/engine.d.ts +262 -0
- package/dist/skills/engine.d.ts.map +1 -0
- package/dist/skills/engine.js +788 -0
- package/dist/skills/engine.js.map +1 -0
- package/dist/skills/engine.test.d.ts +2 -0
- package/dist/skills/engine.test.d.ts.map +1 -0
- package/dist/skills/engine.test.js +134 -0
- package/dist/skills/engine.test.js.map +1 -0
- package/dist/skills/parser.d.ts +129 -0
- package/dist/skills/parser.d.ts.map +1 -0
- package/dist/skills/parser.js +318 -0
- package/dist/skills/parser.js.map +1 -0
- package/dist/social.d.ts +149 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +401 -0
- package/dist/social.js.map +1 -0
- package/dist/storage-optimized.d.ts +116 -0
- package/dist/storage-optimized.d.ts.map +1 -0
- package/dist/storage-optimized.js +264 -0
- package/dist/storage-optimized.js.map +1 -0
- package/dist/storage.d.ts +584 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +2703 -0
- package/dist/storage.js.map +1 -0
- package/dist/storage.test.d.ts +2 -0
- package/dist/storage.test.d.ts.map +1 -0
- package/dist/storage.test.js +78 -0
- package/dist/storage.test.js.map +1 -0
- package/dist/swp.d.ts +443 -0
- package/dist/swp.d.ts.map +1 -0
- package/dist/swp.js +223 -0
- package/dist/swp.js.map +1 -0
- package/dist/swp.test.d.ts +5 -0
- package/dist/swp.test.d.ts.map +1 -0
- package/dist/swp.test.js +127 -0
- package/dist/swp.test.js.map +1 -0
- package/dist/templates.d.ts +25 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +1048 -0
- package/dist/templates.js.map +1 -0
- package/dist/test-e2e.d.ts +14 -0
- package/dist/test-e2e.d.ts.map +1 -0
- package/dist/test-e2e.js +266 -0
- package/dist/test-e2e.js.map +1 -0
- package/dist/workers/research-worker.d.ts +19 -0
- package/dist/workers/research-worker.d.ts.map +1 -0
- package/dist/workers/research-worker.js +141 -0
- package/dist/workers/research-worker.js.map +1 -0
- package/package.json +110 -0
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Society Protocol — Adapter Host v1.0
|
|
3
|
+
*
|
|
4
|
+
* HTTP bridge between Society P2P network and external AI agents.
|
|
5
|
+
* Features:
|
|
6
|
+
* - RESTful API for adapter registration
|
|
7
|
+
* - Webhook support for push notifications
|
|
8
|
+
* - Automatic step claiming based on capabilities
|
|
9
|
+
* - Health monitoring
|
|
10
|
+
* - SECURITY: API key authentication, rate limiting, CORS, input validation
|
|
11
|
+
*/
|
|
12
|
+
import express from 'express';
|
|
13
|
+
import { ulid } from 'ulid';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
class RateLimiter {
|
|
17
|
+
windowMs;
|
|
18
|
+
maxRequests;
|
|
19
|
+
cache = new Map();
|
|
20
|
+
constructor(windowMs = 15 * 60 * 1000, // 15 minutes
|
|
21
|
+
maxRequests = 100) {
|
|
22
|
+
this.windowMs = windowMs;
|
|
23
|
+
this.maxRequests = maxRequests;
|
|
24
|
+
}
|
|
25
|
+
check(identifier) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const entry = this.cache.get(identifier);
|
|
28
|
+
if (!entry || now > entry.resetTime) {
|
|
29
|
+
// New window
|
|
30
|
+
const newEntry = {
|
|
31
|
+
count: 1,
|
|
32
|
+
resetTime: now + this.windowMs
|
|
33
|
+
};
|
|
34
|
+
this.cache.set(identifier, newEntry);
|
|
35
|
+
return { allowed: true, remaining: this.maxRequests - 1, resetTime: newEntry.resetTime };
|
|
36
|
+
}
|
|
37
|
+
if (entry.count >= this.maxRequests) {
|
|
38
|
+
return { allowed: false, remaining: 0, resetTime: entry.resetTime };
|
|
39
|
+
}
|
|
40
|
+
entry.count++;
|
|
41
|
+
return { allowed: true, remaining: this.maxRequests - entry.count, resetTime: entry.resetTime };
|
|
42
|
+
}
|
|
43
|
+
cleanup() {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
for (const [key, entry] of this.cache) {
|
|
46
|
+
if (now > entry.resetTime) {
|
|
47
|
+
this.cache.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ─── Adapter Host ───────────────────────────────────────────────
|
|
53
|
+
export class AdapterHost extends EventEmitter {
|
|
54
|
+
storage;
|
|
55
|
+
coc;
|
|
56
|
+
config;
|
|
57
|
+
app = express();
|
|
58
|
+
server;
|
|
59
|
+
adapters = new Map();
|
|
60
|
+
stepClaims = new Map(); // step_id -> adapter_id
|
|
61
|
+
healthCheckInterval;
|
|
62
|
+
rateLimiter;
|
|
63
|
+
cleanupInterval;
|
|
64
|
+
constructor(storage, coc, config) {
|
|
65
|
+
super();
|
|
66
|
+
this.storage = storage;
|
|
67
|
+
this.coc = coc;
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.setupSecurity();
|
|
70
|
+
this.app.use(express.json({ limit: this.config.security?.maxBodySize || '10mb' }));
|
|
71
|
+
this.setupRoutes();
|
|
72
|
+
this.setupEventListeners();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Setup security middleware
|
|
76
|
+
*/
|
|
77
|
+
setupSecurity() {
|
|
78
|
+
const security = this.config.security || {};
|
|
79
|
+
// 1. Security Headers
|
|
80
|
+
if (security.securityHeaders !== false) {
|
|
81
|
+
this.app.use((req, res, next) => {
|
|
82
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
83
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
84
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
85
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
86
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'");
|
|
87
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
88
|
+
res.removeHeader('X-Powered-By');
|
|
89
|
+
next();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// 2. CORS - Restrictive by default
|
|
93
|
+
this.app.use((req, res, next) => {
|
|
94
|
+
const origin = req.headers.origin;
|
|
95
|
+
const host = this.config.host || '127.0.0.1';
|
|
96
|
+
// Always allow localhost
|
|
97
|
+
const isLocalhost = !origin ||
|
|
98
|
+
origin.startsWith('http://localhost') ||
|
|
99
|
+
origin.startsWith('http://127.0.0.1');
|
|
100
|
+
if (isLocalhost) {
|
|
101
|
+
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
102
|
+
}
|
|
103
|
+
else if (security.allowedOrigins?.includes(origin || '')) {
|
|
104
|
+
res.setHeader('Access-Control-Allow-Origin', origin || '');
|
|
105
|
+
}
|
|
106
|
+
// Otherwise, no CORS headers (browser will block)
|
|
107
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
108
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
|
109
|
+
if (req.method === 'OPTIONS') {
|
|
110
|
+
res.sendStatus(204);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
next();
|
|
114
|
+
});
|
|
115
|
+
// 3. Rate Limiting
|
|
116
|
+
if (security.rateLimitEnabled !== false) {
|
|
117
|
+
this.rateLimiter = new RateLimiter(security.rateLimitWindowMs || 15 * 60 * 1000, security.rateLimitMaxRequests || 100);
|
|
118
|
+
this.app.use((req, res, next) => {
|
|
119
|
+
const identifier = this.getClientIdentifier(req);
|
|
120
|
+
const result = this.rateLimiter.check(identifier);
|
|
121
|
+
// Add rate limit headers
|
|
122
|
+
res.setHeader('X-RateLimit-Limit', security.rateLimitMaxRequests || 100);
|
|
123
|
+
res.setHeader('X-RateLimit-Remaining', result.remaining);
|
|
124
|
+
res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000));
|
|
125
|
+
if (!result.allowed) {
|
|
126
|
+
res.status(429).json({
|
|
127
|
+
error: 'Too many requests',
|
|
128
|
+
retry_after: Math.ceil((result.resetTime - Date.now()) / 1000)
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
next();
|
|
133
|
+
});
|
|
134
|
+
// Cleanup old entries every 5 minutes
|
|
135
|
+
this.cleanupInterval = setInterval(() => {
|
|
136
|
+
this.rateLimiter?.cleanup();
|
|
137
|
+
}, 5 * 60 * 1000);
|
|
138
|
+
}
|
|
139
|
+
// 4. API Key Authentication (if configured)
|
|
140
|
+
if (security.apiKey) {
|
|
141
|
+
const headerName = security.apiKeyHeader || 'x-api-key';
|
|
142
|
+
this.app.use((req, res, next) => {
|
|
143
|
+
// Skip auth for health endpoint
|
|
144
|
+
if (req.path === '/health') {
|
|
145
|
+
next();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const providedKey = req.headers[headerName.toLowerCase()];
|
|
149
|
+
if (!providedKey) {
|
|
150
|
+
res.status(401).json({
|
|
151
|
+
error: 'Authentication required',
|
|
152
|
+
message: `Provide API key via ${headerName} header`
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Constant-time comparison to prevent timing attacks
|
|
157
|
+
if (!this.constantTimeCompare(providedKey, security.apiKey)) {
|
|
158
|
+
res.status(403).json({ error: 'Invalid API key' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
next();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 5. Localhost-only enforcement (default security)
|
|
165
|
+
this.app.use((req, res, next) => {
|
|
166
|
+
const host = this.config.host || '127.0.0.1';
|
|
167
|
+
// If configured for localhost only, enforce it
|
|
168
|
+
if (host === '127.0.0.1' || host === 'localhost') {
|
|
169
|
+
const clientIp = this.getClientIp(req);
|
|
170
|
+
const isLocal = clientIp === '127.0.0.1' ||
|
|
171
|
+
clientIp === '::1' ||
|
|
172
|
+
clientIp === '::ffff:127.0.0.1';
|
|
173
|
+
if (!isLocal && !security.allowedOrigins) {
|
|
174
|
+
res.status(403).json({
|
|
175
|
+
error: 'Access denied',
|
|
176
|
+
message: 'This API is restricted to localhost. Configure allowedOrigins to enable remote access.'
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
next();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get client IP address
|
|
186
|
+
*/
|
|
187
|
+
getClientIp(req) {
|
|
188
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
189
|
+
if (forwarded && this.config.security?.trustProxy) {
|
|
190
|
+
return forwarded.split(',')[0].trim();
|
|
191
|
+
}
|
|
192
|
+
return req.socket.remoteAddress || 'unknown';
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get client identifier for rate limiting
|
|
196
|
+
*/
|
|
197
|
+
getClientIdentifier(req) {
|
|
198
|
+
const apiKey = req.headers['x-api-key'];
|
|
199
|
+
if (apiKey) {
|
|
200
|
+
// Hash API key for privacy
|
|
201
|
+
return createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
|
|
202
|
+
}
|
|
203
|
+
return this.getClientIp(req);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Constant-time string comparison to prevent timing attacks
|
|
207
|
+
*/
|
|
208
|
+
constantTimeCompare(a, b) {
|
|
209
|
+
if (a.length !== b.length)
|
|
210
|
+
return false;
|
|
211
|
+
let result = 0;
|
|
212
|
+
for (let i = 0; i < a.length; i++) {
|
|
213
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
214
|
+
}
|
|
215
|
+
return result === 0;
|
|
216
|
+
}
|
|
217
|
+
start() {
|
|
218
|
+
const host = this.config.host || '127.0.0.1';
|
|
219
|
+
this.server = this.app.listen(this.config.port, host, () => {
|
|
220
|
+
console.log(`[adapters] Host API listening on http://${host}:${this.config.port}`);
|
|
221
|
+
if (this.config.security?.apiKey) {
|
|
222
|
+
console.log(`[adapters] API key authentication enabled`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log(`[adapters] WARNING: No API key configured - restrict to localhost only!`);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// Start health check loop
|
|
229
|
+
this.healthCheckInterval = setInterval(() => {
|
|
230
|
+
this.runHealthChecks();
|
|
231
|
+
}, 30000);
|
|
232
|
+
}
|
|
233
|
+
stop() {
|
|
234
|
+
if (this.healthCheckInterval) {
|
|
235
|
+
clearInterval(this.healthCheckInterval);
|
|
236
|
+
}
|
|
237
|
+
if (this.cleanupInterval) {
|
|
238
|
+
clearInterval(this.cleanupInterval);
|
|
239
|
+
}
|
|
240
|
+
if (this.server) {
|
|
241
|
+
this.server.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ─── Input Sanitization ───────────────────────────────────────
|
|
245
|
+
getParam(param) {
|
|
246
|
+
if (Array.isArray(param))
|
|
247
|
+
return param[0] || '';
|
|
248
|
+
return param || '';
|
|
249
|
+
}
|
|
250
|
+
sanitizeString(input, maxLength) {
|
|
251
|
+
if (typeof input !== 'string')
|
|
252
|
+
return '';
|
|
253
|
+
// Remove control characters and limit length
|
|
254
|
+
return input
|
|
255
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars
|
|
256
|
+
.replace(/[<>]/g, '') // Basic XSS prevention
|
|
257
|
+
.trim()
|
|
258
|
+
.slice(0, maxLength);
|
|
259
|
+
}
|
|
260
|
+
sanitizeUrl(input) {
|
|
261
|
+
if (typeof input !== 'string')
|
|
262
|
+
return undefined;
|
|
263
|
+
try {
|
|
264
|
+
const url = new URL(input);
|
|
265
|
+
// Only allow http/https
|
|
266
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
// SSRF protection: block private/internal IP ranges
|
|
270
|
+
if (this.isPrivateHost(url.hostname)) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
return url.toString();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
isPrivateHost(hostname) {
|
|
280
|
+
// Block localhost variants
|
|
281
|
+
if (['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
// Block private IPv4 ranges (RFC 1918) and link-local
|
|
285
|
+
const privateRanges = [
|
|
286
|
+
/^10\./, // 10.0.0.0/8
|
|
287
|
+
/^172\.(1[6-9]|2\d|3[01])\./, // 172.16.0.0/12
|
|
288
|
+
/^192\.168\./, // 192.168.0.0/16
|
|
289
|
+
/^169\.254\./, // 169.254.0.0/16 (link-local)
|
|
290
|
+
/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
|
|
291
|
+
/^0\./, // 0.0.0.0/8
|
|
292
|
+
/^::ffff:127\./, // IPv4-mapped localhost
|
|
293
|
+
/^fc/i, // IPv6 ULA fc00::/7
|
|
294
|
+
/^fd/i, // IPv6 ULA
|
|
295
|
+
/^fe80/i, // IPv6 link-local
|
|
296
|
+
];
|
|
297
|
+
return privateRanges.some(re => re.test(hostname));
|
|
298
|
+
}
|
|
299
|
+
// ─── Routes ───────────────────────────────────────────────────
|
|
300
|
+
setupRoutes() {
|
|
301
|
+
// Health check
|
|
302
|
+
this.app.get('/health', (req, res) => {
|
|
303
|
+
res.json({
|
|
304
|
+
status: 'ok',
|
|
305
|
+
version: 'society-1.0.0',
|
|
306
|
+
adapters: this.adapters.size,
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
// Register adapter
|
|
311
|
+
this.app.post('/adapters/register', (req, res) => {
|
|
312
|
+
this.handleRegister(req, res);
|
|
313
|
+
});
|
|
314
|
+
// Get adapter info
|
|
315
|
+
this.app.get('/adapters/:adapter_id', (req, res) => {
|
|
316
|
+
this.handleGetAdapter(req, res);
|
|
317
|
+
});
|
|
318
|
+
// Update adapter capabilities
|
|
319
|
+
this.app.put('/adapters/:adapter_id/capabilities', (req, res) => {
|
|
320
|
+
this.handleUpdateCapabilities(req, res);
|
|
321
|
+
});
|
|
322
|
+
// Send heartbeat
|
|
323
|
+
this.app.post('/adapters/:adapter_id/heartbeat', (req, res) => {
|
|
324
|
+
this.handleHeartbeat(req, res);
|
|
325
|
+
});
|
|
326
|
+
// Poll for pending steps
|
|
327
|
+
this.app.get('/adapters/:adapter_id/steps/pending', (req, res) => {
|
|
328
|
+
this.handlePollPending(req, res);
|
|
329
|
+
});
|
|
330
|
+
// Claim a step (attempt to assign)
|
|
331
|
+
this.app.post('/adapters/:adapter_id/steps/:step_id/claim', (req, res) => {
|
|
332
|
+
this.handleClaimStep(req, res);
|
|
333
|
+
});
|
|
334
|
+
// Submit step result
|
|
335
|
+
this.app.post('/adapters/:adapter_id/steps/:step_id/submit', (req, res) => {
|
|
336
|
+
this.handleSubmitStep(req, res);
|
|
337
|
+
});
|
|
338
|
+
// Get step details
|
|
339
|
+
this.app.get('/steps/:step_id', (req, res) => {
|
|
340
|
+
this.handleGetStep(req, res);
|
|
341
|
+
});
|
|
342
|
+
// List available adapters
|
|
343
|
+
this.app.get('/adapters', (req, res) => {
|
|
344
|
+
this.handleListAdapters(req, res);
|
|
345
|
+
});
|
|
346
|
+
// Metrics endpoint
|
|
347
|
+
this.app.get('/metrics', (req, res) => {
|
|
348
|
+
this.handleMetrics(req, res);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// ─── Route Handlers ───────────────────────────────────────────
|
|
352
|
+
handleRegister(req, res) {
|
|
353
|
+
const profile = req.body;
|
|
354
|
+
// Validate required fields
|
|
355
|
+
if (!profile.display_name || !profile.kinds || profile.kinds.length === 0) {
|
|
356
|
+
res.status(400).json({
|
|
357
|
+
error: 'Missing required fields: display_name, kinds',
|
|
358
|
+
required: ['display_name', 'kinds', 'runtime'],
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Input sanitization
|
|
363
|
+
const sanitizedProfile = {
|
|
364
|
+
...profile,
|
|
365
|
+
display_name: this.sanitizeString(profile.display_name, 100),
|
|
366
|
+
description: profile.description ? this.sanitizeString(profile.description, 500) : undefined,
|
|
367
|
+
endpoint: profile.endpoint ? (this.sanitizeUrl(profile.endpoint) || '') : '',
|
|
368
|
+
kinds: profile.kinds.map(k => this.sanitizeString(k, 50)).filter(k => k),
|
|
369
|
+
specialties: profile.specialties?.map(s => this.sanitizeString(s, 50)).filter(s => s) || [],
|
|
370
|
+
};
|
|
371
|
+
// Validate kinds
|
|
372
|
+
const validKinds = ['task', 'review', 'merge', 'decision', 'synthesis', 'verification'];
|
|
373
|
+
const invalidKinds = sanitizedProfile.kinds.filter(k => !validKinds.includes(k));
|
|
374
|
+
if (invalidKinds.length > 0) {
|
|
375
|
+
res.status(400).json({
|
|
376
|
+
error: `Invalid kinds: ${invalidKinds.join(', ')}`,
|
|
377
|
+
valid_kinds: validKinds,
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Validate runtime
|
|
382
|
+
const validRuntimes = ['claude-code', 'nanobot', 'ollama', 'openai', 'custom', 'docker'];
|
|
383
|
+
if (!validRuntimes.includes(sanitizedProfile.runtime)) {
|
|
384
|
+
res.status(400).json({
|
|
385
|
+
error: `Invalid runtime: ${sanitizedProfile.runtime}`,
|
|
386
|
+
valid_runtimes: validRuntimes,
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const adapterId = `adapter_${ulid()}`;
|
|
391
|
+
const registration = {
|
|
392
|
+
adapter_id: adapterId,
|
|
393
|
+
profile: { ...sanitizedProfile, adapter_id: adapterId },
|
|
394
|
+
registered_at: Date.now(),
|
|
395
|
+
last_heartbeat: Date.now(),
|
|
396
|
+
active_tasks: 0,
|
|
397
|
+
total_tasks_completed: 0,
|
|
398
|
+
health: 'healthy',
|
|
399
|
+
};
|
|
400
|
+
this.adapters.set(adapterId, registration);
|
|
401
|
+
// Store in persistent storage
|
|
402
|
+
this.storage.registerAdapter(adapterId, profile.runtime, profile.display_name, profile.specialties || [], profile.kinds, profile.max_concurrency || 1, profile.endpoint || '', profile.auth_type || 'none', {
|
|
403
|
+
ownerDid: profile.owner_did,
|
|
404
|
+
roomId: profile.room_id,
|
|
405
|
+
missionTags: profile.mission_tags,
|
|
406
|
+
health: 'healthy',
|
|
407
|
+
hostId: profile.host_id,
|
|
408
|
+
peerId: profile.peer_id,
|
|
409
|
+
});
|
|
410
|
+
this.emit('adapter:registered', adapterId, profile);
|
|
411
|
+
res.status(201).json({
|
|
412
|
+
adapter_id: adapterId,
|
|
413
|
+
status: 'registered',
|
|
414
|
+
message: 'Adapter registered successfully',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
handleGetAdapter(req, res) {
|
|
418
|
+
const adapter_id = this.getParam(req.params.adapter_id);
|
|
419
|
+
const adapter = this.adapters.get(adapter_id);
|
|
420
|
+
if (!adapter) {
|
|
421
|
+
res.status(404).json({ error: 'Adapter not found' });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
res.json({
|
|
425
|
+
...adapter,
|
|
426
|
+
uptime_ms: Date.now() - adapter.registered_at,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
handleUpdateCapabilities(req, res) {
|
|
430
|
+
const adapter_id = this.getParam(req.params.adapter_id);
|
|
431
|
+
const capabilities = req.body;
|
|
432
|
+
const adapter = this.adapters.get(adapter_id);
|
|
433
|
+
if (!adapter) {
|
|
434
|
+
res.status(404).json({ error: 'Adapter not found' });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// Update capabilities
|
|
438
|
+
adapter.profile = {
|
|
439
|
+
...adapter.profile,
|
|
440
|
+
...capabilities,
|
|
441
|
+
};
|
|
442
|
+
res.json({ status: 'updated', adapter_id });
|
|
443
|
+
}
|
|
444
|
+
handleHeartbeat(req, res) {
|
|
445
|
+
const adapter_id = this.getParam(req.params.adapter_id);
|
|
446
|
+
const { active_tasks, queue_depth, metrics } = req.body;
|
|
447
|
+
const adapter = this.adapters.get(adapter_id);
|
|
448
|
+
if (!adapter) {
|
|
449
|
+
res.status(404).json({ error: 'Adapter not found' });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
adapter.last_heartbeat = Date.now();
|
|
453
|
+
if (active_tasks !== undefined) {
|
|
454
|
+
adapter.active_tasks = active_tasks;
|
|
455
|
+
}
|
|
456
|
+
// Update health status
|
|
457
|
+
if (queue_depth > 10 || (metrics?.success_rate !== undefined && metrics.success_rate < 0.5)) {
|
|
458
|
+
adapter.health = 'degraded';
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
adapter.health = 'healthy';
|
|
462
|
+
}
|
|
463
|
+
this.storage.updateAdapterHeartbeat(adapter_id, adapter.health, queue_depth || 0, metrics?.success_rate);
|
|
464
|
+
res.json({ status: 'ok', timestamp: Date.now() });
|
|
465
|
+
}
|
|
466
|
+
handlePollPending(req, res) {
|
|
467
|
+
const adapter_id = this.getParam(req.params.adapter_id);
|
|
468
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
469
|
+
const adapter = this.adapters.get(adapter_id);
|
|
470
|
+
if (!adapter) {
|
|
471
|
+
res.status(404).json({ error: 'Adapter not found' });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// Find steps that match adapter capabilities
|
|
475
|
+
const steps = this.findMatchingSteps(adapter, limit);
|
|
476
|
+
res.json({
|
|
477
|
+
steps: steps.map(s => ({
|
|
478
|
+
step_id: s.step_id,
|
|
479
|
+
chain_id: s.chain_id,
|
|
480
|
+
kind: s.kind,
|
|
481
|
+
title: s.title,
|
|
482
|
+
description: s.description,
|
|
483
|
+
requirements: s.requirements,
|
|
484
|
+
timeout_ms: s.timeout_ms,
|
|
485
|
+
})),
|
|
486
|
+
adapter: {
|
|
487
|
+
id: adapter_id,
|
|
488
|
+
active_tasks: adapter.active_tasks,
|
|
489
|
+
health: adapter.health,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
handleClaimStep(req, res) {
|
|
494
|
+
const adapter_id = this.getParam(req.params.adapter_id);
|
|
495
|
+
const step_id = this.getParam(req.params.step_id);
|
|
496
|
+
const { lease_ms } = req.body;
|
|
497
|
+
const adapter = this.adapters.get(adapter_id);
|
|
498
|
+
if (!adapter) {
|
|
499
|
+
res.status(404).json({ error: 'Adapter not found' });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
// Check if step is already claimed
|
|
503
|
+
if (this.stepClaims.has(step_id)) {
|
|
504
|
+
res.status(409).json({
|
|
505
|
+
error: 'Step already claimed',
|
|
506
|
+
claimed_by: this.stepClaims.get(step_id),
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Get step info
|
|
511
|
+
const step = this.storage.db
|
|
512
|
+
.prepare('SELECT * FROM coc_steps WHERE step_id = ? AND status = ?')
|
|
513
|
+
.get(step_id, 'proposed');
|
|
514
|
+
if (!step) {
|
|
515
|
+
res.status(404).json({ error: 'Step not found or not available' });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Check if adapter can handle this step kind
|
|
519
|
+
if (!adapter.profile.kinds.includes(step.kind)) {
|
|
520
|
+
res.status(400).json({
|
|
521
|
+
error: 'Adapter cannot handle this step kind',
|
|
522
|
+
step_kind: step.kind,
|
|
523
|
+
adapter_kinds: adapter.profile.kinds,
|
|
524
|
+
});
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// Claim the step
|
|
528
|
+
this.stepClaims.set(step_id, adapter_id);
|
|
529
|
+
adapter.active_tasks++;
|
|
530
|
+
// Emit to CoC engine for assignment
|
|
531
|
+
this.coc.emit('adapter:lease_request', {
|
|
532
|
+
chain_id: step.chain_id,
|
|
533
|
+
step_id,
|
|
534
|
+
adapter_id,
|
|
535
|
+
worker_did: adapter.profile.owner_did || adapter_id,
|
|
536
|
+
lease_ms: lease_ms || 120000,
|
|
537
|
+
});
|
|
538
|
+
res.json({
|
|
539
|
+
status: 'claimed',
|
|
540
|
+
step_id,
|
|
541
|
+
chain_id: step.chain_id,
|
|
542
|
+
lease_expires_at: Date.now() + (lease_ms || 120000),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
handleSubmitStep(req, res) {
|
|
546
|
+
const adapter_id = this.getParam(req.params.adapter_id);
|
|
547
|
+
const step_id = this.getParam(req.params.step_id);
|
|
548
|
+
const submission = req.body;
|
|
549
|
+
const adapter = this.adapters.get(adapter_id);
|
|
550
|
+
if (!adapter) {
|
|
551
|
+
res.status(404).json({ error: 'Adapter not found' });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Verify adapter owns this step
|
|
555
|
+
if (this.stepClaims.get(step_id) !== adapter_id) {
|
|
556
|
+
res.status(403).json({ error: 'Step not claimed by this adapter' });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Get step info
|
|
560
|
+
const step = this.storage.db
|
|
561
|
+
.prepare('SELECT * FROM coc_steps WHERE step_id = ?')
|
|
562
|
+
.get(step_id);
|
|
563
|
+
if (!step) {
|
|
564
|
+
res.status(404).json({ error: 'Step not found' });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Release claim
|
|
568
|
+
this.stepClaims.delete(step_id);
|
|
569
|
+
adapter.active_tasks = Math.max(0, adapter.active_tasks - 1);
|
|
570
|
+
if (submission.status === 'completed') {
|
|
571
|
+
adapter.total_tasks_completed++;
|
|
572
|
+
}
|
|
573
|
+
// Emit to CoC engine
|
|
574
|
+
this.coc.emit('adapter:submit_request', {
|
|
575
|
+
chain_id: step.chain_id,
|
|
576
|
+
step_id,
|
|
577
|
+
assignee_did: adapter.profile.owner_did || adapter_id,
|
|
578
|
+
adapter_id,
|
|
579
|
+
status: submission.status,
|
|
580
|
+
memo: submission.memo,
|
|
581
|
+
artifacts: submission.artifacts,
|
|
582
|
+
metrics: submission.metrics,
|
|
583
|
+
});
|
|
584
|
+
res.json({
|
|
585
|
+
status: 'submitted',
|
|
586
|
+
step_id,
|
|
587
|
+
chain_id: step.chain_id,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
handleGetStep(req, res) {
|
|
591
|
+
const step_id = this.getParam(req.params.step_id);
|
|
592
|
+
const step = this.storage.db
|
|
593
|
+
.prepare('SELECT * FROM coc_steps WHERE step_id = ?')
|
|
594
|
+
.get(step_id);
|
|
595
|
+
if (!step) {
|
|
596
|
+
res.status(404).json({ error: 'Step not found' });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
res.json({
|
|
600
|
+
step_id: step.step_id,
|
|
601
|
+
chain_id: step.chain_id,
|
|
602
|
+
kind: step.kind,
|
|
603
|
+
title: step.title,
|
|
604
|
+
description: step.description,
|
|
605
|
+
status: step.status,
|
|
606
|
+
assignee_did: step.assignee_did,
|
|
607
|
+
depends_on: JSON.parse(step.depends_on || '[]'),
|
|
608
|
+
requirements: step.requirements_json ? JSON.parse(step.requirements_json) : undefined,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
handleListAdapters(req, res) {
|
|
612
|
+
const filterKind = req.query.kind;
|
|
613
|
+
let adapters = Array.from(this.adapters.values());
|
|
614
|
+
if (filterKind) {
|
|
615
|
+
adapters = adapters.filter(a => a.profile.kinds.includes(filterKind));
|
|
616
|
+
}
|
|
617
|
+
res.json({
|
|
618
|
+
adapters: adapters.map(a => ({
|
|
619
|
+
adapter_id: a.adapter_id,
|
|
620
|
+
display_name: a.profile.display_name,
|
|
621
|
+
runtime: a.profile.runtime,
|
|
622
|
+
kinds: a.profile.kinds,
|
|
623
|
+
specialties: a.profile.specialties,
|
|
624
|
+
active_tasks: a.active_tasks,
|
|
625
|
+
health: a.health,
|
|
626
|
+
last_heartbeat: a.last_heartbeat,
|
|
627
|
+
})),
|
|
628
|
+
total: adapters.length,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
handleMetrics(req, res) {
|
|
632
|
+
const stats = {
|
|
633
|
+
total_adapters: this.adapters.size,
|
|
634
|
+
healthy_adapters: 0,
|
|
635
|
+
degraded_adapters: 0,
|
|
636
|
+
total_active_tasks: 0,
|
|
637
|
+
total_completed_tasks: 0,
|
|
638
|
+
steps_claimed: this.stepClaims.size,
|
|
639
|
+
};
|
|
640
|
+
for (const adapter of this.adapters.values()) {
|
|
641
|
+
if (adapter.health === 'healthy')
|
|
642
|
+
stats.healthy_adapters++;
|
|
643
|
+
else
|
|
644
|
+
stats.degraded_adapters++;
|
|
645
|
+
stats.total_active_tasks += adapter.active_tasks;
|
|
646
|
+
stats.total_completed_tasks += adapter.total_tasks_completed;
|
|
647
|
+
}
|
|
648
|
+
res.json(stats);
|
|
649
|
+
}
|
|
650
|
+
// ─── Helpers ──────────────────────────────────────────────────
|
|
651
|
+
setupEventListeners() {
|
|
652
|
+
// Listen for step unlocks to notify matching adapters
|
|
653
|
+
this.coc.on('step:unlocked', (chainId, stepId, step) => {
|
|
654
|
+
this.notifyMatchingAdapters(step);
|
|
655
|
+
});
|
|
656
|
+
// Clean up claims when steps complete or fail
|
|
657
|
+
this.coc.on('step:submitted', (chainId, stepId) => {
|
|
658
|
+
const adapterId = this.stepClaims.get(stepId);
|
|
659
|
+
if (adapterId) {
|
|
660
|
+
const adapter = this.adapters.get(adapterId);
|
|
661
|
+
if (adapter) {
|
|
662
|
+
adapter.active_tasks = Math.max(0, adapter.active_tasks - 1);
|
|
663
|
+
}
|
|
664
|
+
this.stepClaims.delete(stepId);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
findMatchingSteps(adapter, limit) {
|
|
669
|
+
// Query for proposed steps matching adapter capabilities
|
|
670
|
+
const kinds = adapter.profile.kinds;
|
|
671
|
+
const specialties = adapter.profile.specialties || [];
|
|
672
|
+
// Build query to find matching steps
|
|
673
|
+
const placeholders = kinds.map(() => '?').join(',');
|
|
674
|
+
const query = `
|
|
675
|
+
SELECT * FROM coc_steps
|
|
676
|
+
WHERE status = 'proposed'
|
|
677
|
+
AND kind IN (${placeholders})
|
|
678
|
+
ORDER BY created_at DESC
|
|
679
|
+
LIMIT ?
|
|
680
|
+
`;
|
|
681
|
+
return this.storage.db.prepare(query).all(...kinds, limit);
|
|
682
|
+
}
|
|
683
|
+
notifyMatchingAdapters(step) {
|
|
684
|
+
// Find adapters that can handle this step
|
|
685
|
+
for (const [adapterId, adapter] of this.adapters) {
|
|
686
|
+
if (adapter.profile.kinds.includes(step.kind)) {
|
|
687
|
+
// Check specialties match if required
|
|
688
|
+
if (step.requirements?.capabilities) {
|
|
689
|
+
const hasCapability = step.requirements.capabilities.some((cap) => adapter.profile.specialties?.includes(cap));
|
|
690
|
+
if (!hasCapability)
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
// Send webhook notification if endpoint configured
|
|
694
|
+
if (adapter.profile.endpoint) {
|
|
695
|
+
this.sendWebhookNotification(adapter, step);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async sendWebhookNotification(adapter, step) {
|
|
701
|
+
try {
|
|
702
|
+
await fetch(adapter.profile.endpoint, {
|
|
703
|
+
method: 'POST',
|
|
704
|
+
headers: {
|
|
705
|
+
'Content-Type': 'application/json',
|
|
706
|
+
...(this.config.webhookSecret && {
|
|
707
|
+
'X-Webhook-Secret': this.config.webhookSecret,
|
|
708
|
+
}),
|
|
709
|
+
},
|
|
710
|
+
body: JSON.stringify({
|
|
711
|
+
type: 'step_available',
|
|
712
|
+
step: {
|
|
713
|
+
step_id: step.step_id,
|
|
714
|
+
chain_id: step.chain_id,
|
|
715
|
+
kind: step.kind,
|
|
716
|
+
title: step.title,
|
|
717
|
+
description: step.description,
|
|
718
|
+
},
|
|
719
|
+
timestamp: Date.now(),
|
|
720
|
+
}),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
// Webhook failed, ignore
|
|
725
|
+
if (process.env.SOCIETY_DEBUG) {
|
|
726
|
+
console.debug(`[adapters] Webhook failed for ${adapter.adapter_id}:`, err);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
runHealthChecks() {
|
|
731
|
+
const now = Date.now();
|
|
732
|
+
const timeout = 2 * 60 * 1000; // 2 minutes
|
|
733
|
+
for (const [adapterId, adapter] of this.adapters) {
|
|
734
|
+
if (now - adapter.last_heartbeat > timeout) {
|
|
735
|
+
adapter.health = 'unhealthy';
|
|
736
|
+
// Release any claimed steps
|
|
737
|
+
for (const [stepId, claimedAdapter] of this.stepClaims) {
|
|
738
|
+
if (claimedAdapter === adapterId) {
|
|
739
|
+
this.stepClaims.delete(stepId);
|
|
740
|
+
adapter.active_tasks = Math.max(0, adapter.active_tasks - 1);
|
|
741
|
+
// Emit step expiry
|
|
742
|
+
this.coc.emit('step:expired', null, stepId, adapterId);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// ─── Public API ───────────────────────────────────────────────
|
|
749
|
+
getAdapter(adapterId) {
|
|
750
|
+
return this.adapters.get(adapterId);
|
|
751
|
+
}
|
|
752
|
+
getActiveAdapters() {
|
|
753
|
+
return Array.from(this.adapters.values()).filter(a => a.health === 'healthy' && a.active_tasks < (a.profile.max_concurrency || 1));
|
|
754
|
+
}
|
|
755
|
+
getStats() {
|
|
756
|
+
return {
|
|
757
|
+
totalAdapters: this.adapters.size,
|
|
758
|
+
healthyAdapters: Array.from(this.adapters.values()).filter(a => a.health === 'healthy').length,
|
|
759
|
+
totalActiveTasks: Array.from(this.adapters.values()).reduce((sum, a) => sum + a.active_tasks, 0),
|
|
760
|
+
stepsClaimed: this.stepClaims.size,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
//# sourceMappingURL=adapters.js.map
|