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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -0
  3. package/bin/codex-overleaf-link.mjs +223 -0
  4. package/extension/src/shared/agentTranscript.js +1175 -0
  5. package/extension/src/shared/auditRecords.js +568 -0
  6. package/extension/src/shared/compatibility.js +372 -0
  7. package/extension/src/shared/compileAdapter.js +176 -0
  8. package/extension/src/shared/governanceRules.js +252 -0
  9. package/extension/src/shared/i18n.js +565 -0
  10. package/extension/src/shared/models.js +106 -0
  11. package/extension/src/shared/otText.js +505 -0
  12. package/extension/src/shared/projectFiles.js +180 -0
  13. package/extension/src/shared/reviewing.js +99 -0
  14. package/extension/src/shared/sensitiveScan.js +116 -0
  15. package/extension/src/shared/sessionState.js +1084 -0
  16. package/extension/src/shared/staleGuard.js +150 -0
  17. package/extension/src/shared/storageDb.js +986 -0
  18. package/extension/src/shared/storageKeys.js +29 -0
  19. package/extension/src/shared/storageMigration.js +168 -0
  20. package/extension/src/shared/summary.js +248 -0
  21. package/extension/src/shared/undoOperations.js +369 -0
  22. package/native-host/src/codexArgs.js +43 -0
  23. package/native-host/src/codexHome.js +538 -0
  24. package/native-host/src/codexModels.js +247 -0
  25. package/native-host/src/codexPrompt.js +192 -0
  26. package/native-host/src/codexPromptAssembly.js +411 -0
  27. package/native-host/src/codexSessionRunner.js +1247 -0
  28. package/native-host/src/commandApproval.js +914 -0
  29. package/native-host/src/debugLog.js +78 -0
  30. package/native-host/src/diffEngine.js +247 -0
  31. package/native-host/src/index.js +132 -0
  32. package/native-host/src/launcher.js +81 -0
  33. package/native-host/src/localSkills.js +476 -0
  34. package/native-host/src/manifest.js +226 -0
  35. package/native-host/src/mirrorSensitiveScan.js +119 -0
  36. package/native-host/src/mirrorWorkspace.js +1019 -0
  37. package/native-host/src/nativeDoctor.js +826 -0
  38. package/native-host/src/nativeEnvironment.js +315 -0
  39. package/native-host/src/nativeHostPlatform.js +112 -0
  40. package/native-host/src/nativeMessaging.js +60 -0
  41. package/native-host/src/nativeQuotas.js +294 -0
  42. package/native-host/src/nativeResponseBudget.js +194 -0
  43. package/native-host/src/runtimeInstaller.js +357 -0
  44. package/native-host/src/taskRunner.js +3 -0
  45. package/native-host/src/taskRunnerRuntime.js +1083 -0
  46. package/native-host/src/textPatch.js +287 -0
  47. package/package.json +40 -0
  48. package/scripts/codex-json-agent.mjs +269 -0
  49. package/scripts/install-native-host.mjs +255 -0
  50. package/scripts/npm-package-files-v1.1.1.txt +52 -0
  51. package/scripts/uninstall-native-host.mjs +298 -0
  52. 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 };