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.
@@ -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 = JSON.parse(call.content[0].text);
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
- if (server) await new Promise((resolve) => server.close(resolve));
327
- try {
328
- fs.rmSync(tmpFeedbackDir, { recursive: true, force: true });
329
- } catch (e) {}
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 SUBWAY_ROOT = path.join(__dirname, '..', '..', '..', 'Subway_RN_Demo');
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
- loadKeyStore,
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) {