rlhf-feedback-loop 0.6.8 → 0.6.10
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/README.md +45 -31
- package/adapters/chatgpt/openapi.yaml +124 -2
- package/adapters/mcp/server-stdio.js +84 -25
- package/bin/cli.js +34 -3
- package/openapi/openapi.yaml +124 -2
- package/package.json +14 -9
- package/scripts/billing.js +349 -89
- package/scripts/prove-adapters.js +135 -5
- package/scripts/prove-subway-upgrades.js +28 -1
- package/src/api/server.js +58 -24
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
5
6
|
const { startServer } = require('../src/api/server');
|
|
6
7
|
const { handleRequest } = require('../adapters/mcp/server-stdio');
|
|
7
8
|
const { validateSubagentProfiles, listSubagentProfiles } = require('./subagent-profiles');
|
|
@@ -26,6 +27,114 @@ function escapeRegExp(value) {
|
|
|
26
27
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function parseLeadingJson(text) {
|
|
31
|
+
const raw = String(text || '');
|
|
32
|
+
const marker = '\n\n---';
|
|
33
|
+
const boundary = raw.indexOf(marker);
|
|
34
|
+
const jsonSegment = boundary === -1 ? raw : raw.slice(0, boundary);
|
|
35
|
+
return JSON.parse(jsonSegment.trim());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function proveMcpStdioTransport({
|
|
39
|
+
root,
|
|
40
|
+
transport = 'ndjson',
|
|
41
|
+
timeoutMs = 5000,
|
|
42
|
+
}) {
|
|
43
|
+
const serverPath = path.join(root, 'adapters', 'mcp', 'server-stdio.js');
|
|
44
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
45
|
+
cwd: root,
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
env: process.env,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
51
|
+
let stderrBuffer = '';
|
|
52
|
+
|
|
53
|
+
function parseResponse() {
|
|
54
|
+
const headerEnd = stdoutBuffer.indexOf('\r\n\r\n');
|
|
55
|
+
if (headerEnd !== -1) {
|
|
56
|
+
const header = stdoutBuffer.slice(0, headerEnd).toString('utf8');
|
|
57
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const length = Number(match[1]);
|
|
60
|
+
const bodyStart = headerEnd + 4;
|
|
61
|
+
const bodyEnd = bodyStart + length;
|
|
62
|
+
if (stdoutBuffer.length < bodyEnd) return null;
|
|
63
|
+
return stdoutBuffer.slice(bodyStart, bodyEnd).toString('utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const newlineIndex = stdoutBuffer.indexOf('\n');
|
|
67
|
+
if (newlineIndex === -1) return null;
|
|
68
|
+
const line = stdoutBuffer.slice(0, newlineIndex).toString('utf8').trim();
|
|
69
|
+
if (!line) return null;
|
|
70
|
+
return line;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
let settled = false;
|
|
75
|
+
const done = (err, value) => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
try {
|
|
79
|
+
child.kill('SIGKILL');
|
|
80
|
+
} catch (_) {
|
|
81
|
+
// no-op
|
|
82
|
+
}
|
|
83
|
+
if (err) reject(err);
|
|
84
|
+
else resolve(value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
done(new Error(`stdio ${transport} initialize timeout; stderr=${stderrBuffer}`));
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
|
|
91
|
+
child.on('error', (err) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
done(err);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
child.stderr.on('data', (chunk) => {
|
|
97
|
+
stderrBuffer += String(chunk || '');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
child.stdout.on('data', (chunk) => {
|
|
101
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, Buffer.from(chunk)]);
|
|
102
|
+
const body = parseResponse();
|
|
103
|
+
if (!body) return;
|
|
104
|
+
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(body);
|
|
108
|
+
done(null, parsed);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
done(err);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const initialize = {
|
|
115
|
+
jsonrpc: '2.0',
|
|
116
|
+
id: 777,
|
|
117
|
+
method: 'initialize',
|
|
118
|
+
params: {
|
|
119
|
+
protocolVersion: '2025-06-18',
|
|
120
|
+
capabilities: {},
|
|
121
|
+
clientInfo: {
|
|
122
|
+
name: 'prove-adapters',
|
|
123
|
+
version: '1.0.0',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (transport === 'framed') {
|
|
129
|
+
const body = JSON.stringify(initialize);
|
|
130
|
+
child.stdin.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
child.stdin.write(`${JSON.stringify(initialize)}\n`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
29
138
|
async function runProof(options = {}) {
|
|
30
139
|
const proofDir = options.proofDir || process.env.RLHF_PROOF_DIR || DEFAULT_PROOF_DIR;
|
|
31
140
|
const writeArtifacts = options.writeArtifacts !== false;
|
|
@@ -36,6 +145,9 @@ async function runProof(options = {}) {
|
|
|
36
145
|
}
|
|
37
146
|
|
|
38
147
|
const tmpFeedbackDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rlhf-proof-'));
|
|
148
|
+
const previousFeedbackDir = process.env.RLHF_FEEDBACK_DIR;
|
|
149
|
+
const previousApiKey = process.env.RLHF_API_KEY;
|
|
150
|
+
const previousMcpProfile = process.env.RLHF_MCP_PROFILE;
|
|
39
151
|
process.env.RLHF_FEEDBACK_DIR = tmpFeedbackDir;
|
|
40
152
|
process.env.RLHF_API_KEY = 'proof-key';
|
|
41
153
|
process.env.RLHF_MCP_PROFILE = 'default';
|
|
@@ -188,6 +300,20 @@ async function runProof(options = {}) {
|
|
|
188
300
|
addResult('mcp.initialize', true, { server: init.serverInfo.name });
|
|
189
301
|
}
|
|
190
302
|
|
|
303
|
+
{
|
|
304
|
+
const framedResponse = await proveMcpStdioTransport({ root: ROOT, transport: 'framed' });
|
|
305
|
+
check(framedResponse.id === 777, 'stdio framed initialize returned wrong id');
|
|
306
|
+
check(Boolean(framedResponse.result && framedResponse.result.serverInfo), 'stdio framed initialize missing serverInfo');
|
|
307
|
+
addResult('mcp.stdio.framed.initialize', true, { server: framedResponse.result.serverInfo.name });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
{
|
|
311
|
+
const ndjsonResponse = await proveMcpStdioTransport({ root: ROOT, transport: 'ndjson' });
|
|
312
|
+
check(ndjsonResponse.id === 777, 'stdio ndjson initialize returned wrong id');
|
|
313
|
+
check(Boolean(ndjsonResponse.result && ndjsonResponse.result.serverInfo), 'stdio ndjson initialize missing serverInfo');
|
|
314
|
+
addResult('mcp.stdio.ndjson.initialize', true, { server: ndjsonResponse.result.serverInfo.name });
|
|
315
|
+
}
|
|
316
|
+
|
|
191
317
|
{
|
|
192
318
|
const list = await handleRequest({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
|
|
193
319
|
check(Array.isArray(list.tools) && list.tools.length > 0, 'mcp tools/list empty');
|
|
@@ -245,7 +371,7 @@ async function runProof(options = {}) {
|
|
|
245
371
|
},
|
|
246
372
|
},
|
|
247
373
|
});
|
|
248
|
-
const payload =
|
|
374
|
+
const payload = parseLeadingJson(call.content[0].text);
|
|
249
375
|
check(payload.accepted === false, 'mcp capture_feedback should apply rubric gating');
|
|
250
376
|
addResult('mcp.tools.call.capture_feedback.rubric_gate', true, { accepted: payload.accepted });
|
|
251
377
|
}
|
|
@@ -323,10 +449,14 @@ async function runProof(options = {}) {
|
|
|
323
449
|
} catch (err) {
|
|
324
450
|
addResult('fatal', false, { error: err.message });
|
|
325
451
|
} finally {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
452
|
+
await new Promise((resolve) => server.close(resolve));
|
|
453
|
+
fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
|
|
454
|
+
if (previousFeedbackDir === undefined) delete process.env.RLHF_FEEDBACK_DIR;
|
|
455
|
+
else process.env.RLHF_FEEDBACK_DIR = previousFeedbackDir;
|
|
456
|
+
if (previousApiKey === undefined) delete process.env.RLHF_API_KEY;
|
|
457
|
+
else process.env.RLHF_API_KEY = previousApiKey;
|
|
458
|
+
if (previousMcpProfile === undefined) delete process.env.RLHF_MCP_PROFILE;
|
|
459
|
+
else process.env.RLHF_MCP_PROFILE = previousMcpProfile;
|
|
330
460
|
}
|
|
331
461
|
|
|
332
462
|
if (writeArtifacts) {
|
|
@@ -22,7 +22,34 @@ const PROOF_DIR = path.join(__dirname, '..', 'proof', 'subway-upgrades');
|
|
|
22
22
|
const REPORT_JSON = path.join(PROOF_DIR, 'subway-upgrades-report.json');
|
|
23
23
|
const REPORT_MD = path.join(PROOF_DIR, 'subway-upgrades-report.md');
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const REQUIRED_SUBWAY_ARTIFACTS = [
|
|
26
|
+
['.claude', 'scripts', 'feedback', 'vector-store.js'],
|
|
27
|
+
['.claude', 'scripts', 'feedback', 'dpo-optimizer.js'],
|
|
28
|
+
['.claude', 'scripts', 'feedback', 'thompson-sampling.js'],
|
|
29
|
+
['.github', 'workflows', 'self-healing-monitor.yml'],
|
|
30
|
+
['.github', 'workflows', 'self-healing-auto-fix.yml'],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function hasRequiredSubwayArtifacts(root) {
|
|
34
|
+
if (!root || !fs.existsSync(root)) return false;
|
|
35
|
+
return REQUIRED_SUBWAY_ARTIFACTS.every((segments) => fs.existsSync(path.join(root, ...segments)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveSubwayRoot() {
|
|
39
|
+
const envRoot = process.env.SUBWAY_ROOT;
|
|
40
|
+
if (envRoot) return envRoot;
|
|
41
|
+
|
|
42
|
+
const candidates = [
|
|
43
|
+
path.join(__dirname, '..', '..', '..', 'Subway_RN_Demo'),
|
|
44
|
+
path.join(__dirname, '..', '..', '..', '..', 'Subway_RN_Demo'),
|
|
45
|
+
path.join(__dirname, '..', '..', 'Subway_RN_Demo'),
|
|
46
|
+
];
|
|
47
|
+
const ready = candidates.find((candidate) => hasRequiredSubwayArtifacts(candidate));
|
|
48
|
+
if (ready) return ready;
|
|
49
|
+
return candidates[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SUBWAY_ROOT = resolveSubwayRoot();
|
|
26
53
|
|
|
27
54
|
function run() {
|
|
28
55
|
const results = { passed: 0, failed: 0, requirements: {} };
|
package/src/api/server.js
CHANGED
|
@@ -39,7 +39,7 @@ const {
|
|
|
39
39
|
verifyWebhookSignature,
|
|
40
40
|
verifyGithubWebhookSignature,
|
|
41
41
|
handleGithubWebhook,
|
|
42
|
-
|
|
42
|
+
getFunnelAnalytics,
|
|
43
43
|
} = require('../../scripts/billing');
|
|
44
44
|
|
|
45
45
|
function getSafeDataDir() {
|
|
@@ -150,6 +150,15 @@ function extractBearerToken(req) {
|
|
|
150
150
|
return auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Admin-only guard for static RLHF_API_KEY.
|
|
155
|
+
* Billing keys are intentionally excluded from admin actions.
|
|
156
|
+
*/
|
|
157
|
+
function isStaticAdminAuthorized(req, expected) {
|
|
158
|
+
if (!expected) return true;
|
|
159
|
+
return extractBearerToken(req) === expected;
|
|
160
|
+
}
|
|
161
|
+
|
|
153
162
|
function extractTags(input) {
|
|
154
163
|
if (Array.isArray(input)) return input;
|
|
155
164
|
if (typeof input === 'string') {
|
|
@@ -230,6 +239,16 @@ function createApiServer() {
|
|
|
230
239
|
return;
|
|
231
240
|
}
|
|
232
241
|
|
|
242
|
+
if (req.method === 'GET' && pathname === '/healthz') {
|
|
243
|
+
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = getFeedbackPaths();
|
|
244
|
+
sendJson(res, 200, {
|
|
245
|
+
status: 'ok',
|
|
246
|
+
feedbackLogPath: FEEDBACK_LOG_PATH,
|
|
247
|
+
memoryLogPath: MEMORY_LOG_PATH,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
233
252
|
// Stripe webhook is unauthenticated — uses HMAC signature verification instead
|
|
234
253
|
if (req.method === 'POST' && pathname === '/v1/billing/webhook') {
|
|
235
254
|
try {
|
|
@@ -302,6 +321,28 @@ function createApiServer() {
|
|
|
302
321
|
return;
|
|
303
322
|
}
|
|
304
323
|
|
|
324
|
+
// Public checkout session creation for top-of-funnel acquisition.
|
|
325
|
+
if (req.method === 'POST' && pathname === '/v1/billing/checkout') {
|
|
326
|
+
try {
|
|
327
|
+
const body = await parseJsonBody(req);
|
|
328
|
+
const result = await createCheckoutSession({
|
|
329
|
+
successUrl: body.successUrl,
|
|
330
|
+
cancelUrl: body.cancelUrl,
|
|
331
|
+
customerEmail: body.customerEmail,
|
|
332
|
+
installId: body.installId,
|
|
333
|
+
metadata: body.metadata,
|
|
334
|
+
});
|
|
335
|
+
sendJson(res, 200, result);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err.statusCode) {
|
|
338
|
+
sendJson(res, err.statusCode, { error: err.message });
|
|
339
|
+
} else {
|
|
340
|
+
sendJson(res, 500, { error: err.message || 'Internal Server Error' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
305
346
|
if (!isAuthorized(req, expectedApiKey)) {
|
|
306
347
|
sendJson(res, 401, { error: 'Unauthorized' });
|
|
307
348
|
return;
|
|
@@ -314,16 +355,6 @@ function createApiServer() {
|
|
|
314
355
|
}
|
|
315
356
|
|
|
316
357
|
try {
|
|
317
|
-
if (req.method === 'GET' && pathname === '/healthz') {
|
|
318
|
-
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = getFeedbackPaths();
|
|
319
|
-
sendJson(res, 200, {
|
|
320
|
-
status: 'ok',
|
|
321
|
-
feedbackLogPath: FEEDBACK_LOG_PATH,
|
|
322
|
-
memoryLogPath: MEMORY_LOG_PATH,
|
|
323
|
-
});
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
358
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
328
359
|
sendJson(res, 200, analyzeFeedback());
|
|
329
360
|
return;
|
|
@@ -487,18 +518,6 @@ function createApiServer() {
|
|
|
487
518
|
// Billing routes
|
|
488
519
|
// ----------------------------------------------------------------
|
|
489
520
|
|
|
490
|
-
// POST /v1/billing/checkout — create Stripe Checkout session
|
|
491
|
-
if (req.method === 'POST' && pathname === '/v1/billing/checkout') {
|
|
492
|
-
const body = await parseJsonBody(req);
|
|
493
|
-
const result = await createCheckoutSession({
|
|
494
|
-
successUrl: body.successUrl,
|
|
495
|
-
cancelUrl: body.cancelUrl,
|
|
496
|
-
customerEmail: body.customerEmail,
|
|
497
|
-
});
|
|
498
|
-
sendJson(res, 200, result);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
521
|
// GET /v1/billing/usage — usage for the authenticated key
|
|
503
522
|
if (req.method === 'GET' && pathname === '/v1/billing/usage') {
|
|
504
523
|
const token = extractBearerToken(req);
|
|
@@ -517,15 +536,30 @@ function createApiServer() {
|
|
|
517
536
|
|
|
518
537
|
// POST /v1/billing/provision — manually provision key (admin)
|
|
519
538
|
if (req.method === 'POST' && pathname === '/v1/billing/provision') {
|
|
539
|
+
if (!isStaticAdminAuthorized(req, expectedApiKey)) {
|
|
540
|
+
sendJson(res, 403, { error: 'Forbidden: admin key required' });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
520
544
|
const body = await parseJsonBody(req);
|
|
521
545
|
if (!body.customerId) {
|
|
522
546
|
throw createHttpError(400, 'customerId is required');
|
|
523
547
|
}
|
|
524
|
-
const result = provisionApiKey(body.customerId
|
|
548
|
+
const result = provisionApiKey(body.customerId, {
|
|
549
|
+
installId: body.installId,
|
|
550
|
+
source: 'admin_provision',
|
|
551
|
+
});
|
|
525
552
|
sendJson(res, 200, result);
|
|
526
553
|
return;
|
|
527
554
|
}
|
|
528
555
|
|
|
556
|
+
// GET /v1/analytics/funnel — aggregate acquisition/activation/paid funnel metrics
|
|
557
|
+
if (req.method === 'GET' && pathname === '/v1/analytics/funnel') {
|
|
558
|
+
const summary = getFunnelAnalytics();
|
|
559
|
+
sendJson(res, 200, summary);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
529
563
|
sendJson(res, 404, { error: 'Not Found' });
|
|
530
564
|
} catch (err) {
|
|
531
565
|
if (err.statusCode) {
|