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,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
9
|
+
const CHANGESET_DIR = path.join(PROJECT_ROOT, '.changeset');
|
|
10
|
+
const DEFAULT_PACKAGE_NAME = 'thumbgate';
|
|
11
|
+
const MIN_SUMMARY_LENGTH = 20;
|
|
12
|
+
const RELEASE_TYPES = new Set(['major', 'minor', 'patch']);
|
|
13
|
+
const RELEASE_RELEVANT_FILES = new Set([
|
|
14
|
+
'README.md',
|
|
15
|
+
'package.json',
|
|
16
|
+
'package-lock.json',
|
|
17
|
+
'server.json',
|
|
18
|
+
]);
|
|
19
|
+
const RELEASE_RELEVANT_PREFIXES = [
|
|
20
|
+
'.claude-plugin/',
|
|
21
|
+
'.cursor-plugin/',
|
|
22
|
+
'.well-known/',
|
|
23
|
+
'adapters/',
|
|
24
|
+
'bin/',
|
|
25
|
+
'config/',
|
|
26
|
+
'plugins/',
|
|
27
|
+
'public/',
|
|
28
|
+
'scripts/',
|
|
29
|
+
'src/',
|
|
30
|
+
'workers/',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
34
|
+
const options = {};
|
|
35
|
+
for (const arg of argv) {
|
|
36
|
+
if (arg.startsWith('--base=')) {
|
|
37
|
+
options.baseRef = arg.slice('--base='.length);
|
|
38
|
+
} else if (arg.startsWith('--since=')) {
|
|
39
|
+
options.baseRef = arg.slice('--since='.length);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isChangesetMarkdownFile(relPath) {
|
|
46
|
+
return relPath.startsWith('.changeset/')
|
|
47
|
+
&& relPath.endsWith('.md')
|
|
48
|
+
&& path.basename(relPath) !== 'README.md';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isReleaseRelevantFile(relPath) {
|
|
52
|
+
const normalized = String(relPath || '').trim().replaceAll('\\', '/');
|
|
53
|
+
if (!normalized || isChangesetMarkdownFile(normalized)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (normalized.startsWith('docs/')
|
|
57
|
+
|| normalized.startsWith('proof/')
|
|
58
|
+
|| normalized.startsWith('tests/')
|
|
59
|
+
|| normalized.startsWith('.github/')) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (RELEASE_RELEVANT_FILES.has(normalized)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return RELEASE_RELEVANT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isVersionedReleaseChangeSet(changedFiles = []) {
|
|
69
|
+
const normalizedFiles = changedFiles.map((file) => String(file || '').trim().replaceAll('\\', '/'));
|
|
70
|
+
return normalizedFiles.includes('package.json')
|
|
71
|
+
&& normalizedFiles.includes('CHANGELOG.md')
|
|
72
|
+
&& normalizedFiles.some(isChangesetMarkdownFile);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function splitChangesetDocument(content) {
|
|
76
|
+
const normalized = String(content || '').replaceAll('\r\n', '\n');
|
|
77
|
+
const lines = normalized.split('\n');
|
|
78
|
+
if (lines[0]?.trim() !== '---') {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
83
|
+
if (closingIndex === -1) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
frontmatterLines: lines.slice(1, closingIndex),
|
|
89
|
+
summary: lines.slice(closingIndex + 1).join('\n').trim(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stripWrappingQuotes(value) {
|
|
94
|
+
const text = String(value || '').trim();
|
|
95
|
+
if (text.length >= 2 && ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith('\'') && text.endsWith('\'')))) {
|
|
96
|
+
return text.slice(1, -1).trim();
|
|
97
|
+
}
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseReleaseLine(line) {
|
|
102
|
+
const normalized = String(line || '').trim();
|
|
103
|
+
if (!normalized) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const separatorIndex = normalized.indexOf(':');
|
|
108
|
+
if (separatorIndex <= 0 || separatorIndex === normalized.length - 1) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const packageName = stripWrappingQuotes(normalized.slice(0, separatorIndex));
|
|
113
|
+
const releaseType = normalized.slice(separatorIndex + 1).trim();
|
|
114
|
+
if (!packageName || !RELEASE_TYPES.has(releaseType)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
packageName,
|
|
120
|
+
releaseType,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseChangesetMarkdown(content) {
|
|
125
|
+
const document = splitChangesetDocument(content);
|
|
126
|
+
if (!document) {
|
|
127
|
+
return {
|
|
128
|
+
releases: {},
|
|
129
|
+
summary: '',
|
|
130
|
+
errors: ['missing frontmatter'],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const summary = document.summary;
|
|
135
|
+
const releases = {};
|
|
136
|
+
const errors = [];
|
|
137
|
+
const lines = document.frontmatterLines.map((line) => line.trim()).filter(Boolean);
|
|
138
|
+
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
const entry = parseReleaseLine(line);
|
|
141
|
+
if (!entry) {
|
|
142
|
+
errors.push(`invalid frontmatter line: ${line}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
releases[entry.packageName] = entry.releaseType;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!summary) {
|
|
149
|
+
errors.push('missing summary');
|
|
150
|
+
} else if (summary.length < MIN_SUMMARY_LENGTH) {
|
|
151
|
+
errors.push(`summary must be at least ${MIN_SUMMARY_LENGTH} characters`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Object.keys(releases).length === 0) {
|
|
155
|
+
errors.push('missing release entries');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
releases,
|
|
160
|
+
summary,
|
|
161
|
+
errors,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectChangesets({
|
|
166
|
+
dir = CHANGESET_DIR,
|
|
167
|
+
packageName = DEFAULT_PACKAGE_NAME,
|
|
168
|
+
} = {}) {
|
|
169
|
+
if (!fs.existsSync(dir)) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return fs.readdirSync(dir)
|
|
174
|
+
.filter((name) => name.endsWith('.md') && name !== 'README.md')
|
|
175
|
+
.sort()
|
|
176
|
+
.map((name) => {
|
|
177
|
+
const filePath = path.join(dir, name);
|
|
178
|
+
const parsed = parseChangesetMarkdown(fs.readFileSync(filePath, 'utf8'));
|
|
179
|
+
const releaseType = parsed.releases[packageName] || null;
|
|
180
|
+
const errors = [...parsed.errors];
|
|
181
|
+
if (!releaseType) {
|
|
182
|
+
errors.push(`missing ${packageName} release entry`);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
file: path.posix.join('.changeset', name),
|
|
186
|
+
releaseType,
|
|
187
|
+
summary: parsed.summary,
|
|
188
|
+
errors,
|
|
189
|
+
validForPackage: errors.length === 0,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function evaluateChangesetRequirement({
|
|
195
|
+
changedFiles = [],
|
|
196
|
+
changesets = [],
|
|
197
|
+
} = {}) {
|
|
198
|
+
const relevantFiles = changedFiles.filter(isReleaseRelevantFile);
|
|
199
|
+
const required = relevantFiles.length > 0;
|
|
200
|
+
const validChangesets = changesets.filter((entry) => entry.validForPackage);
|
|
201
|
+
const invalidChangesets = changesets.filter((entry) => !entry.validForPackage);
|
|
202
|
+
const versionedRelease = isVersionedReleaseChangeSet(changedFiles);
|
|
203
|
+
|
|
204
|
+
if (!required) {
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
required: false,
|
|
208
|
+
relevantFiles,
|
|
209
|
+
validChangesets,
|
|
210
|
+
invalidChangesets,
|
|
211
|
+
reason: 'No release-relevant changes detected. Changeset not required.',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (validChangesets.length > 0) {
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
required: true,
|
|
219
|
+
relevantFiles,
|
|
220
|
+
validChangesets,
|
|
221
|
+
invalidChangesets,
|
|
222
|
+
reason: `Found ${validChangesets.length} valid changeset file(s) for release-relevant changes.`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (versionedRelease) {
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
required: true,
|
|
230
|
+
relevantFiles,
|
|
231
|
+
validChangesets,
|
|
232
|
+
invalidChangesets,
|
|
233
|
+
reason: 'Release PR already consumed pending changesets into versioned artifacts.',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
required: true,
|
|
240
|
+
relevantFiles,
|
|
241
|
+
validChangesets,
|
|
242
|
+
invalidChangesets,
|
|
243
|
+
reason: 'Release-relevant changes require at least one valid .changeset entry for thumbgate.',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function runGitCommand(args, {
|
|
248
|
+
cwd = PROJECT_ROOT,
|
|
249
|
+
runner = execFileSync,
|
|
250
|
+
} = {}) {
|
|
251
|
+
return String(runner('git', args, {
|
|
252
|
+
cwd,
|
|
253
|
+
encoding: 'utf8',
|
|
254
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
255
|
+
}) || '').trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveBaseRef({
|
|
259
|
+
args = parseArgs(),
|
|
260
|
+
env = process.env,
|
|
261
|
+
cwd = PROJECT_ROOT,
|
|
262
|
+
runner = execFileSync,
|
|
263
|
+
} = {}) {
|
|
264
|
+
const explicitBase = String(args.baseRef || '').trim();
|
|
265
|
+
const baseRef = explicitBase
|
|
266
|
+
|| String(env.CHANGESET_BASE_REF || '').trim()
|
|
267
|
+
|| String(env.GITHUB_BASE_REF || '').trim()
|
|
268
|
+
|| (env.GITHUB_EVENT_NAME === 'merge_group' ? 'origin/main' : '');
|
|
269
|
+
|
|
270
|
+
if (!baseRef) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const candidates = [baseRef];
|
|
275
|
+
if (!baseRef.startsWith('origin/')) {
|
|
276
|
+
candidates.push(`origin/${baseRef}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const candidate of candidates) {
|
|
280
|
+
try {
|
|
281
|
+
runGitCommand(['rev-parse', '--verify', candidate], { cwd, runner });
|
|
282
|
+
return candidate;
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return candidates.at(-1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getChangedFiles({
|
|
290
|
+
baseRef,
|
|
291
|
+
cwd = PROJECT_ROOT,
|
|
292
|
+
runner = execFileSync,
|
|
293
|
+
} = {}) {
|
|
294
|
+
if (!baseRef) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const mergeBase = runGitCommand(['merge-base', 'HEAD', baseRef], { cwd, runner });
|
|
299
|
+
const output = runGitCommand(['diff', '--name-only', '--diff-filter=ACDMRTUXB', `${mergeBase}...HEAD`], { cwd, runner });
|
|
300
|
+
return output ? output.split('\n').map((line) => line.trim()).filter(Boolean) : [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function formatFailure(result) {
|
|
304
|
+
const lines = [result.reason, ''];
|
|
305
|
+
if (result.relevantFiles.length > 0) {
|
|
306
|
+
lines.push('Release-relevant files:');
|
|
307
|
+
result.relevantFiles.forEach((file) => lines.push(`- ${file}`));
|
|
308
|
+
lines.push('');
|
|
309
|
+
}
|
|
310
|
+
if (result.invalidChangesets.length > 0) {
|
|
311
|
+
lines.push('Invalid changesets:');
|
|
312
|
+
result.invalidChangesets.forEach((entry) => {
|
|
313
|
+
lines.push(`- ${entry.file}: ${entry.errors.join('; ')}`);
|
|
314
|
+
});
|
|
315
|
+
lines.push('');
|
|
316
|
+
}
|
|
317
|
+
lines.push('Run `npm run changeset` and add a release note for thumbgate before merging.');
|
|
318
|
+
return lines.join('\n');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function runCli({
|
|
322
|
+
cwd = PROJECT_ROOT,
|
|
323
|
+
env = process.env,
|
|
324
|
+
runner = execFileSync,
|
|
325
|
+
} = {}) {
|
|
326
|
+
const baseRef = resolveBaseRef({ env, cwd, runner });
|
|
327
|
+
if (!baseRef) {
|
|
328
|
+
const result = {
|
|
329
|
+
ok: true,
|
|
330
|
+
skipped: true,
|
|
331
|
+
reason: 'No base ref detected. Skipping changeset check outside PR or merge-group context.',
|
|
332
|
+
};
|
|
333
|
+
console.log(result.reason);
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const changedFiles = getChangedFiles({ baseRef, cwd, runner });
|
|
338
|
+
const changesets = collectChangesets();
|
|
339
|
+
const result = evaluateChangesetRequirement({ changedFiles, changesets });
|
|
340
|
+
if (result.ok) {
|
|
341
|
+
console.log(result.reason);
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.error(formatFailure(result));
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (require.main === module) {
|
|
351
|
+
runCli();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = {
|
|
355
|
+
CHANGESET_DIR,
|
|
356
|
+
DEFAULT_PACKAGE_NAME,
|
|
357
|
+
MIN_SUMMARY_LENGTH,
|
|
358
|
+
RELEASE_RELEVANT_FILES,
|
|
359
|
+
RELEASE_RELEVANT_PREFIXES,
|
|
360
|
+
collectChangesets,
|
|
361
|
+
evaluateChangesetRequirement,
|
|
362
|
+
formatFailure,
|
|
363
|
+
getChangedFiles,
|
|
364
|
+
isChangesetMarkdownFile,
|
|
365
|
+
isReleaseRelevantFile,
|
|
366
|
+
isVersionedReleaseChangeSet,
|
|
367
|
+
parseArgs,
|
|
368
|
+
parseChangesetMarkdown,
|
|
369
|
+
resolveBaseRef,
|
|
370
|
+
runCli,
|
|
371
|
+
runGitCommand,
|
|
372
|
+
};
|
|
@@ -11,9 +11,8 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const {
|
|
13
13
|
collectLocalGitHubAboutErrors,
|
|
14
|
-
compareGitHubAbout,
|
|
15
|
-
fetchLiveGitHubAbout,
|
|
16
14
|
loadGitHubAboutConfig,
|
|
15
|
+
verifyLiveGitHubAbout,
|
|
17
16
|
} = require('./github-about');
|
|
18
17
|
const {
|
|
19
18
|
PRODUCTHUNT_URL,
|
|
@@ -295,8 +294,12 @@ async function main() {
|
|
|
295
294
|
|
|
296
295
|
if (checkLiveGitHubAbout) {
|
|
297
296
|
try {
|
|
298
|
-
const
|
|
299
|
-
|
|
297
|
+
const liveCheck = await verifyLiveGitHubAbout({
|
|
298
|
+
expected: githubAbout,
|
|
299
|
+
repo: githubAbout.repo,
|
|
300
|
+
root: ROOT,
|
|
301
|
+
});
|
|
302
|
+
errors.push(...liveCheck.errors);
|
|
300
303
|
} catch (error) {
|
|
301
304
|
errors.push(`Unable to verify live GitHub About: ${error.message}`);
|
|
302
305
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { buildDockerSandboxPlan } = require('./docker-sandbox-planner');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Computer-Use Action Firewall — normalizes OpenAI Responses API
|
|
@@ -129,13 +130,13 @@ function evaluateAction(action, preset = 'dev-sandbox', customRules = []) {
|
|
|
129
130
|
const normalized = action.type ? action : normalizeAction(action);
|
|
130
131
|
const presetConfig = PRESETS[preset];
|
|
131
132
|
if (!presetConfig) {
|
|
132
|
-
return {
|
|
133
|
+
return attachExecutionSurface({
|
|
133
134
|
decision: 'deny',
|
|
134
135
|
reason: `Unknown preset: ${preset}`,
|
|
135
136
|
preset,
|
|
136
137
|
riskLevel: normalized.riskLevel,
|
|
137
138
|
auditEntry: createAuditEntry(normalized, { decision: 'deny', reason: `Unknown preset: ${preset}`, preset }),
|
|
138
|
-
};
|
|
139
|
+
}, normalized);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// Custom rules override preset defaults
|
|
@@ -143,81 +144,108 @@ function evaluateAction(action, preset = 'dev-sandbox', customRules = []) {
|
|
|
143
144
|
if (rule.action === normalized.type) {
|
|
144
145
|
const decision = rule.decision || 'deny';
|
|
145
146
|
const reason = rule.reason || `Custom rule override for ${normalized.type}`;
|
|
146
|
-
return {
|
|
147
|
+
return attachExecutionSurface({
|
|
147
148
|
decision,
|
|
148
149
|
reason,
|
|
149
150
|
preset,
|
|
150
151
|
riskLevel: normalized.riskLevel,
|
|
151
152
|
auditEntry: createAuditEntry(normalized, { decision, reason, preset }),
|
|
152
|
-
};
|
|
153
|
+
}, normalized);
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
// Check dangerous shell patterns (always deny)
|
|
157
158
|
const dangerousMatch = matchesDangerousPattern(normalized);
|
|
158
159
|
if (dangerousMatch) {
|
|
159
|
-
return {
|
|
160
|
+
return attachExecutionSurface({
|
|
160
161
|
decision: 'deny',
|
|
161
162
|
reason: `Dangerous shell pattern detected: ${dangerousMatch}`,
|
|
162
163
|
preset,
|
|
163
164
|
riskLevel: 'critical',
|
|
164
165
|
auditEntry: createAuditEntry(normalized, { decision: 'deny', reason: `Dangerous shell pattern: ${dangerousMatch}`, preset }),
|
|
165
|
-
};
|
|
166
|
+
}, normalized);
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
// Check secret patterns (always deny)
|
|
169
170
|
const secretMatch = matchesSecretPattern(normalized);
|
|
170
171
|
if (secretMatch) {
|
|
171
|
-
return {
|
|
172
|
+
return attachExecutionSurface({
|
|
172
173
|
decision: 'deny',
|
|
173
174
|
reason: `Secret pattern detected in content: ${secretMatch}`,
|
|
174
175
|
preset,
|
|
175
176
|
riskLevel: 'critical',
|
|
176
177
|
auditEntry: createAuditEntry(normalized, { decision: 'deny', reason: `Secret pattern: ${secretMatch}`, preset }),
|
|
177
|
-
};
|
|
178
|
+
}, normalized);
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
// Evaluate against preset
|
|
181
182
|
if (presetConfig.deny.includes(normalized.type)) {
|
|
182
|
-
return {
|
|
183
|
+
return attachExecutionSurface({
|
|
183
184
|
decision: 'deny',
|
|
184
185
|
reason: `Action ${normalized.type} denied by ${preset} preset`,
|
|
185
186
|
preset,
|
|
186
187
|
riskLevel: normalized.riskLevel,
|
|
187
188
|
auditEntry: createAuditEntry(normalized, { decision: 'deny', reason: `Denied by preset`, preset }),
|
|
188
|
-
};
|
|
189
|
+
}, normalized);
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
if (presetConfig.requireApproval.includes(normalized.type)) {
|
|
192
|
-
return {
|
|
193
|
+
return attachExecutionSurface({
|
|
193
194
|
decision: 'require-approval',
|
|
194
195
|
reason: `Action ${normalized.type} requires approval in ${preset} preset`,
|
|
195
196
|
preset,
|
|
196
197
|
riskLevel: normalized.riskLevel,
|
|
197
198
|
auditEntry: createAuditEntry(normalized, { decision: 'require-approval', reason: `Requires approval`, preset }),
|
|
198
|
-
};
|
|
199
|
+
}, normalized);
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
if (presetConfig.allow.includes(normalized.type)) {
|
|
202
|
-
return {
|
|
203
|
+
return attachExecutionSurface({
|
|
203
204
|
decision: 'allow',
|
|
204
205
|
reason: `Action ${normalized.type} allowed by ${preset} preset`,
|
|
205
206
|
preset,
|
|
206
207
|
riskLevel: normalized.riskLevel,
|
|
207
208
|
auditEntry: createAuditEntry(normalized, { decision: 'allow', reason: `Allowed by preset`, preset }),
|
|
208
|
-
};
|
|
209
|
+
}, normalized);
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
// Default: unknown actions require approval
|
|
212
|
-
return {
|
|
213
|
+
return attachExecutionSurface({
|
|
213
214
|
decision: 'require-approval',
|
|
214
215
|
reason: `Action ${normalized.type} not in preset; defaulting to require-approval`,
|
|
215
216
|
preset,
|
|
216
217
|
riskLevel: normalized.riskLevel,
|
|
217
218
|
auditEntry: createAuditEntry(normalized, { decision: 'require-approval', reason: `Not in preset`, preset }),
|
|
219
|
+
}, normalized);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function attachExecutionSurface(result, action) {
|
|
223
|
+
const executionSurface = buildDockerSandboxPlan({
|
|
224
|
+
toolName: action.type === 'shell.exec' ? 'Bash' : 'Write',
|
|
225
|
+
actionType: action.type,
|
|
226
|
+
command: action.type === 'shell.exec' ? action.target : '',
|
|
227
|
+
repoPath: action.args.repoPath || action.args.cwd || '',
|
|
228
|
+
affectedFiles: action.type.startsWith('file.') && action.target ? [action.target] : [],
|
|
229
|
+
riskBand: toSandboxRiskBand(action.riskLevel),
|
|
230
|
+
requiresNetwork: ['upload', 'download', 'message.send'].includes(action.type),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!executionSurface.shouldSandbox) {
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
...result,
|
|
239
|
+
executionSurface,
|
|
218
240
|
};
|
|
219
241
|
}
|
|
220
242
|
|
|
243
|
+
function toSandboxRiskBand(riskLevel) {
|
|
244
|
+
if (riskLevel === 'high') return 'high';
|
|
245
|
+
if (riskLevel === 'medium') return 'medium';
|
|
246
|
+
return 'low';
|
|
247
|
+
}
|
|
248
|
+
|
|
221
249
|
function createAuditEntry(action, decision) {
|
|
222
250
|
return {
|
|
223
251
|
timestamp: action.timestamp || new Date().toISOString(),
|
|
@@ -247,4 +275,6 @@ module.exports = {
|
|
|
247
275
|
loadConfig,
|
|
248
276
|
matchesDangerousPattern,
|
|
249
277
|
matchesSecretPattern,
|
|
278
|
+
attachExecutionSurface,
|
|
279
|
+
toSandboxRiskBand,
|
|
250
280
|
};
|