codex-overleaf-link 1.1.1
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/LICENSE +21 -0
- package/README.md +457 -0
- package/bin/codex-overleaf-link.mjs +223 -0
- package/extension/src/shared/agentTranscript.js +1175 -0
- package/extension/src/shared/auditRecords.js +568 -0
- package/extension/src/shared/compatibility.js +372 -0
- package/extension/src/shared/compileAdapter.js +176 -0
- package/extension/src/shared/governanceRules.js +252 -0
- package/extension/src/shared/i18n.js +565 -0
- package/extension/src/shared/models.js +106 -0
- package/extension/src/shared/otText.js +505 -0
- package/extension/src/shared/projectFiles.js +180 -0
- package/extension/src/shared/reviewing.js +99 -0
- package/extension/src/shared/sensitiveScan.js +116 -0
- package/extension/src/shared/sessionState.js +1084 -0
- package/extension/src/shared/staleGuard.js +150 -0
- package/extension/src/shared/storageDb.js +986 -0
- package/extension/src/shared/storageKeys.js +29 -0
- package/extension/src/shared/storageMigration.js +168 -0
- package/extension/src/shared/summary.js +248 -0
- package/extension/src/shared/undoOperations.js +369 -0
- package/native-host/src/codexArgs.js +43 -0
- package/native-host/src/codexHome.js +538 -0
- package/native-host/src/codexModels.js +247 -0
- package/native-host/src/codexPrompt.js +192 -0
- package/native-host/src/codexPromptAssembly.js +411 -0
- package/native-host/src/codexSessionRunner.js +1247 -0
- package/native-host/src/commandApproval.js +914 -0
- package/native-host/src/debugLog.js +78 -0
- package/native-host/src/diffEngine.js +247 -0
- package/native-host/src/index.js +132 -0
- package/native-host/src/launcher.js +81 -0
- package/native-host/src/localSkills.js +476 -0
- package/native-host/src/manifest.js +226 -0
- package/native-host/src/mirrorSensitiveScan.js +119 -0
- package/native-host/src/mirrorWorkspace.js +1019 -0
- package/native-host/src/nativeDoctor.js +826 -0
- package/native-host/src/nativeEnvironment.js +315 -0
- package/native-host/src/nativeHostPlatform.js +112 -0
- package/native-host/src/nativeMessaging.js +60 -0
- package/native-host/src/nativeQuotas.js +294 -0
- package/native-host/src/nativeResponseBudget.js +194 -0
- package/native-host/src/runtimeInstaller.js +357 -0
- package/native-host/src/taskRunner.js +3 -0
- package/native-host/src/taskRunnerRuntime.js +1083 -0
- package/native-host/src/textPatch.js +287 -0
- package/package.json +40 -0
- package/scripts/codex-json-agent.mjs +269 -0
- package/scripts/install-native-host.mjs +255 -0
- package/scripts/npm-package-files-v1.1.1.txt +52 -0
- package/scripts/uninstall-native-host.mjs +298 -0
- package/scripts/verify-npm-package.mjs +296 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate whether a skill-installer command is safe to execute.
|
|
8
|
+
* @param {Object} command
|
|
9
|
+
* @param {string} command.executable - The command executable (e.g., 'git').
|
|
10
|
+
* @param {Array<string>} command.args - Command arguments.
|
|
11
|
+
* @param {string} command.cwd - Working directory for the command.
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} options.skillsRoot - Allowed write root for skill installation.
|
|
14
|
+
* @returns {{ approved: boolean, reason: string, category: 'read-only'|'contained-write'|'blocked' }}
|
|
15
|
+
*/
|
|
16
|
+
function evaluateSkillCommand(command = {}, options = {}) {
|
|
17
|
+
const normalized = normalizeSkillCommand(command);
|
|
18
|
+
const context = normalizeApprovalOptions(options, normalized.cwd);
|
|
19
|
+
if (!normalized.tokens.length || normalized.tokens.some(isUnsafeShellToken)) {
|
|
20
|
+
return blocked('Skill installation commands must be recognized and write only under Codex Overleaf skill roots.');
|
|
21
|
+
}
|
|
22
|
+
if (normalized.rawString && hasUnsupportedShellSyntax(normalized.rawString)) {
|
|
23
|
+
return blocked('Skill installation command uses unsupported shell syntax.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const executable = pathBasename(normalized.tokens[0]);
|
|
27
|
+
if (['bash', 'sh', 'zsh'].includes(executable)) {
|
|
28
|
+
const inline = extractShellInlineCommand(normalized.tokens);
|
|
29
|
+
return inline
|
|
30
|
+
? evaluateSkillCommand({ command: inline, cwd: normalized.cwd }, context)
|
|
31
|
+
: blocked('Shell wrappers are allowed only for a single inline command.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isAllowedInstallerInspectionCommand(executable, normalized.tokens.slice(1), context)) {
|
|
35
|
+
return {
|
|
36
|
+
approved: true,
|
|
37
|
+
reason: 'Read-only inspection command is contained within allowed skill roots.',
|
|
38
|
+
category: 'read-only'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (executable === 'git') {
|
|
43
|
+
const gitClone = evaluateGitCloneInstallerCommand(normalized.tokens, context);
|
|
44
|
+
if (gitClone.approved) {
|
|
45
|
+
return gitClone;
|
|
46
|
+
}
|
|
47
|
+
return blocked(gitClone.reason);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return blocked('Skill installation commands must be recognized and write only under Codex Overleaf skill roots.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a URL is a safe HTTPS git clone target.
|
|
55
|
+
* @param {string} url
|
|
56
|
+
* @returns {{ safe: boolean, reason: string }}
|
|
57
|
+
*/
|
|
58
|
+
function validateGitCloneUrl(url) {
|
|
59
|
+
const text = String(url || '').trim();
|
|
60
|
+
if (!text) {
|
|
61
|
+
return { safe: false, reason: 'Git clone URL is empty.' };
|
|
62
|
+
}
|
|
63
|
+
if (/^ext::/i.test(text)) {
|
|
64
|
+
return { safe: false, reason: 'Git ext transport is not allowed.' };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const parsed = new URL(text);
|
|
68
|
+
if (parsed.protocol !== 'https:') {
|
|
69
|
+
return { safe: false, reason: 'Git clone URL must use HTTPS.' };
|
|
70
|
+
}
|
|
71
|
+
if (!parsed.hostname) {
|
|
72
|
+
return { safe: false, reason: 'Git clone URL must include a hostname.' };
|
|
73
|
+
}
|
|
74
|
+
return { safe: true, reason: 'Git clone URL uses HTTPS.' };
|
|
75
|
+
} catch {
|
|
76
|
+
return { safe: false, reason: 'Git clone URL must be an absolute HTTPS URL.' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeApprovalOptions(options = {}, commandCwd = '') {
|
|
81
|
+
const env = options.env || process.env;
|
|
82
|
+
const skillsRoot = path.resolve(String(options.skillsRoot || path.join(String(env.CODEX_HOME || ''), 'skills') || ''));
|
|
83
|
+
const workspacePath = String(options.workspacePath || commandCwd || '').trim();
|
|
84
|
+
const codexHomeSkillsRoot = path.join(String(env.CODEX_HOME || ''), 'skills');
|
|
85
|
+
return {
|
|
86
|
+
...options,
|
|
87
|
+
env,
|
|
88
|
+
skillsRoot,
|
|
89
|
+
workspacePath,
|
|
90
|
+
cwd: commandCwd || workspacePath,
|
|
91
|
+
readRoots: Array.from(new Set([
|
|
92
|
+
workspacePath,
|
|
93
|
+
commandCwd,
|
|
94
|
+
skillsRoot,
|
|
95
|
+
codexHomeSkillsRoot
|
|
96
|
+
].filter(root => root && path.isAbsolute(root)).map(root => path.resolve(root)))),
|
|
97
|
+
writeRoots: Array.from(new Set([
|
|
98
|
+
skillsRoot,
|
|
99
|
+
codexHomeSkillsRoot
|
|
100
|
+
].filter(root => root && path.isAbsolute(root)).map(root => path.resolve(root))))
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeSkillCommand(command = {}) {
|
|
105
|
+
const raw = extractCommandValue(command);
|
|
106
|
+
if (Array.isArray(raw)) {
|
|
107
|
+
return {
|
|
108
|
+
tokens: raw.map(String),
|
|
109
|
+
cwd: String(command.cwd || '').trim(),
|
|
110
|
+
rawString: ''
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
114
|
+
return {
|
|
115
|
+
tokens: tokenizeShellCommand(raw),
|
|
116
|
+
cwd: String(command.cwd || '').trim(),
|
|
117
|
+
rawString: raw
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const executable = String(command.executable || '').trim();
|
|
121
|
+
const args = Array.isArray(command.args) ? command.args.map(String) : [];
|
|
122
|
+
return {
|
|
123
|
+
tokens: executable ? [executable, ...args] : [],
|
|
124
|
+
cwd: String(command.cwd || '').trim(),
|
|
125
|
+
rawString: ''
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractCommandValue(params = {}) {
|
|
130
|
+
if (Array.isArray(params.command) || typeof params.command === 'string') {
|
|
131
|
+
return params.command;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(params.cmd) || typeof params.cmd === 'string') {
|
|
134
|
+
return params.cmd;
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(params.argv)) {
|
|
137
|
+
return params.argv;
|
|
138
|
+
}
|
|
139
|
+
if (typeof params.shellCommand === 'string') {
|
|
140
|
+
return params.shellCommand;
|
|
141
|
+
}
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isAllowedInstallerInspectionCommand(executable, args = [], context = {}) {
|
|
146
|
+
const allowed = new Set([
|
|
147
|
+
'rg', 'grep', 'cat', 'head', 'tail', 'nl', 'ls',
|
|
148
|
+
'wc', 'diff', 'sort', 'tr', 'cut', 'uniq',
|
|
149
|
+
'stat', 'file', 'basename', 'dirname', 'realpath',
|
|
150
|
+
'shasum', 'md5', 'md5sum'
|
|
151
|
+
]);
|
|
152
|
+
return allowed.has(executable)
|
|
153
|
+
&& !hasDisallowedInstallerInspectionArguments(executable, args)
|
|
154
|
+
&& areInstallerInspectionReadPathsContained(executable, args, context);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hasDisallowedInstallerInspectionArguments(executable, args = []) {
|
|
158
|
+
if (hasDisallowedCommandArguments(executable, args)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const flags = args.map(String);
|
|
162
|
+
if (executable === 'sort') {
|
|
163
|
+
return flags.some((flag, index) => flag === '-o'
|
|
164
|
+
|| flag.startsWith('-o')
|
|
165
|
+
|| flags[index - 1] === '-o'
|
|
166
|
+
|| flag === '--output'
|
|
167
|
+
|| flag.startsWith('--output=')
|
|
168
|
+
|| flags[index - 1] === '--output');
|
|
169
|
+
}
|
|
170
|
+
if (executable === 'rg') {
|
|
171
|
+
return flags.some(flag => flag === '--pre' || flag.startsWith('--pre='));
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function areInstallerInspectionReadPathsContained(executable, args = [], context = {}) {
|
|
177
|
+
const parsed = parseInstallerInspectionReadPaths(executable, args);
|
|
178
|
+
return parsed.valid
|
|
179
|
+
&& parsed.paths.every(target => isInstallerReadPathInsideAllowedRoot(target, context));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseInstallerInspectionReadPaths(executable, args = []) {
|
|
183
|
+
if (executable === 'tr') {
|
|
184
|
+
return { valid: true, paths: [] };
|
|
185
|
+
}
|
|
186
|
+
if (executable === 'rg' || executable === 'grep') {
|
|
187
|
+
return parseSearchInspectionReadPaths(executable, args);
|
|
188
|
+
}
|
|
189
|
+
const parsed = collectInstallerInspectionArguments(executable, args);
|
|
190
|
+
return parsed.valid
|
|
191
|
+
? { valid: true, paths: parsed.optionPathValues.concat(parsed.positionals) }
|
|
192
|
+
: { valid: false, paths: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseSearchInspectionReadPaths(executable, args = []) {
|
|
196
|
+
const parsed = collectInstallerInspectionArguments(executable, args);
|
|
197
|
+
if (!parsed.valid) {
|
|
198
|
+
return { valid: false, paths: [] };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const paths = [...parsed.optionPathValues];
|
|
202
|
+
if (parsed.noPatternMode || parsed.usesPatternOption) {
|
|
203
|
+
paths.push(...parsed.positionals);
|
|
204
|
+
} else if (parsed.positionals.length > 1) {
|
|
205
|
+
paths.push(...parsed.positionals.slice(1));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { valid: true, paths };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function collectInstallerInspectionArguments(executable, args = []) {
|
|
212
|
+
const spec = getInstallerInspectionOptionSpec(executable);
|
|
213
|
+
const result = {
|
|
214
|
+
valid: true,
|
|
215
|
+
positionals: [],
|
|
216
|
+
optionPathValues: [],
|
|
217
|
+
usesPatternOption: false,
|
|
218
|
+
noPatternMode: false
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
222
|
+
const token = String(args[index] || '');
|
|
223
|
+
if (!token) {
|
|
224
|
+
return { ...result, valid: false };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (token === '--') {
|
|
228
|
+
result.positionals.push(...args.slice(index + 1).map(String));
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (token !== '-' && token.startsWith('--')) {
|
|
233
|
+
const handled = collectLongInstallerInspectionOption(token, args, index, spec, result);
|
|
234
|
+
if (!handled.valid) {
|
|
235
|
+
return { ...result, valid: false };
|
|
236
|
+
}
|
|
237
|
+
if (handled.consumed) {
|
|
238
|
+
index += handled.consumed;
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (token !== '-' && token.startsWith('-')) {
|
|
244
|
+
const handled = collectShortInstallerInspectionOption(token, args, index, spec, result);
|
|
245
|
+
if (!handled.valid) {
|
|
246
|
+
return { ...result, valid: false };
|
|
247
|
+
}
|
|
248
|
+
if (handled.consumed) {
|
|
249
|
+
index += handled.consumed;
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
result.positionals.push(token);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function collectLongInstallerInspectionOption(token, args, index, spec, result) {
|
|
261
|
+
const equalsIndex = token.indexOf('=');
|
|
262
|
+
const name = equalsIndex >= 0 ? token.slice(0, equalsIndex) : token;
|
|
263
|
+
const inlineValue = equalsIndex >= 0 ? token.slice(equalsIndex + 1) : null;
|
|
264
|
+
if (spec.noPatternModeOptions.has(name)) {
|
|
265
|
+
result.noPatternMode = true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (inlineValue !== null) {
|
|
269
|
+
collectInstallerInspectionOptionValue(name, inlineValue, spec, result);
|
|
270
|
+
return { valid: true, consumed: 0 };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (optionRequiresValue(name, spec)) {
|
|
274
|
+
if (index + 1 >= args.length) {
|
|
275
|
+
return { valid: false, consumed: 0 };
|
|
276
|
+
}
|
|
277
|
+
collectInstallerInspectionOptionValue(name, String(args[index + 1] || ''), spec, result);
|
|
278
|
+
return { valid: true, consumed: 1 };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { valid: true, consumed: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function collectShortInstallerInspectionOption(token, args, index, spec, result) {
|
|
285
|
+
const attached = findAttachedShortInstallerInspectionOption(token, spec);
|
|
286
|
+
if (attached) {
|
|
287
|
+
collectInstallerInspectionOptionValue(attached.name, attached.value, spec, result);
|
|
288
|
+
return { valid: true, consumed: 0 };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (spec.noPatternModeOptions.has(token)) {
|
|
292
|
+
result.noPatternMode = true;
|
|
293
|
+
}
|
|
294
|
+
if (optionRequiresValue(token, spec)) {
|
|
295
|
+
if (index + 1 >= args.length) {
|
|
296
|
+
return { valid: false, consumed: 0 };
|
|
297
|
+
}
|
|
298
|
+
collectInstallerInspectionOptionValue(token, String(args[index + 1] || ''), spec, result);
|
|
299
|
+
return { valid: true, consumed: 1 };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { valid: true, consumed: 0 };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function findAttachedShortInstallerInspectionOption(token, spec) {
|
|
306
|
+
const options = [
|
|
307
|
+
...spec.pathValueOptions,
|
|
308
|
+
...spec.patternValueOptions,
|
|
309
|
+
...spec.valueOptions
|
|
310
|
+
].filter(option => /^-[A-Za-z]$/.test(option));
|
|
311
|
+
const option = options.find(candidate => token.startsWith(candidate) && token.length > candidate.length);
|
|
312
|
+
return option ? { name: option, value: token.slice(option.length) } : null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function collectInstallerInspectionOptionValue(name, value, spec, result) {
|
|
316
|
+
if (spec.pathValueOptions.has(name)) {
|
|
317
|
+
result.optionPathValues.push(value);
|
|
318
|
+
}
|
|
319
|
+
if (spec.patternValueOptions.has(name) || spec.patternPathValueOptions.has(name)) {
|
|
320
|
+
result.usesPatternOption = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function optionRequiresValue(name, spec) {
|
|
325
|
+
return spec.pathValueOptions.has(name)
|
|
326
|
+
|| spec.patternValueOptions.has(name)
|
|
327
|
+
|| spec.valueOptions.has(name);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getInstallerInspectionOptionSpec(executable) {
|
|
331
|
+
if (executable === 'rg') {
|
|
332
|
+
return buildInstallerInspectionOptionSpec({
|
|
333
|
+
pathValueOptions: ['-f', '--file', '--ignore-file'],
|
|
334
|
+
patternPathValueOptions: ['-f', '--file'],
|
|
335
|
+
patternValueOptions: ['-e', '--regexp'],
|
|
336
|
+
valueOptions: [
|
|
337
|
+
'-A', '--after-context', '-B', '--before-context', '-C', '--context',
|
|
338
|
+
'-g', '--glob', '--iglob', '-j', '--threads', '-m', '--max-count',
|
|
339
|
+
'-r', '--replace', '-t', '--type', '-T', '--type-not',
|
|
340
|
+
'--color', '--colors', '--context-separator', '--encoding', '--engine',
|
|
341
|
+
'--field-context-separator', '--field-match-separator', '--filter',
|
|
342
|
+
'--max-depth', '--max-filesize', '--path-separator', '--pre-glob',
|
|
343
|
+
'--sort', '--sortr'
|
|
344
|
+
],
|
|
345
|
+
noPatternModeOptions: ['--files']
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (executable === 'grep') {
|
|
349
|
+
return buildInstallerInspectionOptionSpec({
|
|
350
|
+
pathValueOptions: ['-f', '--file', '--exclude-from'],
|
|
351
|
+
patternPathValueOptions: ['-f', '--file'],
|
|
352
|
+
patternValueOptions: ['-e', '--regexp'],
|
|
353
|
+
valueOptions: [
|
|
354
|
+
'-A', '--after-context', '-B', '--before-context', '-C', '--context',
|
|
355
|
+
'-D', '--devices', '-d', '--directories', '-m', '--max-count',
|
|
356
|
+
'--binary-files', '--exclude', '--group-separator', '--include', '--label'
|
|
357
|
+
]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (executable === 'head' || executable === 'tail') {
|
|
361
|
+
return buildInstallerInspectionOptionSpec({
|
|
362
|
+
valueOptions: ['-c', '--bytes', '-n', '--lines']
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (executable === 'cut') {
|
|
366
|
+
return buildInstallerInspectionOptionSpec({
|
|
367
|
+
valueOptions: [
|
|
368
|
+
'-b', '--bytes', '-c', '--characters', '-d', '--delimiter',
|
|
369
|
+
'-f', '--fields', '--output-delimiter'
|
|
370
|
+
]
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (executable === 'sort') {
|
|
374
|
+
return buildInstallerInspectionOptionSpec({
|
|
375
|
+
pathValueOptions: ['-T', '--temporary-directory'],
|
|
376
|
+
valueOptions: ['-k', '--key', '-S', '--buffer-size', '-t', '--field-separator']
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (executable === 'shasum') {
|
|
380
|
+
return buildInstallerInspectionOptionSpec({
|
|
381
|
+
valueOptions: ['-a', '--algorithm']
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return buildInstallerInspectionOptionSpec();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildInstallerInspectionOptionSpec(input = {}) {
|
|
388
|
+
return {
|
|
389
|
+
pathValueOptions: new Set(input.pathValueOptions || []),
|
|
390
|
+
patternPathValueOptions: new Set(input.patternPathValueOptions || []),
|
|
391
|
+
patternValueOptions: new Set(input.patternValueOptions || []),
|
|
392
|
+
valueOptions: new Set(input.valueOptions || []),
|
|
393
|
+
noPatternModeOptions: new Set(input.noPatternModeOptions || [])
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function evaluateGitCloneInstallerCommand(tokens = [], context = {}) {
|
|
398
|
+
const parsed = parseGitCloneInstallerCommand(tokens, context.cwd || context.workspacePath);
|
|
399
|
+
if (!parsed) {
|
|
400
|
+
return {
|
|
401
|
+
approved: false,
|
|
402
|
+
reason: 'Only contained git clone commands are allowed for skill installation.',
|
|
403
|
+
category: 'blocked'
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const url = validateGitCloneUrl(parsed.url);
|
|
407
|
+
if (!url.safe) {
|
|
408
|
+
return {
|
|
409
|
+
approved: false,
|
|
410
|
+
reason: url.reason,
|
|
411
|
+
category: 'blocked'
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (!parsed.writeTargets.length || !parsed.writeTargets.every(target => isInstallerPathInsideAllowedSkillRoot(target, context))) {
|
|
415
|
+
return {
|
|
416
|
+
approved: false,
|
|
417
|
+
reason: 'Git clone destination must stay inside the Codex Overleaf skill root.',
|
|
418
|
+
category: 'blocked'
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
approved: true,
|
|
423
|
+
reason: 'HTTPS git clone destination is contained inside the Codex Overleaf skill root.',
|
|
424
|
+
category: 'contained-write'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function parseGitCloneInstallerCommand(tokens = [], cwd = '') {
|
|
429
|
+
if (tokens[1] !== 'clone') {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
const writeTargets = [];
|
|
433
|
+
const positionals = [];
|
|
434
|
+
for (let index = 2; index < tokens.length; index += 1) {
|
|
435
|
+
const token = String(tokens[index] || '');
|
|
436
|
+
if (!token) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (token === '--') {
|
|
441
|
+
for (let positionalIndex = index + 1; positionalIndex < tokens.length; positionalIndex += 1) {
|
|
442
|
+
positionals.push(String(tokens[positionalIndex] || ''));
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (isDisallowedGitCloneOption(token)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const separateGitDir = token.match(/^--separate-git-dir=(.+)$/);
|
|
452
|
+
if (separateGitDir) {
|
|
453
|
+
writeTargets.push(separateGitDir[1]);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (token === '--separate-git-dir') {
|
|
457
|
+
if (index + 1 >= tokens.length) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
writeTargets.push(tokens[index + 1]);
|
|
461
|
+
index += 1;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (isAllowedGitCloneBooleanOption(token)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const inlineOption = parseAllowedGitCloneInlineOption(token);
|
|
468
|
+
if (inlineOption) {
|
|
469
|
+
if (!isAllowedGitCloneOptionValue(inlineOption.name, inlineOption.value)) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (isAllowedGitCloneValueOption(token)) {
|
|
475
|
+
if (index + 1 >= tokens.length || !isAllowedGitCloneOptionValue(token, tokens[index + 1])) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
index += 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (token.startsWith('-')) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
positionals.push(token);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (positionals.length < 1 || positionals.length > 2 || !positionals.every(Boolean)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (positionals.length === 2) {
|
|
492
|
+
writeTargets.push(positionals[1]);
|
|
493
|
+
} else {
|
|
494
|
+
writeTargets.push(cwd || '.');
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
url: positionals[0],
|
|
498
|
+
writeTargets
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isDisallowedGitCloneOption(token) {
|
|
503
|
+
return token === '-c'
|
|
504
|
+
|| token.startsWith('-c')
|
|
505
|
+
|| token === '--config'
|
|
506
|
+
|| token.startsWith('--config=')
|
|
507
|
+
|| token === '--upload-pack'
|
|
508
|
+
|| token.startsWith('--upload-pack=')
|
|
509
|
+
|| token === '-u'
|
|
510
|
+
|| token.startsWith('-u');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isAllowedGitCloneBooleanOption(token) {
|
|
514
|
+
return new Set([
|
|
515
|
+
'--quiet',
|
|
516
|
+
'-q',
|
|
517
|
+
'--verbose',
|
|
518
|
+
'-v',
|
|
519
|
+
'--progress',
|
|
520
|
+
'--no-checkout',
|
|
521
|
+
'-n',
|
|
522
|
+
'--bare',
|
|
523
|
+
'--mirror',
|
|
524
|
+
'--single-branch',
|
|
525
|
+
'--no-single-branch',
|
|
526
|
+
'--no-tags'
|
|
527
|
+
]).has(token);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function parseAllowedGitCloneInlineOption(token) {
|
|
531
|
+
const match = String(token || '').match(/^(--depth|--branch|--filter|--origin)=(.+)$/);
|
|
532
|
+
return match ? { name: match[1], value: match[2] } : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isAllowedGitCloneValueOption(token) {
|
|
536
|
+
return new Set(['--depth', '--branch', '-b', '--filter', '--origin', '-o']).has(token);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isAllowedGitCloneOptionValue(option, value) {
|
|
540
|
+
const text = String(value || '');
|
|
541
|
+
if (!text || text.startsWith('-') || /[\0\r\n]/.test(text)) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
if (option === '--depth') {
|
|
545
|
+
return /^[1-9][0-9]{0,5}$/.test(text);
|
|
546
|
+
}
|
|
547
|
+
if (option === '--branch' || option === '-b') {
|
|
548
|
+
return isSafeGitRefName(text);
|
|
549
|
+
}
|
|
550
|
+
if (option === '--filter') {
|
|
551
|
+
return /^(blob:none|tree:[0-9]+)$/.test(text);
|
|
552
|
+
}
|
|
553
|
+
if (option === '--origin' || option === '-o') {
|
|
554
|
+
return /^[A-Za-z0-9._-]{1,64}$/.test(text) && !text.includes('..');
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isSafeGitRefName(value) {
|
|
560
|
+
const text = String(value || '');
|
|
561
|
+
return text.length <= 200
|
|
562
|
+
&& !text.includes('..')
|
|
563
|
+
&& !text.includes('//')
|
|
564
|
+
&& !text.includes('@{')
|
|
565
|
+
&& !text.endsWith('.')
|
|
566
|
+
&& !/[\\\s~^:?*[\]\0\r\n]/.test(text);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function isInstallerReadPathInsideAllowedRoot(value, context = {}) {
|
|
570
|
+
if (!isReadablePathArgument(value)) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
const expanded = expandInstallerPath(value, context);
|
|
574
|
+
return Boolean(expanded) && isInsideAllowedInstallerReadRoot(expanded, context);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function isReadablePathArgument(value) {
|
|
578
|
+
const text = String(value || '').trim();
|
|
579
|
+
return Boolean(text) && text !== '-';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isInstallerPathInsideAllowedSkillRoot(value, context = {}) {
|
|
583
|
+
const expanded = expandInstallerPath(value, context);
|
|
584
|
+
return Boolean(expanded) && isInsideAllowedSkillWriteRoot(expanded, context);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function expandInstallerPath(value, context = {}) {
|
|
588
|
+
const text = String(value || '').trim();
|
|
589
|
+
if (!text || isUrlLike(text)) {
|
|
590
|
+
return '';
|
|
591
|
+
}
|
|
592
|
+
let expanded = text;
|
|
593
|
+
const env = context.env || process.env;
|
|
594
|
+
if (expanded === '~' || expanded.startsWith('~/')) {
|
|
595
|
+
const home = String(env.HOME || '');
|
|
596
|
+
if (!home || !path.isAbsolute(home)) {
|
|
597
|
+
return '';
|
|
598
|
+
}
|
|
599
|
+
expanded = expanded === '~' ? home : path.join(home, expanded.slice(2));
|
|
600
|
+
} else if (expanded.startsWith('~')) {
|
|
601
|
+
return '';
|
|
602
|
+
}
|
|
603
|
+
expanded = expandInstallerEnvironmentVariables(expanded, env);
|
|
604
|
+
if (expanded.includes('$')) {
|
|
605
|
+
return '';
|
|
606
|
+
}
|
|
607
|
+
return path.isAbsolute(expanded)
|
|
608
|
+
? path.resolve(expanded)
|
|
609
|
+
: path.resolve(context.cwd || context.workspacePath || process.cwd(), expanded);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function expandInstallerEnvironmentVariables(value, env = process.env) {
|
|
613
|
+
return String(value || '').replace(
|
|
614
|
+
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
615
|
+
(_, bracedName, bareName) => String(env[bracedName || bareName] || '')
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function isInsideAllowedInstallerReadRoot(target, context = {}) {
|
|
620
|
+
try {
|
|
621
|
+
const approvedRootSymlinkTargets = getApprovedSkillRootSymlinkTargets(context);
|
|
622
|
+
const roots = context.readRoots || [];
|
|
623
|
+
return roots.some(root => isSafeContainedReadTarget(target, root, { approvedRootSymlinkTargets }));
|
|
624
|
+
} catch {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function isInsideAllowedSkillWriteRoot(target, context = {}) {
|
|
630
|
+
try {
|
|
631
|
+
const approvedRootSymlinkTargets = getApprovedSkillRootSymlinkTargets(context);
|
|
632
|
+
const roots = context.writeRoots || [];
|
|
633
|
+
return roots.some(root => isSafeContainedWriteTarget(target, root, { approvedRootSymlinkTargets }));
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function getApprovedSkillRootSymlinkTargets(context = {}) {
|
|
640
|
+
const targets = new Set();
|
|
641
|
+
const realRoot = safeRealpathNonSymlinkDirectory(context.skillsRoot);
|
|
642
|
+
if (realRoot) {
|
|
643
|
+
targets.add(realRoot);
|
|
644
|
+
}
|
|
645
|
+
return targets;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function isSafeContainedReadTarget(target, root, options = {}) {
|
|
649
|
+
const resolvedTarget = path.resolve(String(target || ''));
|
|
650
|
+
const resolvedRoot = path.resolve(String(root || ''));
|
|
651
|
+
if (!isLexicallyInsideOrSame(resolvedTarget, resolvedRoot)) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const rootExists = fs.existsSync(resolvedRoot);
|
|
656
|
+
const rootReal = safeRealpathDirectory(resolvedRoot, options.approvedRootSymlinkTargets);
|
|
657
|
+
if (rootExists && !rootReal) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
const relativeParts = path.relative(resolvedRoot, resolvedTarget).split(path.sep).filter(Boolean);
|
|
661
|
+
let current = resolvedRoot;
|
|
662
|
+
if (!rootExists) {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
for (let index = 0; index < relativeParts.length; index += 1) {
|
|
667
|
+
current = path.join(current, relativeParts[index]);
|
|
668
|
+
if (!fs.existsSync(current)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
const isFinalPart = index === relativeParts.length - 1;
|
|
672
|
+
const safe = isFinalPart
|
|
673
|
+
? isSafeExistingReadPath(current, rootReal)
|
|
674
|
+
: isSafeExistingDirectory(current, rootReal);
|
|
675
|
+
if (!safe) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function safeRealpathNonSymlinkDirectory(target) {
|
|
683
|
+
try {
|
|
684
|
+
const stat = fs.lstatSync(target);
|
|
685
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
686
|
+
return '';
|
|
687
|
+
}
|
|
688
|
+
return fs.realpathSync.native(target);
|
|
689
|
+
} catch {
|
|
690
|
+
return '';
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function isSafeContainedWriteTarget(target, root, options = {}) {
|
|
695
|
+
const resolvedTarget = path.resolve(String(target || ''));
|
|
696
|
+
const resolvedRoot = path.resolve(String(root || ''));
|
|
697
|
+
if (!isLexicallyInsideOrSame(resolvedTarget, resolvedRoot)) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const rootReal = safeRealpathDirectory(resolvedRoot, options.approvedRootSymlinkTargets);
|
|
702
|
+
const relativeParts = path.relative(resolvedRoot, resolvedTarget).split(path.sep).filter(Boolean);
|
|
703
|
+
let current = resolvedRoot;
|
|
704
|
+
if (!isSafeExistingDirectory(current, rootReal)) {
|
|
705
|
+
return !fs.existsSync(current);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
for (const part of relativeParts) {
|
|
709
|
+
current = path.join(current, part);
|
|
710
|
+
if (!fs.existsSync(current)) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (!isSafeExistingDirectory(current, rootReal)) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function isSafeExistingReadPath(target, rootReal) {
|
|
721
|
+
try {
|
|
722
|
+
const stat = fs.lstatSync(target);
|
|
723
|
+
if (stat.isSymbolicLink()) {
|
|
724
|
+
if (!rootReal) {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
const realTarget = fs.realpathSync.native(target);
|
|
728
|
+
return isLexicallyInsideOrSame(realTarget, rootReal);
|
|
729
|
+
}
|
|
730
|
+
if (!rootReal) {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
const realTarget = fs.realpathSync.native(target);
|
|
734
|
+
return isLexicallyInsideOrSame(realTarget, rootReal);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return error.code === 'ENOENT';
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function safeRealpathDirectory(target, approvedRootSymlinkTargets = new Set()) {
|
|
741
|
+
try {
|
|
742
|
+
const stat = fs.lstatSync(target);
|
|
743
|
+
if (stat.isSymbolicLink()) {
|
|
744
|
+
const realTarget = fs.realpathSync.native(target);
|
|
745
|
+
return fs.statSync(realTarget).isDirectory() && approvedRootSymlinkTargets.has(realTarget)
|
|
746
|
+
? realTarget
|
|
747
|
+
: '';
|
|
748
|
+
}
|
|
749
|
+
if (!stat.isDirectory()) {
|
|
750
|
+
return '';
|
|
751
|
+
}
|
|
752
|
+
return fs.realpathSync.native(target);
|
|
753
|
+
} catch {
|
|
754
|
+
return '';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function isSafeExistingDirectory(target, rootReal) {
|
|
759
|
+
try {
|
|
760
|
+
const stat = fs.lstatSync(target);
|
|
761
|
+
if (stat.isSymbolicLink()) {
|
|
762
|
+
if (!rootReal) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
const realTarget = fs.realpathSync.native(target);
|
|
766
|
+
return isLexicallyInsideOrSame(realTarget, rootReal);
|
|
767
|
+
}
|
|
768
|
+
if (!stat.isDirectory()) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
if (!rootReal) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
const realTarget = fs.realpathSync.native(target);
|
|
775
|
+
return isLexicallyInsideOrSame(realTarget, rootReal);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
return error.code === 'ENOENT';
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function isLexicallyInsideOrSame(target, root) {
|
|
782
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
783
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function isUrlLike(value) {
|
|
787
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(String(value || ''));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function hasUnsupportedShellSyntax(command) {
|
|
791
|
+
return hasAmbiguousShellEscape(command) || hasUnbalancedShellQuote(command);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function hasAmbiguousShellEscape(command) {
|
|
795
|
+
return /\\["';&|<>`$(){}\n\r]/.test(command);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function hasUnbalancedShellQuote(command) {
|
|
799
|
+
let quote = '';
|
|
800
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
801
|
+
const char = command[index];
|
|
802
|
+
if (char === '\\' && quote !== "'") {
|
|
803
|
+
index += 1;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (quote) {
|
|
807
|
+
if (char === quote) {
|
|
808
|
+
quote = '';
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (char === '"' || char === "'") {
|
|
813
|
+
quote = char;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return Boolean(quote);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function isUnsafeShellToken(token) {
|
|
820
|
+
return ['&&', '||', ';', '|', '>', '>>', '<', '<<', '`'].includes(token)
|
|
821
|
+
|| /\$\(/.test(token);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function hasDisallowedCommandArguments(executable, args = []) {
|
|
825
|
+
const flags = args.map(String);
|
|
826
|
+
if (executable === 'find') {
|
|
827
|
+
return flags.some(flag => ['-exec', '-execdir', '-delete', '-ok', '-okdir'].includes(flag));
|
|
828
|
+
}
|
|
829
|
+
if (executable === 'sed') {
|
|
830
|
+
return flags.some(flag => flag === '-i' || /^-i[^a-zA-Z0-9]?/.test(flag));
|
|
831
|
+
}
|
|
832
|
+
if (executable === 'awk') {
|
|
833
|
+
return flags.some((flag, index) => flag === '-i' && flags[index + 1] === 'inplace');
|
|
834
|
+
}
|
|
835
|
+
if (executable === 'shasum' || executable === 'md5sum') {
|
|
836
|
+
return flags.some(flag => flag === '-c' || flag === '--check');
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function pathBasename(value) {
|
|
842
|
+
return String(value || '').split(/[\\/]/).pop();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function extractShellInlineCommand(tokens = []) {
|
|
846
|
+
const index = tokens.findIndex(token => token === '-c' || token === '-lc' || token === '-ilc');
|
|
847
|
+
if (index < 0 || index + 1 >= tokens.length || tokens.length !== index + 2) {
|
|
848
|
+
return '';
|
|
849
|
+
}
|
|
850
|
+
return tokens[index + 1];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function tokenizeShellCommand(command) {
|
|
854
|
+
const tokens = [];
|
|
855
|
+
let current = '';
|
|
856
|
+
let quote = '';
|
|
857
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
858
|
+
const char = command[index];
|
|
859
|
+
if (quote) {
|
|
860
|
+
if (char === quote) {
|
|
861
|
+
quote = '';
|
|
862
|
+
} else {
|
|
863
|
+
current += char;
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (char === '"' || char === "'") {
|
|
868
|
+
quote = char;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (/\s/.test(char)) {
|
|
872
|
+
if (current) {
|
|
873
|
+
tokens.push(current);
|
|
874
|
+
current = '';
|
|
875
|
+
}
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (char === '&' && command[index + 1] === '&') {
|
|
879
|
+
if (current) tokens.push(current);
|
|
880
|
+
tokens.push('&&');
|
|
881
|
+
current = '';
|
|
882
|
+
index += 1;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (char === '|' && command[index + 1] === '|') {
|
|
886
|
+
if (current) tokens.push(current);
|
|
887
|
+
tokens.push('||');
|
|
888
|
+
current = '';
|
|
889
|
+
index += 1;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (';|<>`'.includes(char)) {
|
|
893
|
+
if (current) tokens.push(current);
|
|
894
|
+
tokens.push(char);
|
|
895
|
+
current = '';
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
current += char;
|
|
899
|
+
}
|
|
900
|
+
if (current) {
|
|
901
|
+
tokens.push(current);
|
|
902
|
+
}
|
|
903
|
+
return tokens;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function blocked(reason) {
|
|
907
|
+
return {
|
|
908
|
+
approved: false,
|
|
909
|
+
reason,
|
|
910
|
+
category: 'blocked'
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
module.exports = { evaluateSkillCommand, validateGitCloneUrl };
|