thumbgate 0.9.13 → 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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +6 -3
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +105 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/forge/forge.yaml +28 -0
- package/adapters/mcp/server-stdio.js +32 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +53 -3
- package/config/mcp-allowlists.json +10 -0
- package/openapi/openapi.yaml +105 -0
- package/package.json +4 -4
- package/plugins/amp-skill/INSTALL.md +3 -4
- package/plugins/amp-skill/SKILL.md +0 -1
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-skill/INSTALL.md +1 -2
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +1 -0
- package/public/dashboard.html +1 -1
- package/public/guide.html +1 -1
- package/public/index.html +29 -5
- package/public/learn/agent-harness-pattern.html +1 -1
- package/public/learn/ai-agent-persistent-memory.html +1 -1
- package/public/learn/mcp-pre-action-gates-explained.html +1 -1
- package/public/learn/stop-ai-agent-force-push.html +1 -1
- package/public/learn/vibe-coding-safety-net.html +1 -1
- package/public/learn.html +62 -1
- package/public/lessons.html +1 -1
- package/public/pro.html +1 -1
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/async-job-runner.js +84 -24
- package/scripts/auto-wire-hooks.js +59 -1
- package/scripts/context-manager.js +330 -0
- package/scripts/dashboard.js +1 -1
- package/scripts/distribution-surfaces.js +12 -0
- package/scripts/ensure-repo-bootstrap.js +15 -14
- package/scripts/feedback-history-distiller.js +7 -1
- package/scripts/feedback-loop.js +10 -4
- package/scripts/feedback-paths.js +142 -10
- package/scripts/feedback-root-consolidator.js +18 -4
- package/scripts/gates-engine.js +96 -10
- package/scripts/hook-auto-capture.sh +1 -1
- package/scripts/hosted-job-launcher.js +260 -0
- package/scripts/managed-dpo-export.js +91 -0
- package/scripts/obsidian-export.js +0 -1
- package/scripts/operational-integrity.js +50 -7
- package/scripts/post-everywhere.js +10 -0
- package/scripts/prove-lancedb.js +62 -4
- package/scripts/publish-decision.js +16 -0
- package/scripts/self-healing-check.js +6 -1
- package/scripts/seo-gsd.js +217 -4
- package/scripts/social-analytics/load-env.js +33 -2
- package/scripts/social-analytics/store.js +200 -2
- package/scripts/statusline-cache-path.js +9 -6
- package/scripts/sync-version.js +18 -11
- package/scripts/tool-registry.js +37 -0
- package/scripts/train_from_feedback.py +0 -4
- package/scripts/workflow-sentinel.js +793 -0
- package/src/api/server.js +297 -38
- /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
package/src/api/server.js
CHANGED
|
@@ -13,6 +13,10 @@ const {
|
|
|
13
13
|
getFeedbackPaths,
|
|
14
14
|
appendDiagnosticRecord,
|
|
15
15
|
} = require('../../scripts/feedback-loop');
|
|
16
|
+
const {
|
|
17
|
+
readActiveProjectState,
|
|
18
|
+
resolveProjectDir,
|
|
19
|
+
} = require('../../scripts/feedback-paths');
|
|
16
20
|
const {
|
|
17
21
|
readRecentConversationWindow,
|
|
18
22
|
} = require('../../scripts/feedback-history-distiller');
|
|
@@ -48,6 +52,11 @@ const {
|
|
|
48
52
|
const {
|
|
49
53
|
buildCloudflareSandboxPlan,
|
|
50
54
|
} = require('../../scripts/cloudflare-dynamic-sandbox');
|
|
55
|
+
const {
|
|
56
|
+
listJobStates,
|
|
57
|
+
readJobState,
|
|
58
|
+
requestJobControl,
|
|
59
|
+
} = require('../../scripts/async-job-runner');
|
|
51
60
|
const {
|
|
52
61
|
loadModel,
|
|
53
62
|
getReliability,
|
|
@@ -132,6 +141,13 @@ const {
|
|
|
132
141
|
const {
|
|
133
142
|
resolveAnalyticsWindow,
|
|
134
143
|
} = require('../../scripts/analytics-window');
|
|
144
|
+
const {
|
|
145
|
+
launchDpoExportJob,
|
|
146
|
+
launchHarnessJob,
|
|
147
|
+
pauseQueuedJob,
|
|
148
|
+
cancelQueuedJob,
|
|
149
|
+
resumeHostedJob,
|
|
150
|
+
} = require('../../scripts/hosted-job-launcher');
|
|
135
151
|
const {
|
|
136
152
|
appendWorkflowSprintLead,
|
|
137
153
|
advanceWorkflowSprintLead,
|
|
@@ -161,6 +177,9 @@ const SESSION_COOKIE_NAME = 'thumbgate_session_id';
|
|
|
161
177
|
const ACQUISITION_COOKIE_NAME = 'thumbgate_acquisition_id';
|
|
162
178
|
const VISITOR_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 90;
|
|
163
179
|
const BUILD_METADATA = resolveBuildMetadata();
|
|
180
|
+
const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
181
|
+
const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
|
|
182
|
+
const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
|
|
164
183
|
|
|
165
184
|
// ---------------------------------------------------------------------------
|
|
166
185
|
// Stripe event tracking helpers
|
|
@@ -203,8 +222,55 @@ function computeConversionStats(events) {
|
|
|
203
222
|
}
|
|
204
223
|
// ---------------------------------------------------------------------------
|
|
205
224
|
|
|
206
|
-
function
|
|
207
|
-
const
|
|
225
|
+
function getRequestedProjectSelection(req, parsed) {
|
|
226
|
+
const projectFromQuery = parsed && parsed.searchParams
|
|
227
|
+
? parsed.searchParams.get('project')
|
|
228
|
+
: null;
|
|
229
|
+
const projectFromHeaders = req && req.headers
|
|
230
|
+
? req.headers['x-thumbgate-project-dir'] || req.headers['x-thumbgate-project']
|
|
231
|
+
: null;
|
|
232
|
+
return projectFromQuery || projectFromHeaders || null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getEffectiveRequestedProjectSelection(req, parsed) {
|
|
236
|
+
return isProjectSelectionAllowed(req, parsed)
|
|
237
|
+
? getRequestedProjectSelection(req, parsed)
|
|
238
|
+
: null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isProjectSelectionAllowed(req, parsed) {
|
|
242
|
+
const explicitProject = getRequestedProjectSelection(req, parsed);
|
|
243
|
+
if (!explicitProject) return true;
|
|
244
|
+
return isLoopbackHost(getRequestHostHeader(req));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolveRequestProjectDir(req, parsed) {
|
|
248
|
+
const explicitProject = getEffectiveRequestedProjectSelection(req, parsed);
|
|
249
|
+
return resolveProjectDir({
|
|
250
|
+
projectDir: explicitProject,
|
|
251
|
+
env: process.env,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function shouldPreferProjectScopedFeedback(req, parsed) {
|
|
256
|
+
const explicitProject = getEffectiveRequestedProjectSelection(req, parsed);
|
|
257
|
+
if (explicitProject) return true;
|
|
258
|
+
if (process.env.THUMBGATE_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR) return true;
|
|
259
|
+
if (process.env.THUMBGATE_FEEDBACK_DIR) return false;
|
|
260
|
+
return Boolean(readActiveProjectState({ env: process.env }));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getRequestFeedbackPaths(req, parsed) {
|
|
264
|
+
const explicitProject = getEffectiveRequestedProjectSelection(req, parsed);
|
|
265
|
+
return getFeedbackPaths({
|
|
266
|
+
projectDir: resolveRequestProjectDir(req, parsed),
|
|
267
|
+
explicitProjectDir: explicitProject,
|
|
268
|
+
skipExplicitFeedbackDir: shouldPreferProjectScopedFeedback(req, parsed),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getSafeDataDir(req, parsed) {
|
|
273
|
+
const { FEEDBACK_LOG_PATH } = getRequestFeedbackPaths(req, parsed);
|
|
208
274
|
return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
209
275
|
}
|
|
210
276
|
|
|
@@ -857,6 +923,14 @@ function getPublicOrigin(req) {
|
|
|
857
923
|
return `${proto}://${host}`;
|
|
858
924
|
}
|
|
859
925
|
|
|
926
|
+
function getRequestHostHeader(req) {
|
|
927
|
+
const forwardedHost = req.headers['x-forwarded-host'];
|
|
928
|
+
if (Array.isArray(forwardedHost)) {
|
|
929
|
+
return forwardedHost[0] || req.headers.host || '';
|
|
930
|
+
}
|
|
931
|
+
return forwardedHost || req.headers.host || '';
|
|
932
|
+
}
|
|
933
|
+
|
|
860
934
|
function isLoopbackHost(hostValue) {
|
|
861
935
|
const rawHost = String(hostValue || '').split(',')[0].trim();
|
|
862
936
|
if (!rawHost) {
|
|
@@ -1156,6 +1230,7 @@ nav .container { display: flex; justify-content: space-between; align-items: cen
|
|
|
1156
1230
|
.actions-bar { flex-direction: column; }
|
|
1157
1231
|
}
|
|
1158
1232
|
</style>
|
|
1233
|
+
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
1159
1234
|
</head>
|
|
1160
1235
|
<body>
|
|
1161
1236
|
<nav><div class="container">
|
|
@@ -1486,6 +1561,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
1486
1561
|
font-size: 14px;
|
|
1487
1562
|
}
|
|
1488
1563
|
</style>
|
|
1564
|
+
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
1489
1565
|
</head>
|
|
1490
1566
|
<body>
|
|
1491
1567
|
<main>
|
|
@@ -1765,6 +1841,7 @@ function renderCheckoutCancelledPage(runtimeConfig) {
|
|
|
1765
1841
|
margin-top: 12px;
|
|
1766
1842
|
}
|
|
1767
1843
|
</style>
|
|
1844
|
+
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
1768
1845
|
</head>
|
|
1769
1846
|
<body>
|
|
1770
1847
|
<main>
|
|
@@ -1937,6 +2014,7 @@ function renderWorkflowSprintIntakeResultPage(runtimeConfig, { title, detail, le
|
|
|
1937
2014
|
border: 1px solid var(--line);
|
|
1938
2015
|
}
|
|
1939
2016
|
</style>
|
|
2017
|
+
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
1940
2018
|
</head>
|
|
1941
2019
|
<body>
|
|
1942
2020
|
<main>
|
|
@@ -2094,10 +2172,10 @@ function extractTags(input) {
|
|
|
2094
2172
|
return [];
|
|
2095
2173
|
}
|
|
2096
2174
|
|
|
2097
|
-
function resolveSafePath(inputPath, { mustExist = false } = {}) {
|
|
2175
|
+
function resolveSafePath(inputPath, { mustExist = false, safeDataDir } = {}) {
|
|
2098
2176
|
const allowExternal = process.env.THUMBGATE_ALLOW_EXTERNAL_PATHS === 'true';
|
|
2099
2177
|
const resolved = path.resolve(String(inputPath || ''));
|
|
2100
|
-
const SAFE_DATA_DIR = getSafeDataDir();
|
|
2178
|
+
const SAFE_DATA_DIR = safeDataDir || getSafeDataDir();
|
|
2101
2179
|
const inSafeRoot = resolved === SAFE_DATA_DIR || resolved.startsWith(`${SAFE_DATA_DIR}${path.sep}`);
|
|
2102
2180
|
|
|
2103
2181
|
if (!allowExternal && !inSafeRoot) {
|
|
@@ -2111,6 +2189,56 @@ function resolveSafePath(inputPath, { mustExist = false } = {}) {
|
|
|
2111
2189
|
return resolved;
|
|
2112
2190
|
}
|
|
2113
2191
|
|
|
2192
|
+
function resolveDpoExportPaths(body = {}, options = {}) {
|
|
2193
|
+
const { safeDataDir, fallbackMemoryLogPath = null } = options;
|
|
2194
|
+
return {
|
|
2195
|
+
inputPath: body.inputPath
|
|
2196
|
+
? resolveSafePath(body.inputPath, { mustExist: true, safeDataDir })
|
|
2197
|
+
: null,
|
|
2198
|
+
memoryLogPath: body.memoryLogPath
|
|
2199
|
+
? resolveSafePath(body.memoryLogPath, { mustExist: true, safeDataDir })
|
|
2200
|
+
: null,
|
|
2201
|
+
outputPath: body.outputPath
|
|
2202
|
+
? resolveSafePath(body.outputPath, { safeDataDir })
|
|
2203
|
+
: null,
|
|
2204
|
+
fallbackMemoryLogPath,
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function loadDpoExportMemories({ inputPath, memoryLogPath, fallbackMemoryLogPath = null }) {
|
|
2209
|
+
if (inputPath) {
|
|
2210
|
+
const raw = fs.readFileSync(inputPath, 'utf-8');
|
|
2211
|
+
const parsedMemories = JSON.parse(raw);
|
|
2212
|
+
return Array.isArray(parsedMemories) ? parsedMemories : parsedMemories.memories || [];
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
return readJSONL(memoryLogPath || fallbackMemoryLogPath || DEFAULT_LOCAL_MEMORY_LOG);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function parseJobStatuses(value) {
|
|
2219
|
+
if (!value) return [];
|
|
2220
|
+
return String(value)
|
|
2221
|
+
.split(',')
|
|
2222
|
+
.map((entry) => entry.trim())
|
|
2223
|
+
.filter(Boolean);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function readHostedJobOrThrow(jobId) {
|
|
2227
|
+
const state = readJobState(jobId);
|
|
2228
|
+
if (!state) {
|
|
2229
|
+
throw createHttpError(404, `Job not found: ${jobId}`);
|
|
2230
|
+
}
|
|
2231
|
+
return state;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
function normalizeJobIdFromPath(pathname, suffix = '') {
|
|
2235
|
+
const pattern = suffix
|
|
2236
|
+
? new RegExp(`^/v1/jobs/([^/]+)${suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`)
|
|
2237
|
+
: /^\/v1\/jobs\/([^/]+)$/;
|
|
2238
|
+
const match = pathname.match(pattern);
|
|
2239
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2114
2242
|
function createApiServer() {
|
|
2115
2243
|
const expectedApiKey = getExpectedApiKey();
|
|
2116
2244
|
|
|
@@ -2121,6 +2249,13 @@ function createApiServer() {
|
|
|
2121
2249
|
const isGetLikeRequest = req.method === 'GET' || isHeadRequest;
|
|
2122
2250
|
const publicOrigin = getPublicOrigin(req);
|
|
2123
2251
|
const hostedConfig = resolveHostedBillingConfig({ requestOrigin: publicOrigin });
|
|
2252
|
+
if (!isProjectSelectionAllowed(req, parsed)) {
|
|
2253
|
+
sendJson(res, 403, { error: 'project selection is only available on localhost requests' });
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
|
|
2257
|
+
const requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
|
|
2258
|
+
const requestSafeDataDir = getSafeDataDir(req, parsed);
|
|
2124
2259
|
|
|
2125
2260
|
// Public MCP endpoint — responds to Smithery registry scanning and MCP initialize
|
|
2126
2261
|
// The initialize handshake is unauthenticated; subsequent tool calls require Bearer auth
|
|
@@ -2394,7 +2529,7 @@ function createApiServer() {
|
|
|
2394
2529
|
const signal = parsed.searchParams.get('signal');
|
|
2395
2530
|
if (signal === 'up' || signal === 'down') {
|
|
2396
2531
|
const chatHistory = readRecentConversationWindow({
|
|
2397
|
-
feedbackDir:
|
|
2532
|
+
feedbackDir: requestSafeDataDir,
|
|
2398
2533
|
limit: 10,
|
|
2399
2534
|
});
|
|
2400
2535
|
const result = captureFeedback({
|
|
@@ -2484,7 +2619,7 @@ async function addContext(){
|
|
|
2484
2619
|
sendJson(res, 400, { error: 'relatedFeedbackId is required' });
|
|
2485
2620
|
return;
|
|
2486
2621
|
}
|
|
2487
|
-
const feedbackDir =
|
|
2622
|
+
const feedbackDir = requestSafeDataDir;
|
|
2488
2623
|
const detailField = signal === 'down' ? 'whatWentWrong' : 'whatWorked';
|
|
2489
2624
|
const updated = updateLessonRecord(feedbackDir, relatedFeedbackId, (existing) => {
|
|
2490
2625
|
const nextTags = Array.from(new Set([
|
|
@@ -2536,7 +2671,7 @@ async function addContext(){
|
|
|
2536
2671
|
const lessonUpdateMatch = pathname.match(/^\/lessons\/([^/]+)\/update$/);
|
|
2537
2672
|
if (req.method === 'POST' && lessonUpdateMatch) {
|
|
2538
2673
|
const lessonId = decodeURIComponent(lessonUpdateMatch[1]);
|
|
2539
|
-
const feedbackDir =
|
|
2674
|
+
const feedbackDir = requestSafeDataDir;
|
|
2540
2675
|
const body = await parseJsonBody(req);
|
|
2541
2676
|
const record = findRecordById(lessonId, feedbackDir);
|
|
2542
2677
|
if (!record) {
|
|
@@ -2568,7 +2703,7 @@ async function addContext(){
|
|
|
2568
2703
|
const lessonDeleteMatch = pathname.match(/^\/lessons\/([^/]+)\/delete$/);
|
|
2569
2704
|
if (req.method === 'POST' && lessonDeleteMatch) {
|
|
2570
2705
|
const lessonId = decodeURIComponent(lessonDeleteMatch[1]);
|
|
2571
|
-
const feedbackDir =
|
|
2706
|
+
const feedbackDir = requestSafeDataDir;
|
|
2572
2707
|
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
2573
2708
|
const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
|
|
2574
2709
|
const deletedMemory = deleteRecordFromJsonl(memoryLogPath, lessonId);
|
|
@@ -2585,7 +2720,7 @@ async function addContext(){
|
|
|
2585
2720
|
const lessonDetailMatch = pathname.match(/^\/lessons\/([^/]+)$/);
|
|
2586
2721
|
if (isGetLikeRequest && lessonDetailMatch && lessonDetailMatch[1] !== '') {
|
|
2587
2722
|
const lessonId = decodeURIComponent(lessonDetailMatch[1]);
|
|
2588
|
-
const feedbackDir =
|
|
2723
|
+
const feedbackDir = requestSafeDataDir;
|
|
2589
2724
|
const record = findRecordById(lessonId, feedbackDir);
|
|
2590
2725
|
if (!record) {
|
|
2591
2726
|
sendHtml(res, 404, renderLessonDetailHtml(null, lessonId));
|
|
@@ -2713,7 +2848,7 @@ async function addContext(){
|
|
|
2713
2848
|
version: pkg.version,
|
|
2714
2849
|
status: 'ok',
|
|
2715
2850
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
2716
|
-
endpoints: ['/health', '/dashboard', '/guide', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/analytics/databricks/export'],
|
|
2851
|
+
endpoints: ['/health', '/dashboard', '/guide', '/learn', '/pro', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
|
|
2717
2852
|
}, {}, {
|
|
2718
2853
|
headOnly: isHeadRequest,
|
|
2719
2854
|
});
|
|
@@ -2959,7 +3094,7 @@ async function addContext(){
|
|
|
2959
3094
|
}
|
|
2960
3095
|
|
|
2961
3096
|
if (isGetLikeRequest && pathname === '/healthz') {
|
|
2962
|
-
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } =
|
|
3097
|
+
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = requestFeedbackPaths;
|
|
2963
3098
|
sendJson(res, 200, {
|
|
2964
3099
|
status: 'ok',
|
|
2965
3100
|
feedbackLogPath: FEEDBACK_LOG_PATH,
|
|
@@ -3497,7 +3632,7 @@ async function addContext(){
|
|
|
3497
3632
|
|
|
3498
3633
|
try {
|
|
3499
3634
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
3500
|
-
sendJson(res, 200, analyzeFeedback());
|
|
3635
|
+
sendJson(res, 200, analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH));
|
|
3501
3636
|
return;
|
|
3502
3637
|
}
|
|
3503
3638
|
|
|
@@ -3649,6 +3784,112 @@ async function addContext(){
|
|
|
3649
3784
|
return;
|
|
3650
3785
|
}
|
|
3651
3786
|
|
|
3787
|
+
if (req.method === 'POST' && pathname === '/v1/jobs/harness') {
|
|
3788
|
+
const body = await parseJsonBody(req);
|
|
3789
|
+
const identifier = body.harness || body.harnessId;
|
|
3790
|
+
if (!identifier) {
|
|
3791
|
+
throw createHttpError(400, 'harness is required');
|
|
3792
|
+
}
|
|
3793
|
+
const inputs = parseOptionalObject(body.inputs, 'inputs') || {};
|
|
3794
|
+
try {
|
|
3795
|
+
const launched = launchHarnessJob(identifier, inputs, {
|
|
3796
|
+
jobId: normalizeNullableText(body.jobId) || undefined,
|
|
3797
|
+
skill: normalizeNullableText(body.skill) || undefined,
|
|
3798
|
+
partnerProfile: normalizeNullableText(body.partnerProfile) || undefined,
|
|
3799
|
+
autoImprove: body.autoImprove !== false,
|
|
3800
|
+
});
|
|
3801
|
+
sendJson(res, 202, {
|
|
3802
|
+
accepted: true,
|
|
3803
|
+
jobId: launched.jobId,
|
|
3804
|
+
status: launched.state.status,
|
|
3805
|
+
launchMode: launched.launchMode,
|
|
3806
|
+
pid: launched.pid,
|
|
3807
|
+
statusUrl: `/v1/jobs/${encodeURIComponent(launched.jobId)}`,
|
|
3808
|
+
job: launched.state,
|
|
3809
|
+
});
|
|
3810
|
+
} catch (err) {
|
|
3811
|
+
throw createHttpError(err.statusCode || 400, err.message || 'Invalid hosted harness request');
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
if (req.method === 'GET' && pathname === '/v1/jobs') {
|
|
3817
|
+
const limit = Number(parsed.searchParams.get('limit') || 20);
|
|
3818
|
+
const statuses = parseJobStatuses(parsed.searchParams.get('status'));
|
|
3819
|
+
const jobs = listJobStates({
|
|
3820
|
+
limit: Number.isFinite(limit) ? limit : 20,
|
|
3821
|
+
statuses,
|
|
3822
|
+
});
|
|
3823
|
+
sendJson(res, 200, {
|
|
3824
|
+
total: jobs.length,
|
|
3825
|
+
jobs,
|
|
3826
|
+
});
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
{
|
|
3831
|
+
const jobId = normalizeJobIdFromPath(pathname);
|
|
3832
|
+
if (req.method === 'GET' && jobId) {
|
|
3833
|
+
sendJson(res, 200, {
|
|
3834
|
+
job: readHostedJobOrThrow(jobId),
|
|
3835
|
+
});
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
{
|
|
3841
|
+
const jobId = normalizeJobIdFromPath(pathname, '/control');
|
|
3842
|
+
if (req.method === 'POST' && jobId) {
|
|
3843
|
+
const state = readHostedJobOrThrow(jobId);
|
|
3844
|
+
const body = await parseJsonBody(req);
|
|
3845
|
+
const action = normalizeNullableText(body.action);
|
|
3846
|
+
if (!action || !JOB_CONTROL_ACTIONS.has(action)) {
|
|
3847
|
+
throw createHttpError(400, 'action must be one of pause, cancel, or resume');
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
if (TERMINAL_JOB_STATUSES.has(state.status)) {
|
|
3851
|
+
throw createHttpError(409, `Job ${jobId} is already ${state.status}`);
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
if (action === 'resume') {
|
|
3855
|
+
const launched = resumeHostedJob(jobId);
|
|
3856
|
+
sendJson(res, 202, {
|
|
3857
|
+
accepted: true,
|
|
3858
|
+
action,
|
|
3859
|
+
jobId,
|
|
3860
|
+
launchMode: launched.launchMode,
|
|
3861
|
+
pid: launched.pid,
|
|
3862
|
+
job: launched.state,
|
|
3863
|
+
});
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
if (IDLE_JOB_STATUSES.has(state.status)) {
|
|
3868
|
+
const job = action === 'pause'
|
|
3869
|
+
? pauseQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {})
|
|
3870
|
+
: cancelQueuedJob(jobId, parseOptionalObject(body.metadata, 'metadata') || {});
|
|
3871
|
+
sendJson(res, 202, {
|
|
3872
|
+
accepted: true,
|
|
3873
|
+
action,
|
|
3874
|
+
jobId,
|
|
3875
|
+
job,
|
|
3876
|
+
});
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
const metadata = parseOptionalObject(body.metadata, 'metadata') || {};
|
|
3881
|
+
const control = requestJobControl(jobId, action, metadata);
|
|
3882
|
+
sendJson(res, 202, {
|
|
3883
|
+
accepted: true,
|
|
3884
|
+
action,
|
|
3885
|
+
jobId,
|
|
3886
|
+
control,
|
|
3887
|
+
job: readHostedJobOrThrow(jobId),
|
|
3888
|
+
});
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3652
3893
|
if (req.method === 'POST' && pathname === '/v1/gates/constraint') {
|
|
3653
3894
|
const body = await parseJsonBody(req);
|
|
3654
3895
|
if (!body.key || body.value === undefined) {
|
|
@@ -3740,7 +3981,9 @@ async function addContext(){
|
|
|
3740
3981
|
|
|
3741
3982
|
if (req.method === 'GET' && pathname === '/v1/feedback/summary') {
|
|
3742
3983
|
const recent = Number(parsed.searchParams.get('recent') || 20);
|
|
3743
|
-
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20
|
|
3984
|
+
const summary = feedbackSummary(Number.isFinite(recent) ? recent : 20, {
|
|
3985
|
+
feedbackDir: requestFeedbackDir,
|
|
3986
|
+
});
|
|
3744
3987
|
sendJson(res, 200, { summary });
|
|
3745
3988
|
return;
|
|
3746
3989
|
}
|
|
@@ -3757,6 +4000,7 @@ async function addContext(){
|
|
|
3757
4000
|
limit: Number.isFinite(limit) ? limit : 10,
|
|
3758
4001
|
category,
|
|
3759
4002
|
tags,
|
|
4003
|
+
feedbackDir: requestFeedbackDir,
|
|
3760
4004
|
});
|
|
3761
4005
|
sendJson(res, 200, results);
|
|
3762
4006
|
return;
|
|
@@ -3846,7 +4090,9 @@ async function addContext(){
|
|
|
3846
4090
|
if (req.method === 'POST' && pathname === '/v1/feedback/rules') {
|
|
3847
4091
|
const body = await parseJsonBody(req);
|
|
3848
4092
|
const minOccurrences = Number(body.minOccurrences || 2);
|
|
3849
|
-
const outputPath = body.outputPath
|
|
4093
|
+
const outputPath = body.outputPath
|
|
4094
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
4095
|
+
: undefined;
|
|
3850
4096
|
const result = writePreventionRules(outputPath, Number.isFinite(minOccurrences) ? minOccurrences : 2);
|
|
3851
4097
|
sendJson(res, 200, {
|
|
3852
4098
|
path: result.path,
|
|
@@ -3872,25 +4118,38 @@ async function addContext(){
|
|
|
3872
4118
|
|
|
3873
4119
|
if (req.method === 'POST' && pathname === '/v1/dpo/export') {
|
|
3874
4120
|
const body = await parseJsonBody(req);
|
|
3875
|
-
|
|
4121
|
+
const paths = resolveDpoExportPaths(body, {
|
|
4122
|
+
safeDataDir: requestSafeDataDir,
|
|
4123
|
+
fallbackMemoryLogPath: requestFeedbackPaths.MEMORY_LOG_PATH,
|
|
4124
|
+
});
|
|
4125
|
+
const wantsAsync = body.async === true || normalizeNullableText(body.mode) === 'async';
|
|
3876
4126
|
|
|
3877
|
-
if (
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
4127
|
+
if (wantsAsync) {
|
|
4128
|
+
try {
|
|
4129
|
+
const launched = launchDpoExportJob(paths, {
|
|
4130
|
+
jobId: normalizeNullableText(body.jobId) || undefined,
|
|
4131
|
+
});
|
|
4132
|
+
sendJson(res, 202, {
|
|
4133
|
+
accepted: true,
|
|
4134
|
+
async: true,
|
|
4135
|
+
jobId: launched.jobId,
|
|
4136
|
+
status: launched.state.status,
|
|
4137
|
+
outputPath: paths.outputPath,
|
|
4138
|
+
statusUrl: `/v1/jobs/${encodeURIComponent(launched.jobId)}`,
|
|
4139
|
+
launchMode: launched.launchMode,
|
|
4140
|
+
job: launched.state,
|
|
4141
|
+
});
|
|
4142
|
+
} catch (err) {
|
|
4143
|
+
throw createHttpError(err.statusCode || 400, err.message || 'Invalid DPO export request');
|
|
4144
|
+
}
|
|
4145
|
+
return;
|
|
3887
4146
|
}
|
|
3888
4147
|
|
|
4148
|
+
const memories = loadDpoExportMemories(paths);
|
|
3889
4149
|
const result = exportDpoFromMemories(memories);
|
|
3890
|
-
if (
|
|
3891
|
-
|
|
3892
|
-
fs.
|
|
3893
|
-
fs.writeFileSync(safeOutputPath, result.jsonl);
|
|
4150
|
+
if (paths.outputPath) {
|
|
4151
|
+
fs.mkdirSync(path.dirname(paths.outputPath), { recursive: true });
|
|
4152
|
+
fs.writeFileSync(paths.outputPath, result.jsonl);
|
|
3894
4153
|
}
|
|
3895
4154
|
|
|
3896
4155
|
sendJson(res, 200, {
|
|
@@ -3899,14 +4158,16 @@ async function addContext(){
|
|
|
3899
4158
|
learnings: result.learnings.length,
|
|
3900
4159
|
unpairedErrors: result.unpairedErrors.length,
|
|
3901
4160
|
unpairedLearnings: result.unpairedLearnings.length,
|
|
3902
|
-
outputPath:
|
|
4161
|
+
outputPath: paths.outputPath,
|
|
3903
4162
|
});
|
|
3904
4163
|
return;
|
|
3905
4164
|
}
|
|
3906
4165
|
|
|
3907
4166
|
if (req.method === 'POST' && pathname === '/v1/analytics/databricks/export') {
|
|
3908
4167
|
const body = await parseJsonBody(req);
|
|
3909
|
-
const outputPath = body.outputPath
|
|
4168
|
+
const outputPath = body.outputPath
|
|
4169
|
+
? resolveSafePath(body.outputPath, { safeDataDir: requestSafeDataDir })
|
|
4170
|
+
: undefined;
|
|
3910
4171
|
const result = exportDatabricksBundle(undefined, outputPath);
|
|
3911
4172
|
sendJson(res, 200, result);
|
|
3912
4173
|
return;
|
|
@@ -3971,7 +4232,7 @@ async function addContext(){
|
|
|
3971
4232
|
// ----------------------------------------------------------------
|
|
3972
4233
|
|
|
3973
4234
|
if (req.method === 'GET' && pathname === '/v1/quality/scores') {
|
|
3974
|
-
const modelPath = path.join(
|
|
4235
|
+
const modelPath = path.join(requestSafeDataDir, 'feedback_model.json');
|
|
3975
4236
|
const model = loadModel(modelPath);
|
|
3976
4237
|
const reliability = getReliability(model);
|
|
3977
4238
|
const category = parsed.searchParams.get('category');
|
|
@@ -3991,7 +4252,7 @@ async function addContext(){
|
|
|
3991
4252
|
}
|
|
3992
4253
|
|
|
3993
4254
|
if (req.method === 'GET' && pathname === '/v1/quality/rules') {
|
|
3994
|
-
const rulesPath = path.join(
|
|
4255
|
+
const rulesPath = path.join(requestSafeDataDir, 'prevention-rules.md');
|
|
3995
4256
|
let markdown = '';
|
|
3996
4257
|
if (fs.existsSync(rulesPath)) {
|
|
3997
4258
|
markdown = fs.readFileSync(rulesPath, 'utf8').trim();
|
|
@@ -4008,7 +4269,7 @@ async function addContext(){
|
|
|
4008
4269
|
}
|
|
4009
4270
|
|
|
4010
4271
|
if (req.method === 'GET' && pathname === '/v1/quality/posteriors') {
|
|
4011
|
-
const modelPath = path.join(
|
|
4272
|
+
const modelPath = path.join(requestSafeDataDir, 'feedback_model.json');
|
|
4012
4273
|
const model = loadModel(modelPath);
|
|
4013
4274
|
const posteriors = samplePosteriors(model);
|
|
4014
4275
|
sendJson(res, 200, { posteriors });
|
|
@@ -4247,9 +4508,8 @@ async function addContext(){
|
|
|
4247
4508
|
return;
|
|
4248
4509
|
}
|
|
4249
4510
|
|
|
4250
|
-
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4251
4511
|
const billingSummary = await getBillingSummaryLive(summaryOptions);
|
|
4252
|
-
const data = generateDashboard(
|
|
4512
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
4253
4513
|
analyticsWindow: summaryOptions,
|
|
4254
4514
|
billingSummary,
|
|
4255
4515
|
billingSource: 'live',
|
|
@@ -4275,9 +4535,8 @@ async function addContext(){
|
|
|
4275
4535
|
}
|
|
4276
4536
|
|
|
4277
4537
|
try {
|
|
4278
|
-
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4279
4538
|
const billingSummary = await getBillingSummaryLive(summaryOptions);
|
|
4280
|
-
const data = generateDashboard(
|
|
4539
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
4281
4540
|
analyticsWindow: summaryOptions,
|
|
4282
4541
|
billingSummary,
|
|
4283
4542
|
billingSource: 'live',
|
|
File without changes
|