invar-tools 1.16.0__py3-none-any.whl → 1.17.0__py3-none-any.whl
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.
- invar/core/format_strategies.py +3 -1
- invar/mcp/handlers.py +10 -1
- invar/node_tools/eslint-plugin/cli.js +55 -9
- invar/node_tools/fc-runner/bundle.js +77 -0
- invar/node_tools/fc-runner/cli.d.ts +12 -0
- invar/node_tools/fc-runner/cli.d.ts.map +1 -0
- invar/node_tools/fc-runner/cli.js +117 -45
- invar/node_tools/fc-runner/cli.js.map +1 -0
- invar/node_tools/fc-runner/index.d.ts +338 -0
- invar/node_tools/fc-runner/index.d.ts.map +1 -0
- invar/node_tools/fc-runner/index.js +260 -0
- invar/node_tools/fc-runner/index.js.map +1 -0
- invar/shell/prove/guard_ts.py +85 -5
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/METADATA +1 -1
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/RECORD +20 -12
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.16.0.dist-info → invar_tools-1.17.0.dist-info}/licenses/NOTICE +0 -0
invar/core/format_strategies.py
CHANGED
|
@@ -159,11 +159,13 @@ def text_with_pattern(
|
|
|
159
159
|
)
|
|
160
160
|
noise_lines = st.lists(noise_line, min_size=0, max_size=10)
|
|
161
161
|
|
|
162
|
+
# Note: Hypothesis will automatically explore different orderings of the lines,
|
|
163
|
+
# so explicit shuffling is unnecessary. The concatenation order is sufficient.
|
|
162
164
|
return st.builds(
|
|
163
165
|
lambda p, n: "\n".join(p + n),
|
|
164
166
|
pattern_lines,
|
|
165
167
|
noise_lines,
|
|
166
|
-
)
|
|
168
|
+
)
|
|
167
169
|
|
|
168
170
|
|
|
169
171
|
@pre(lambda pattern: len(pattern) > 0)
|
invar/mcp/handlers.py
CHANGED
|
@@ -24,6 +24,11 @@ def _validate_path(path: str) -> tuple[bool, str]:
|
|
|
24
24
|
|
|
25
25
|
Returns (is_valid, error_message).
|
|
26
26
|
Rejects paths that could be interpreted as shell commands or flags.
|
|
27
|
+
|
|
28
|
+
Note: This validation is for MCP (Model Context Protocol) handlers, which
|
|
29
|
+
are designed to provide AI agents with access to the project filesystem.
|
|
30
|
+
We validate format and reject shell injection patterns, but do not restrict
|
|
31
|
+
to working directory (unlike CLI tools) since MCP is a trusted local protocol.
|
|
27
32
|
"""
|
|
28
33
|
if not path:
|
|
29
34
|
return True, "" # Empty path defaults to "." in handlers
|
|
@@ -38,9 +43,13 @@ def _validate_path(path: str) -> tuple[bool, str]:
|
|
|
38
43
|
if char in path:
|
|
39
44
|
return False, f"Invalid path: contains forbidden character: {char!r}"
|
|
40
45
|
|
|
41
|
-
#
|
|
46
|
+
# Resolve path to canonical form, following symlinks
|
|
47
|
+
# This ensures path is valid and catches directory traversal attempts
|
|
42
48
|
try:
|
|
43
49
|
Path(path).resolve()
|
|
50
|
+
# Note: We don't restrict to cwd here because MCP handlers are designed
|
|
51
|
+
# to access the full project. If path restriction is needed, implement
|
|
52
|
+
# at the MCP server level, not per-handler.
|
|
44
53
|
except (OSError, ValueError) as e:
|
|
45
54
|
return False, f"Invalid path: {e}"
|
|
46
55
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { ESLint } from 'eslint';
|
|
17
17
|
import { resolve, dirname } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
+
import { statSync, realpathSync } from 'fs';
|
|
19
20
|
import plugin from './index.js';
|
|
20
21
|
// Get directory containing this CLI script (for resolving node_modules)
|
|
21
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -58,15 +59,32 @@ async function main() {
|
|
|
58
59
|
process.exit(0);
|
|
59
60
|
}
|
|
60
61
|
const projectPath = resolve(args.projectPath);
|
|
61
|
-
// Validate resolved path is within current working directory
|
|
62
|
+
// Validate resolved path is within current working directory
|
|
62
63
|
// This prevents path traversal attacks via "../../../etc/passwd" patterns
|
|
64
|
+
// and symlink-based bypasses (e.g., "./symlink_inside/../../../etc/passwd")
|
|
63
65
|
const cwd = process.cwd();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
try {
|
|
67
|
+
// Use realpath to resolve symlinks and prevent bypass attacks
|
|
68
|
+
const realProjectPath = realpathSync(projectPath);
|
|
69
|
+
const realCwd = realpathSync(cwd);
|
|
70
|
+
if (!realProjectPath.startsWith(realCwd)) {
|
|
71
|
+
console.error(`Error: Project path must be within current directory`);
|
|
72
|
+
console.error(` Requested: ${args.projectPath}`);
|
|
73
|
+
console.error(` Resolved: ${realProjectPath}`);
|
|
74
|
+
console.error(` Working dir: ${realCwd}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// If realpath fails (path doesn't exist), fall back to string comparison
|
|
80
|
+
// This allows error messages to be more specific
|
|
81
|
+
if (!projectPath.startsWith(cwd)) {
|
|
82
|
+
console.error(`Error: Project path must be within current directory`);
|
|
83
|
+
console.error(` Requested: ${args.projectPath}`);
|
|
84
|
+
console.error(` Resolved: ${projectPath}`);
|
|
85
|
+
console.error(` Working dir: ${cwd}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
70
88
|
}
|
|
71
89
|
try {
|
|
72
90
|
// Get the rules config for the selected mode
|
|
@@ -93,8 +111,36 @@ async function main() {
|
|
|
93
111
|
'@invar': plugin, // Register our plugin programmatically
|
|
94
112
|
},
|
|
95
113
|
}); // Type assertion for ESLint config complexity
|
|
96
|
-
// Lint the project
|
|
97
|
-
|
|
114
|
+
// Lint the project - detect if path is a file or directory
|
|
115
|
+
// ESLint defaults to .js only, so we need glob patterns for .ts/.tsx
|
|
116
|
+
let filesToLint;
|
|
117
|
+
try {
|
|
118
|
+
const stats = statSync(projectPath);
|
|
119
|
+
// Note: Advisory check for optimization - TOCTOU race condition is acceptable
|
|
120
|
+
// because ESLint will handle file system changes gracefully during actual linting
|
|
121
|
+
if (stats.isFile()) {
|
|
122
|
+
// Single file - lint it directly
|
|
123
|
+
filesToLint = [projectPath];
|
|
124
|
+
}
|
|
125
|
+
else if (stats.isDirectory()) {
|
|
126
|
+
// Directory - use glob patterns for TypeScript files primarily
|
|
127
|
+
// Note: Focus on TypeScript files as this is a TypeScript Guard tool
|
|
128
|
+
filesToLint = [
|
|
129
|
+
`${projectPath}/**/*.ts`,
|
|
130
|
+
`${projectPath}/**/*.tsx`,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.error(`Error: Path is neither a file nor a directory: ${projectPath}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
140
|
+
console.error(`Error: Cannot access path: ${errorMessage}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const results = await eslint.lintFiles(filesToLint);
|
|
98
144
|
// Output in standard ESLint JSON format (compatible with guard_ts.py)
|
|
99
145
|
const formatter = await eslint.loadFormatter('json');
|
|
100
146
|
const resultText = await Promise.resolve(formatter.format(results, {
|