thumbgate 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +16 -5
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +19 -7
- package/adapters/opencode/opencode.json +1 -1
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +1 -0
- package/package.json +22 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- 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/compare.html +302 -0
- package/public/index.html +41 -11
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/ai-search-visibility.js +142 -0
- package/scripts/changeset-check.js +372 -0
- package/scripts/check-congruence.js +7 -4
- package/scripts/computer-use-firewall.js +45 -15
- package/scripts/docker-sandbox-planner.js +208 -0
- package/scripts/export-hf-dataset.js +293 -0
- package/scripts/github-about.js +56 -0
- package/scripts/operational-integrity.js +7 -1
- package/scripts/published-cli.js +10 -1
- package/scripts/statusline-links.js +238 -0
- package/scripts/statusline.sh +39 -4
- package/scripts/sync-github-about.js +7 -4
- package/scripts/tool-registry.js +11 -0
- package/scripts/workflow-sentinel.js +83 -35
- package/src/api/server.js +12 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const { getHomeDir, getRuntimeDir, resolveProjectDir } = require('./feedback-paths');
|
|
11
|
+
const { resolveProKey } = require('./pro-local-dashboard');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_ORIGIN = 'http://localhost:3456';
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 150;
|
|
15
|
+
const DEFAULT_BOOT_GRACE_MS = 5000;
|
|
16
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
17
|
+
|
|
18
|
+
function parseOrigin(origin) {
|
|
19
|
+
const url = new URL(origin || DEFAULT_ORIGIN);
|
|
20
|
+
return {
|
|
21
|
+
origin: url.origin,
|
|
22
|
+
host: url.hostname,
|
|
23
|
+
port: Number(url.port || (url.protocol === 'https:' ? 443 : 80)),
|
|
24
|
+
protocol: url.protocol,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isLoopbackHost(host) {
|
|
29
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runtimeStatePath(options = {}) {
|
|
33
|
+
return path.join(getRuntimeDir(options), 'statusline-api.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readRuntimeState(options = {}) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(runtimeStatePath(options), 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeRuntimeState(payload, options = {}) {
|
|
45
|
+
const targetPath = runtimeStatePath(options);
|
|
46
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
47
|
+
fs.writeFileSync(targetPath, JSON.stringify(payload, null, 2) + '\n');
|
|
48
|
+
return targetPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isPidAlive(pid) {
|
|
52
|
+
const numericPid = Number(pid);
|
|
53
|
+
if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
|
|
54
|
+
try {
|
|
55
|
+
process.kill(numericPid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shouldReuseBootingState(state, now = Date.now()) {
|
|
63
|
+
if (!state || !isPidAlive(state.pid)) return false;
|
|
64
|
+
const startedAt = Date.parse(state.startedAt || 0);
|
|
65
|
+
if (!Number.isFinite(startedAt)) return true;
|
|
66
|
+
return now - startedAt < DEFAULT_BOOT_GRACE_MS;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requestOk(url, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const req = http.get(url, (res) => {
|
|
72
|
+
res.resume();
|
|
73
|
+
resolve(res.statusCode >= 200 && res.statusCode < 500);
|
|
74
|
+
});
|
|
75
|
+
req.on('error', () => resolve(false));
|
|
76
|
+
req.setTimeout(timeoutMs, () => {
|
|
77
|
+
req.destroy();
|
|
78
|
+
resolve(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function probeLocalServer(origin, options = {}) {
|
|
84
|
+
const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
85
|
+
return requestOk(`${origin}/health`, timeoutMs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function launchLocalServer(options = {}) {
|
|
89
|
+
const env = options.env || process.env;
|
|
90
|
+
const origin = parseOrigin(options.origin || env.THUMBGATE_LOCAL_API_ORIGIN || DEFAULT_ORIGIN);
|
|
91
|
+
const homeDir = options.homeDir || getHomeDir({ env });
|
|
92
|
+
const resolvedKey = (options.resolveKey || resolveProKey)({ env, homeDir });
|
|
93
|
+
const projectDir = resolveProjectDir({ env, cwd: options.cwd || process.cwd() });
|
|
94
|
+
const childEnv = {
|
|
95
|
+
...env,
|
|
96
|
+
HOST: origin.host,
|
|
97
|
+
PORT: String(origin.port),
|
|
98
|
+
THUMBGATE_LOCAL_API_ORIGIN: origin.origin,
|
|
99
|
+
THUMBGATE_PROJECT_DIR: projectDir,
|
|
100
|
+
THUMBGATE_PRO_MODE: '1',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (resolvedKey && resolvedKey.key) {
|
|
104
|
+
childEnv.THUMBGATE_API_KEY = resolvedKey.key;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const child = spawn(
|
|
108
|
+
process.execPath,
|
|
109
|
+
[path.join(PKG_ROOT, 'bin', 'cli.js'), 'start-api'],
|
|
110
|
+
{
|
|
111
|
+
cwd: projectDir,
|
|
112
|
+
env: childEnv,
|
|
113
|
+
detached: true,
|
|
114
|
+
stdio: 'ignore',
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
child.unref();
|
|
118
|
+
|
|
119
|
+
const state = {
|
|
120
|
+
pid: child.pid,
|
|
121
|
+
projectDir,
|
|
122
|
+
origin: origin.origin,
|
|
123
|
+
startedAt: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
writeRuntimeState(state, { env, home: homeDir });
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildLinkState({
|
|
130
|
+
ready,
|
|
131
|
+
booting,
|
|
132
|
+
origin,
|
|
133
|
+
canBootstrap,
|
|
134
|
+
}) {
|
|
135
|
+
if (ready) {
|
|
136
|
+
return {
|
|
137
|
+
state: 'ready',
|
|
138
|
+
dashboardLabel: 'Dashboard',
|
|
139
|
+
lessonsLabel: 'Lessons',
|
|
140
|
+
upLabel: '👍',
|
|
141
|
+
downLabel: '👎',
|
|
142
|
+
dashboardUrl: `${origin}/dashboard`,
|
|
143
|
+
lessonsUrl: `${origin}/lessons`,
|
|
144
|
+
upUrl: `${origin}/feedback/quick?signal=up`,
|
|
145
|
+
downUrl: `${origin}/feedback/quick?signal=down`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (booting) {
|
|
150
|
+
return {
|
|
151
|
+
state: 'booting',
|
|
152
|
+
dashboardLabel: 'Dashboard…',
|
|
153
|
+
lessonsLabel: 'Lessons…',
|
|
154
|
+
upLabel: '👍',
|
|
155
|
+
downLabel: '👎',
|
|
156
|
+
dashboardUrl: '',
|
|
157
|
+
lessonsUrl: '',
|
|
158
|
+
upUrl: '',
|
|
159
|
+
downUrl: '',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
state: canBootstrap ? 'offline' : 'unavailable',
|
|
165
|
+
dashboardLabel: canBootstrap ? 'Dash: thumbgate pro' : 'Dashboard',
|
|
166
|
+
lessonsLabel: 'Learn: thumbgate lessons',
|
|
167
|
+
upLabel: '👍',
|
|
168
|
+
downLabel: '👎',
|
|
169
|
+
dashboardUrl: '',
|
|
170
|
+
lessonsUrl: '',
|
|
171
|
+
upUrl: '',
|
|
172
|
+
downUrl: '',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function getStatuslineLinks(options = {}) {
|
|
177
|
+
const env = options.env || process.env;
|
|
178
|
+
if (env._TEST_THUMBGATE_STATUSLINE_LINKS_JSON) {
|
|
179
|
+
return JSON.parse(env._TEST_THUMBGATE_STATUSLINE_LINKS_JSON);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const homeDir = options.homeDir || getHomeDir({ env });
|
|
183
|
+
const parsedOrigin = parseOrigin(options.origin || env.THUMBGATE_LOCAL_API_ORIGIN || DEFAULT_ORIGIN);
|
|
184
|
+
const origin = parsedOrigin.origin;
|
|
185
|
+
const allowLocalBootstrap = isLoopbackHost(parsedOrigin.host);
|
|
186
|
+
const probe = options.probeLocalServer || probeLocalServer;
|
|
187
|
+
const resolveKey = options.resolveKey || resolveProKey;
|
|
188
|
+
const startServer = options.launchLocalServer || launchLocalServer;
|
|
189
|
+
const key = resolveKey({ env, homeDir });
|
|
190
|
+
const canBootstrap = allowLocalBootstrap && Boolean(key && key.key);
|
|
191
|
+
|
|
192
|
+
const ready = allowLocalBootstrap ? await probe(origin, options) : false;
|
|
193
|
+
if (ready) {
|
|
194
|
+
return buildLinkState({ ready: true, booting: false, origin, canBootstrap });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const state = readRuntimeState({ env, home: homeDir });
|
|
198
|
+
if (shouldReuseBootingState(state)) {
|
|
199
|
+
return buildLinkState({ ready: false, booting: true, origin, canBootstrap });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (canBootstrap) {
|
|
203
|
+
startServer({
|
|
204
|
+
env,
|
|
205
|
+
homeDir,
|
|
206
|
+
origin,
|
|
207
|
+
cwd: options.cwd || process.cwd(),
|
|
208
|
+
resolveKey: () => key,
|
|
209
|
+
});
|
|
210
|
+
return buildLinkState({ ready: false, booting: true, origin, canBootstrap });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return buildLinkState({ ready: false, booting: false, origin, canBootstrap });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (require.main === module) {
|
|
217
|
+
getStatuslineLinks()
|
|
218
|
+
.then((payload) => {
|
|
219
|
+
process.stdout.write(JSON.stringify(payload));
|
|
220
|
+
})
|
|
221
|
+
.catch(() => {
|
|
222
|
+
process.exit(0);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
buildLinkState,
|
|
228
|
+
getStatuslineLinks,
|
|
229
|
+
isPidAlive,
|
|
230
|
+
launchLocalServer,
|
|
231
|
+
parseOrigin,
|
|
232
|
+
isLoopbackHost,
|
|
233
|
+
probeLocalServer,
|
|
234
|
+
readRuntimeState,
|
|
235
|
+
runtimeStatePath,
|
|
236
|
+
shouldReuseBootingState,
|
|
237
|
+
writeRuntimeState,
|
|
238
|
+
};
|
package/scripts/statusline.sh
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# Resolve script directory safely (CodeQL: no uncontrolled paths)
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
8
8
|
case "$SCRIPT_DIR" in *[!a-zA-Z0-9/_.-]*) echo "ThumbGate: invalid script path"; exit 1;; esac
|
|
9
|
+
LOCAL_API_ORIGIN="${THUMBGATE_LOCAL_API_ORIGIN:-http://localhost:3456}"
|
|
9
10
|
|
|
10
11
|
# ── Parse Claude Code session JSON from stdin ─────────────────────
|
|
11
12
|
eval "$(cat | jq -r '
|
|
@@ -63,7 +64,7 @@ fi
|
|
|
63
64
|
# Background refresh from REST API when cache is stale (>120s)
|
|
64
65
|
if [ $(( _NOW - ${CACHE_TS:-0} )) -gt 120 ]; then
|
|
65
66
|
(
|
|
66
|
-
_R=$(curl -s --max-time 3 "
|
|
67
|
+
_R=$(curl -s --max-time 3 "${LOCAL_API_ORIGIN}/v1/feedback/stats" -H "Authorization: Bearer ${THUMBGATE_API_KEY:-tg_creator_dev_enterprise}" 2>/dev/null)
|
|
67
68
|
[ -z "$_R" ] && exit 0
|
|
68
69
|
echo "$_R" | python3 -c "
|
|
69
70
|
import json,sys,time,os
|
|
@@ -78,6 +79,23 @@ except:pass
|
|
|
78
79
|
disown 2>/dev/null
|
|
79
80
|
fi
|
|
80
81
|
|
|
82
|
+
# ── Clickable statusline affordances ─────────────────────────────
|
|
83
|
+
LINK_STATE="offline"
|
|
84
|
+
UP_URL=""; DOWN_URL=""; DASHBOARD_URL=""; LESSONS_URL=""
|
|
85
|
+
DASHBOARD_LABEL="Dashboard"; LESSONS_LABEL="Lessons"
|
|
86
|
+
_LINKS_JSON=$(node "${SCRIPT_DIR}/statusline-links.js" 2>/dev/null)
|
|
87
|
+
if [ -n "$_LINKS_JSON" ]; then
|
|
88
|
+
eval "$(echo "$_LINKS_JSON" | jq -r '
|
|
89
|
+
@sh "LINK_STATE=\(.state // "offline")",
|
|
90
|
+
@sh "UP_URL=\(.upUrl // "")",
|
|
91
|
+
@sh "DOWN_URL=\(.downUrl // "")",
|
|
92
|
+
@sh "DASHBOARD_URL=\(.dashboardUrl // "")",
|
|
93
|
+
@sh "LESSONS_URL=\(.lessonsUrl // "")",
|
|
94
|
+
@sh "DASHBOARD_LABEL=\(.dashboardLabel // "Dashboard")",
|
|
95
|
+
@sh "LESSONS_LABEL=\(.lessonsLabel // "Lessons")"
|
|
96
|
+
' 2>/dev/null)"
|
|
97
|
+
fi
|
|
98
|
+
|
|
81
99
|
# ── ThumbGate package metadata ────────────────────────────────────────
|
|
82
100
|
TG_VERSION="unknown"; TG_TIER="Free"
|
|
83
101
|
_META_JSON=$(node "${SCRIPT_DIR}/statusline-meta.js" 2>/dev/null)
|
|
@@ -107,17 +125,34 @@ case "${TREND}" in
|
|
|
107
125
|
improving) ARROW="↗" ;; degrading) ARROW="↘" ;; stable) ARROW="→" ;; *) ARROW="?" ;;
|
|
108
126
|
esac
|
|
109
127
|
|
|
128
|
+
osc8_link() {
|
|
129
|
+
local url="$1"
|
|
130
|
+
local label="$2"
|
|
131
|
+
if [ -n "$url" ]; then
|
|
132
|
+
printf '\033]8;;%s\a%s\033]8;;\a' "$url" "$label"
|
|
133
|
+
else
|
|
134
|
+
printf '%s' "$label"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
UP_ICON="$(osc8_link "$UP_URL" "👍")"
|
|
139
|
+
DOWN_ICON="$(osc8_link "$DOWN_URL" "👎")"
|
|
140
|
+
DASHBOARD_LINK="$(osc8_link "$DASHBOARD_URL" "$DASHBOARD_LABEL")"
|
|
141
|
+
LESSONS_LINK="$(osc8_link "$LESSONS_URL" "$LESSONS_LABEL")"
|
|
142
|
+
|
|
110
143
|
# ── Output (single line) ─────────────────────────────────────────
|
|
111
144
|
LINE="ThumbGate v${TG_VERSION} · ${TG_TIER}"
|
|
112
145
|
if [ "$UP" = "0" ] && [ "$DOWN" = "0" ]; then
|
|
113
|
-
|
|
146
|
+
LINE="${D}${LINE} · no feedback yet${RST} · ${C}${DASHBOARD_LINK}${RST} · ${M}${LESSONS_LINK}${RST}"
|
|
147
|
+
printf '%b\n' "$LINE"
|
|
114
148
|
else
|
|
115
|
-
LINE="${LINE} · ${G}${BD}${UP}${RST}
|
|
149
|
+
LINE="${LINE} · ${G}${BD}${UP}${RST}${UP_ICON} ${R}${BD}${DOWN}${RST}${DOWN_ICON} ${ARROW}"
|
|
116
150
|
|
|
117
151
|
# Control Tower alerts (if any)
|
|
118
152
|
[ "${SLO_V:-0}" -gt 0 ] && LINE="${LINE} ${R}${SLO_V} SLO${RST}"
|
|
119
153
|
[ "${AT_RISK:-0}" -gt 0 ] && LINE="${LINE} ${R}${AT_RISK}⚠${RST}"
|
|
120
154
|
[ "${ANOMALIES:-0}" -gt 0 ] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
|
|
155
|
+
LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST} · ${M}${LESSONS_LINK}${RST}"
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
printf '%b\n' "$LINE"
|
|
123
158
|
fi
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
fetchLiveGitHubAbout,
|
|
7
7
|
loadGitHubAboutConfig,
|
|
8
8
|
updateLiveGitHubAbout,
|
|
9
|
+
verifyLiveGitHubAbout,
|
|
9
10
|
} = require('./github-about');
|
|
10
11
|
|
|
11
12
|
async function main() {
|
|
@@ -32,11 +33,13 @@ async function main() {
|
|
|
32
33
|
console.log(`Syncing GitHub About for ${about.repo}...`);
|
|
33
34
|
await updateLiveGitHubAbout({ repo: about.repo });
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const verification = await verifyLiveGitHubAbout({
|
|
37
|
+
expected: about,
|
|
38
|
+
repo: about.repo,
|
|
39
|
+
});
|
|
40
|
+
if (verification.errors.length > 0) {
|
|
38
41
|
console.error(`\n❌ GitHub About sync incomplete for ${about.repo}:\n`);
|
|
39
|
-
for (const error of
|
|
42
|
+
for (const error of verification.errors) {
|
|
40
43
|
console.error(` • ${error}`);
|
|
41
44
|
}
|
|
42
45
|
console.error('');
|
package/scripts/tool-registry.js
CHANGED
|
@@ -399,6 +399,17 @@ const TOOLS = [
|
|
|
399
399
|
},
|
|
400
400
|
},
|
|
401
401
|
}),
|
|
402
|
+
destructiveTool({
|
|
403
|
+
name: 'export_hf_dataset',
|
|
404
|
+
description: 'Export ThumbGate agent traces and DPO preference pairs as a HuggingFace-compatible dataset. Produces traces.jsonl, preferences.jsonl, and dataset_info.json with PII-redacted paths. Ready for huggingface-cli upload.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
outputDir: { type: 'string', description: 'Output directory (default: feedback-dir/hf-dataset)' },
|
|
409
|
+
includeProvenance: { type: 'boolean', description: 'Include provenance events in traces (default: true)' },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
402
413
|
destructiveTool({
|
|
403
414
|
name: 'export_databricks_bundle',
|
|
404
415
|
description: 'Export ThumbGate logs and proof artifacts as a Databricks-ready analytics bundle',
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
normalizePosix,
|
|
15
15
|
resolveRepoRoot,
|
|
16
16
|
} = require('./operational-integrity');
|
|
17
|
+
const { buildDockerSandboxPlan } = require('./docker-sandbox-planner');
|
|
17
18
|
const { evaluatePretool } = require('./hybrid-feedback-context');
|
|
18
19
|
|
|
19
20
|
const GOVERNANCE_STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'governance-state.json');
|
|
@@ -523,12 +524,58 @@ function buildEvidence({
|
|
|
523
524
|
return evidence;
|
|
524
525
|
}
|
|
525
526
|
|
|
527
|
+
function addIntegrityRemediations(push, integrity) {
|
|
528
|
+
if (!integrity || !Array.isArray(integrity.blockers)) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const blockerCodes = new Set(integrity.blockers.map((blocker) => blocker.code));
|
|
533
|
+
const remediationSpecs = [
|
|
534
|
+
{
|
|
535
|
+
codes: ['missing_branch_governance'],
|
|
536
|
+
id: 'set_branch_governance',
|
|
537
|
+
title: 'Declare branch governance',
|
|
538
|
+
action: 'Call set_branch_governance with branchName, baseBranch, and PR/release expectations.',
|
|
539
|
+
why: 'Release, merge, and PR workflows need explicit branch state.',
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
codes: ['merge_requires_pr_context'],
|
|
543
|
+
id: 'attach_pr_context',
|
|
544
|
+
title: 'Attach PR context',
|
|
545
|
+
action: 'Update branch governance with prNumber or prUrl before merging.',
|
|
546
|
+
why: 'Merge actions should be tied to one explicit review surface.',
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
codes: ['missing_release_version', 'release_version_mismatch'],
|
|
550
|
+
id: 'align_release_version',
|
|
551
|
+
title: 'Align release version',
|
|
552
|
+
action: 'Set branch governance releaseVersion and verify it matches package.json before publish.',
|
|
553
|
+
why: 'Release metadata should match the artifact being published.',
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
codes: ['publish_requires_base_branch', 'publish_requires_mainline_head'],
|
|
557
|
+
id: 'switch_to_mainline',
|
|
558
|
+
title: 'Run publish from mainline',
|
|
559
|
+
action: `Move the action onto ${integrity.baseBranch || DEFAULT_BASE_BRANCH} after the merge commit exists.`,
|
|
560
|
+
why: 'Publish and tag flows should execute from the protected mainline branch.',
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
for (const remediation of remediationSpecs) {
|
|
565
|
+
if (!remediation.codes.some((code) => blockerCodes.has(code))) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
push(remediation.id, remediation.title, remediation.action, remediation.why);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
526
572
|
function buildRemediations({
|
|
527
573
|
integrity,
|
|
528
574
|
taskScopeViolation,
|
|
529
575
|
protectedSurface,
|
|
530
576
|
blastRadius,
|
|
531
577
|
memoryGuard,
|
|
578
|
+
executionSurface,
|
|
532
579
|
}) {
|
|
533
580
|
const remediations = [];
|
|
534
581
|
const seen = new Set();
|
|
@@ -555,41 +602,7 @@ function buildRemediations({
|
|
|
555
602
|
'Protected policy files need an explicit time-bounded approval.'
|
|
556
603
|
);
|
|
557
604
|
}
|
|
558
|
-
|
|
559
|
-
const blockerCodes = new Set(integrity.blockers.map((blocker) => blocker.code));
|
|
560
|
-
if (blockerCodes.has('missing_branch_governance')) {
|
|
561
|
-
push(
|
|
562
|
-
'set_branch_governance',
|
|
563
|
-
'Declare branch governance',
|
|
564
|
-
'Call set_branch_governance with branchName, baseBranch, and PR/release expectations.',
|
|
565
|
-
'Release, merge, and PR workflows need explicit branch state.'
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
if (blockerCodes.has('merge_requires_pr_context')) {
|
|
569
|
-
push(
|
|
570
|
-
'attach_pr_context',
|
|
571
|
-
'Attach PR context',
|
|
572
|
-
'Update branch governance with prNumber or prUrl before merging.',
|
|
573
|
-
'Merge actions should be tied to one explicit review surface.'
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
if (blockerCodes.has('missing_release_version') || blockerCodes.has('release_version_mismatch')) {
|
|
577
|
-
push(
|
|
578
|
-
'align_release_version',
|
|
579
|
-
'Align release version',
|
|
580
|
-
'Set branch governance releaseVersion and verify it matches package.json before publish.',
|
|
581
|
-
'Release metadata should match the artifact being published.'
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
if (blockerCodes.has('publish_requires_base_branch') || blockerCodes.has('publish_requires_mainline_head')) {
|
|
585
|
-
push(
|
|
586
|
-
'switch_to_mainline',
|
|
587
|
-
'Run publish from mainline',
|
|
588
|
-
`Move the action onto ${integrity.baseBranch || DEFAULT_BASE_BRANCH} after the merge commit exists.`,
|
|
589
|
-
'Publish and tag flows should execute from the protected mainline branch.'
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
605
|
+
addIntegrityRemediations(push, integrity);
|
|
593
606
|
if (memoryGuard && memoryGuard.mode && memoryGuard.mode !== 'allow') {
|
|
594
607
|
push(
|
|
595
608
|
'retrieve_lessons',
|
|
@@ -606,6 +619,14 @@ function buildRemediations({
|
|
|
606
619
|
'Smaller blast radii are easier to verify and recover.'
|
|
607
620
|
);
|
|
608
621
|
}
|
|
622
|
+
if (executionSurface?.shouldSandbox) {
|
|
623
|
+
push(
|
|
624
|
+
'route_to_docker_sandbox',
|
|
625
|
+
'Route through Docker Sandboxes',
|
|
626
|
+
`Launch the repo in Docker Sandboxes before retrying. Standalone: ${executionSurface.launchers.standalone}. Docker Desktop: ${executionSurface.launchers.dockerDesktop}.`,
|
|
627
|
+
'Isolated execution limits host damage when a high-risk local action goes wrong.'
|
|
628
|
+
);
|
|
629
|
+
}
|
|
609
630
|
|
|
610
631
|
return remediations;
|
|
611
632
|
}
|
|
@@ -615,6 +636,9 @@ function buildReasoning(report) {
|
|
|
615
636
|
`Workflow sentinel risk ${report.band} (${report.riskScore}) for ${report.toolName}.`,
|
|
616
637
|
`Blast radius: ${report.blastRadius.summary}.`,
|
|
617
638
|
];
|
|
639
|
+
if (report.executionSurface?.shouldSandbox) {
|
|
640
|
+
lines.push(`Execution surface: ${report.executionSurface.summary}`);
|
|
641
|
+
}
|
|
618
642
|
for (const driver of report.drivers.slice(0, 4)) {
|
|
619
643
|
lines.push(`Driver ${driver.key} (+${driver.weight}): ${driver.reason}`);
|
|
620
644
|
}
|
|
@@ -624,6 +648,16 @@ function buildReasoning(report) {
|
|
|
624
648
|
return lines;
|
|
625
649
|
}
|
|
626
650
|
|
|
651
|
+
function getSentinelActionType(toolName) {
|
|
652
|
+
if (toolName === 'Bash') {
|
|
653
|
+
return 'shell.exec';
|
|
654
|
+
}
|
|
655
|
+
if (EDIT_LIKE_TOOLS.has(toolName)) {
|
|
656
|
+
return 'file.write';
|
|
657
|
+
}
|
|
658
|
+
return '';
|
|
659
|
+
}
|
|
660
|
+
|
|
627
661
|
function chooseDecision({ riskScore, integrity, memoryGuard, blastRadius, command }) {
|
|
628
662
|
const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
|
|
629
663
|
const destructiveBypass = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command) || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
|
|
@@ -713,6 +747,18 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
|
|
|
713
747
|
taskScopeViolation,
|
|
714
748
|
protectedSurface: protectedSurfaceForRisk,
|
|
715
749
|
});
|
|
750
|
+
const executionSurface = buildDockerSandboxPlan({
|
|
751
|
+
toolName,
|
|
752
|
+
actionType: getSentinelActionType(toolName),
|
|
753
|
+
command: toolInput.command,
|
|
754
|
+
repoPath,
|
|
755
|
+
affectedFiles,
|
|
756
|
+
riskBand: risk.band,
|
|
757
|
+
riskScore: risk.score,
|
|
758
|
+
requiresNetwork: Boolean(
|
|
759
|
+
/\b(?:curl|wget|gh\s+pr|git\s+push|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i.test(toolInput.command || '')
|
|
760
|
+
),
|
|
761
|
+
});
|
|
716
762
|
const decision = chooseDecision({
|
|
717
763
|
riskScore: risk.score,
|
|
718
764
|
integrity,
|
|
@@ -736,6 +782,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
|
|
|
736
782
|
protectedSurface: protectedSurfaceForRisk,
|
|
737
783
|
blastRadius,
|
|
738
784
|
memoryGuard,
|
|
785
|
+
executionSurface,
|
|
739
786
|
});
|
|
740
787
|
const summary = decision === 'allow'
|
|
741
788
|
? 'No predictive workflow blockers detected.'
|
|
@@ -753,6 +800,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
|
|
|
753
800
|
blastRadius,
|
|
754
801
|
evidence,
|
|
755
802
|
remediations,
|
|
803
|
+
executionSurface,
|
|
756
804
|
memoryGuard,
|
|
757
805
|
taskScopeViolation,
|
|
758
806
|
operationalIntegrity: {
|
package/src/api/server.js
CHANGED
|
@@ -169,6 +169,7 @@ const PRO_PAGE_PATH = path.resolve(__dirname, '../../public/pro.html');
|
|
|
169
169
|
const DASHBOARD_PAGE_PATH = path.resolve(__dirname, '../../public/dashboard.html');
|
|
170
170
|
const LESSONS_PAGE_PATH = path.resolve(__dirname, '../../public/lessons.html');
|
|
171
171
|
const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
|
|
172
|
+
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
172
173
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
173
174
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
174
175
|
const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
|
|
@@ -2791,6 +2792,16 @@ async function addContext(){
|
|
|
2791
2792
|
return;
|
|
2792
2793
|
}
|
|
2793
2794
|
|
|
2795
|
+
if (isGetLikeRequest && pathname === '/compare') {
|
|
2796
|
+
try {
|
|
2797
|
+
const html = fs.readFileSync(COMPARE_PAGE_PATH, 'utf-8');
|
|
2798
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
2799
|
+
} catch {
|
|
2800
|
+
sendJson(res, 404, { error: 'Compare page not found' });
|
|
2801
|
+
}
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2794
2805
|
if (isGetLikeRequest && pathname === '/blog') {
|
|
2795
2806
|
try {
|
|
2796
2807
|
const blogPath = path.resolve(__dirname, '../../public/blog.html');
|
|
@@ -2848,7 +2859,7 @@ async function addContext(){
|
|
|
2848
2859
|
version: pkg.version,
|
|
2849
2860
|
status: 'ok',
|
|
2850
2861
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
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'],
|
|
2862
|
+
endpoints: ['/health', '/dashboard', '/guide', '/compare', '/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'],
|
|
2852
2863
|
}, {}, {
|
|
2853
2864
|
headOnly: isHeadRequest,
|
|
2854
2865
|
});
|