polyglot-code-guard 0.1.0__tar.gz
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.
- polyglot_code_guard-0.1.0/LICENSE +21 -0
- polyglot_code_guard-0.1.0/PKG-INFO +105 -0
- polyglot_code_guard-0.1.0/README.md +83 -0
- polyglot_code_guard-0.1.0/pyproject.toml +46 -0
- polyglot_code_guard-0.1.0/setup.cfg +4 -0
- polyglot_code_guard-0.1.0/src/polyglot_code_guard.egg-info/PKG-INFO +105 -0
- polyglot_code_guard-0.1.0/src/polyglot_code_guard.egg-info/SOURCES.txt +14 -0
- polyglot_code_guard-0.1.0/src/polyglot_code_guard.egg-info/dependency_links.txt +1 -0
- polyglot_code_guard-0.1.0/src/polyglot_code_guard.egg-info/entry_points.txt +2 -0
- polyglot_code_guard-0.1.0/src/polyglot_code_guard.egg-info/requires.txt +3 -0
- polyglot_code_guard-0.1.0/src/polyglot_code_guard.egg-info/top_level.txt +1 -0
- polyglot_code_guard-0.1.0/src/polyguard/SKILL.md +35 -0
- polyglot_code_guard-0.1.0/src/polyguard/__init__.py +3 -0
- polyglot_code_guard-0.1.0/src/polyguard/cli.py +34 -0
- polyglot_code_guard-0.1.0/src/polyguard/runner.py +271 -0
- polyglot_code_guard-0.1.0/tests/test_runner.py +74 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Furox-Art
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyglot-code-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fail-closed, multi-language test verification for AI-generated code.
|
|
5
|
+
Author: Furox-Art
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Furox-Art/polyglot-code-guard
|
|
8
|
+
Project-URL: Repository, https://github.com/Furox-Art/polyglot-code-guard
|
|
9
|
+
Project-URL: Issues, https://github.com/Furox-Art/polyglot-code-guard/issues
|
|
10
|
+
Keywords: testing,code-verification,polyglot,ai-generated-code,quality-gate
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Polyguard
|
|
24
|
+
|
|
25
|
+
**Fail-closed verification for AI-generated code across multiple programming languages.**
|
|
26
|
+
|
|
27
|
+
Polyguard runs the real build and test tools for the languages it detects. It returns **PASS** only when every detected language has a recognizable test suite and all configured commands succeed. Missing tests, missing toolchains, timeouts, compilation errors, and failing tests return **FAIL**.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
python -m pip install polyglot-code-guard
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
Run this inside the project directory:
|
|
36
|
+
|
|
37
|
+
polyguard verify
|
|
38
|
+
|
|
39
|
+
The normal verdict is intentionally one word:
|
|
40
|
+
|
|
41
|
+
PASS
|
|
42
|
+
|
|
43
|
+
or:
|
|
44
|
+
|
|
45
|
+
FAIL
|
|
46
|
+
|
|
47
|
+
For diagnostics:
|
|
48
|
+
|
|
49
|
+
polyguard verify --details
|
|
50
|
+
|
|
51
|
+
For automation:
|
|
52
|
+
|
|
53
|
+
polyguard verify --json
|
|
54
|
+
|
|
55
|
+
The process exits with code 0 for PASS and code 1 for FAIL.
|
|
56
|
+
|
|
57
|
+
## Initial language support
|
|
58
|
+
|
|
59
|
+
| Language | Test command |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Python | compileall, then pytest |
|
|
62
|
+
| Rust | cargo test --all-targets |
|
|
63
|
+
| Go | go test ./... |
|
|
64
|
+
| JavaScript / TypeScript | npm test; TypeScript projects also run tsc --noEmit |
|
|
65
|
+
| Java | Maven test or Gradle test |
|
|
66
|
+
| C# | dotnet test |
|
|
67
|
+
| PHP | PHPUnit |
|
|
68
|
+
| Ruby | RSpec or Rake test |
|
|
69
|
+
| Swift | swift test |
|
|
70
|
+
| C / C++ | CMake build and CTest when CTest is configured |
|
|
71
|
+
|
|
72
|
+
The project must contain a detectable test suite for each detected language. Polyguard deliberately does not treat the absence of tests as a successful result.
|
|
73
|
+
|
|
74
|
+
## AI coding-agent skill
|
|
75
|
+
|
|
76
|
+
The repository includes SKILL.md with the mandatory agent workflow and output contract. The same skill instructions are included in the installed Python package as package data.
|
|
77
|
+
|
|
78
|
+
## Important limitations
|
|
79
|
+
|
|
80
|
+
Polyguard is a test-execution gate, not a proof of program correctness. The quality of a PASS depends on the requirements and coverage of the tests. It does not generate comprehensive tests automatically in this first release.
|
|
81
|
+
|
|
82
|
+
The verifier executes project build scripts and tests. Do not run untrusted code on a machine containing secrets or sensitive files. Use a disposable container or virtual machine with restricted permissions and no privileged host mounts.
|
|
83
|
+
|
|
84
|
+
Language detection and test-runner conventions vary across ecosystems. This alpha release uses conservative heuristics; unsupported project layouts may require explicit configuration in a future release.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
python -m pip install -e ".[test]"
|
|
89
|
+
python -m pytest -q
|
|
90
|
+
python -m build
|
|
91
|
+
python -m twine check dist/*
|
|
92
|
+
|
|
93
|
+
## Publishing
|
|
94
|
+
|
|
95
|
+
The publish workflow uses PyPI Trusted Publishing. Configure a pending/normal publisher in PyPI for:
|
|
96
|
+
- Owner: Furox-Art
|
|
97
|
+
- Repository: polyglot-code-guard
|
|
98
|
+
- Workflow: publish.yml
|
|
99
|
+
- Environment: pypi
|
|
100
|
+
|
|
101
|
+
Then create a GitHub release to publish. No long-lived PyPI API token is stored in the repository.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Polyguard
|
|
2
|
+
|
|
3
|
+
**Fail-closed verification for AI-generated code across multiple programming languages.**
|
|
4
|
+
|
|
5
|
+
Polyguard runs the real build and test tools for the languages it detects. It returns **PASS** only when every detected language has a recognizable test suite and all configured commands succeed. Missing tests, missing toolchains, timeouts, compilation errors, and failing tests return **FAIL**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
python -m pip install polyglot-code-guard
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
Run this inside the project directory:
|
|
14
|
+
|
|
15
|
+
polyguard verify
|
|
16
|
+
|
|
17
|
+
The normal verdict is intentionally one word:
|
|
18
|
+
|
|
19
|
+
PASS
|
|
20
|
+
|
|
21
|
+
or:
|
|
22
|
+
|
|
23
|
+
FAIL
|
|
24
|
+
|
|
25
|
+
For diagnostics:
|
|
26
|
+
|
|
27
|
+
polyguard verify --details
|
|
28
|
+
|
|
29
|
+
For automation:
|
|
30
|
+
|
|
31
|
+
polyguard verify --json
|
|
32
|
+
|
|
33
|
+
The process exits with code 0 for PASS and code 1 for FAIL.
|
|
34
|
+
|
|
35
|
+
## Initial language support
|
|
36
|
+
|
|
37
|
+
| Language | Test command |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| Python | compileall, then pytest |
|
|
40
|
+
| Rust | cargo test --all-targets |
|
|
41
|
+
| Go | go test ./... |
|
|
42
|
+
| JavaScript / TypeScript | npm test; TypeScript projects also run tsc --noEmit |
|
|
43
|
+
| Java | Maven test or Gradle test |
|
|
44
|
+
| C# | dotnet test |
|
|
45
|
+
| PHP | PHPUnit |
|
|
46
|
+
| Ruby | RSpec or Rake test |
|
|
47
|
+
| Swift | swift test |
|
|
48
|
+
| C / C++ | CMake build and CTest when CTest is configured |
|
|
49
|
+
|
|
50
|
+
The project must contain a detectable test suite for each detected language. Polyguard deliberately does not treat the absence of tests as a successful result.
|
|
51
|
+
|
|
52
|
+
## AI coding-agent skill
|
|
53
|
+
|
|
54
|
+
The repository includes SKILL.md with the mandatory agent workflow and output contract. The same skill instructions are included in the installed Python package as package data.
|
|
55
|
+
|
|
56
|
+
## Important limitations
|
|
57
|
+
|
|
58
|
+
Polyguard is a test-execution gate, not a proof of program correctness. The quality of a PASS depends on the requirements and coverage of the tests. It does not generate comprehensive tests automatically in this first release.
|
|
59
|
+
|
|
60
|
+
The verifier executes project build scripts and tests. Do not run untrusted code on a machine containing secrets or sensitive files. Use a disposable container or virtual machine with restricted permissions and no privileged host mounts.
|
|
61
|
+
|
|
62
|
+
Language detection and test-runner conventions vary across ecosystems. This alpha release uses conservative heuristics; unsupported project layouts may require explicit configuration in a future release.
|
|
63
|
+
|
|
64
|
+
## Development
|
|
65
|
+
|
|
66
|
+
python -m pip install -e ".[test]"
|
|
67
|
+
python -m pytest -q
|
|
68
|
+
python -m build
|
|
69
|
+
python -m twine check dist/*
|
|
70
|
+
|
|
71
|
+
## Publishing
|
|
72
|
+
|
|
73
|
+
The publish workflow uses PyPI Trusted Publishing. Configure a pending/normal publisher in PyPI for:
|
|
74
|
+
- Owner: Furox-Art
|
|
75
|
+
- Repository: polyglot-code-guard
|
|
76
|
+
- Workflow: publish.yml
|
|
77
|
+
- Environment: pypi
|
|
78
|
+
|
|
79
|
+
Then create a GitHub release to publish. No long-lived PyPI API token is stored in the repository.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "polyglot-code-guard"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Fail-closed, multi-language test verification for AI-generated code."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Furox-Art" }]
|
|
13
|
+
keywords = ["testing", "code-verification", "polyglot", "ai-generated-code", "quality-gate"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = ["pytest>=8.0"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
polyguard = "polyguard.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/Furox-Art/polyglot-code-guard"
|
|
31
|
+
Repository = "https://github.com/Furox-Art/polyglot-code-guard"
|
|
32
|
+
Issues = "https://github.com/Furox-Art/polyglot-code-guard/issues"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = {"" = "src"}
|
|
36
|
+
include-package-data = true
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
polyguard = ["SKILL.md"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyglot-code-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fail-closed, multi-language test verification for AI-generated code.
|
|
5
|
+
Author: Furox-Art
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Furox-Art/polyglot-code-guard
|
|
8
|
+
Project-URL: Repository, https://github.com/Furox-Art/polyglot-code-guard
|
|
9
|
+
Project-URL: Issues, https://github.com/Furox-Art/polyglot-code-guard/issues
|
|
10
|
+
Keywords: testing,code-verification,polyglot,ai-generated-code,quality-gate
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Polyguard
|
|
24
|
+
|
|
25
|
+
**Fail-closed verification for AI-generated code across multiple programming languages.**
|
|
26
|
+
|
|
27
|
+
Polyguard runs the real build and test tools for the languages it detects. It returns **PASS** only when every detected language has a recognizable test suite and all configured commands succeed. Missing tests, missing toolchains, timeouts, compilation errors, and failing tests return **FAIL**.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
python -m pip install polyglot-code-guard
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
Run this inside the project directory:
|
|
36
|
+
|
|
37
|
+
polyguard verify
|
|
38
|
+
|
|
39
|
+
The normal verdict is intentionally one word:
|
|
40
|
+
|
|
41
|
+
PASS
|
|
42
|
+
|
|
43
|
+
or:
|
|
44
|
+
|
|
45
|
+
FAIL
|
|
46
|
+
|
|
47
|
+
For diagnostics:
|
|
48
|
+
|
|
49
|
+
polyguard verify --details
|
|
50
|
+
|
|
51
|
+
For automation:
|
|
52
|
+
|
|
53
|
+
polyguard verify --json
|
|
54
|
+
|
|
55
|
+
The process exits with code 0 for PASS and code 1 for FAIL.
|
|
56
|
+
|
|
57
|
+
## Initial language support
|
|
58
|
+
|
|
59
|
+
| Language | Test command |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Python | compileall, then pytest |
|
|
62
|
+
| Rust | cargo test --all-targets |
|
|
63
|
+
| Go | go test ./... |
|
|
64
|
+
| JavaScript / TypeScript | npm test; TypeScript projects also run tsc --noEmit |
|
|
65
|
+
| Java | Maven test or Gradle test |
|
|
66
|
+
| C# | dotnet test |
|
|
67
|
+
| PHP | PHPUnit |
|
|
68
|
+
| Ruby | RSpec or Rake test |
|
|
69
|
+
| Swift | swift test |
|
|
70
|
+
| C / C++ | CMake build and CTest when CTest is configured |
|
|
71
|
+
|
|
72
|
+
The project must contain a detectable test suite for each detected language. Polyguard deliberately does not treat the absence of tests as a successful result.
|
|
73
|
+
|
|
74
|
+
## AI coding-agent skill
|
|
75
|
+
|
|
76
|
+
The repository includes SKILL.md with the mandatory agent workflow and output contract. The same skill instructions are included in the installed Python package as package data.
|
|
77
|
+
|
|
78
|
+
## Important limitations
|
|
79
|
+
|
|
80
|
+
Polyguard is a test-execution gate, not a proof of program correctness. The quality of a PASS depends on the requirements and coverage of the tests. It does not generate comprehensive tests automatically in this first release.
|
|
81
|
+
|
|
82
|
+
The verifier executes project build scripts and tests. Do not run untrusted code on a machine containing secrets or sensitive files. Use a disposable container or virtual machine with restricted permissions and no privileged host mounts.
|
|
83
|
+
|
|
84
|
+
Language detection and test-runner conventions vary across ecosystems. This alpha release uses conservative heuristics; unsupported project layouts may require explicit configuration in a future release.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
python -m pip install -e ".[test]"
|
|
89
|
+
python -m pytest -q
|
|
90
|
+
python -m build
|
|
91
|
+
python -m twine check dist/*
|
|
92
|
+
|
|
93
|
+
## Publishing
|
|
94
|
+
|
|
95
|
+
The publish workflow uses PyPI Trusted Publishing. Configure a pending/normal publisher in PyPI for:
|
|
96
|
+
- Owner: Furox-Art
|
|
97
|
+
- Repository: polyglot-code-guard
|
|
98
|
+
- Workflow: publish.yml
|
|
99
|
+
- Environment: pypi
|
|
100
|
+
|
|
101
|
+
Then create a GitHub release to publish. No long-lived PyPI API token is stored in the repository.
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/polyglot_code_guard.egg-info/PKG-INFO
|
|
5
|
+
src/polyglot_code_guard.egg-info/SOURCES.txt
|
|
6
|
+
src/polyglot_code_guard.egg-info/dependency_links.txt
|
|
7
|
+
src/polyglot_code_guard.egg-info/entry_points.txt
|
|
8
|
+
src/polyglot_code_guard.egg-info/requires.txt
|
|
9
|
+
src/polyglot_code_guard.egg-info/top_level.txt
|
|
10
|
+
src/polyguard/SKILL.md
|
|
11
|
+
src/polyguard/__init__.py
|
|
12
|
+
src/polyguard/cli.py
|
|
13
|
+
src/polyguard/runner.py
|
|
14
|
+
tests/test_runner.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polyguard
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: polyglot-code-verification
|
|
3
|
+
description: Verify AI-generated code with real language test runners and fail closed when tests are missing or fail.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Polyglot Code Verification
|
|
7
|
+
|
|
8
|
+
Use this skill whenever code is generated, changed, refactored, or proposed as complete.
|
|
9
|
+
|
|
10
|
+
## Mandatory gate
|
|
11
|
+
|
|
12
|
+
1. Identify the project root and the languages present.
|
|
13
|
+
2. Ensure the implementation matches the user's stated requirements.
|
|
14
|
+
3. Run the verifier from the project root: polyguard verify --path . --details.
|
|
15
|
+
4. Do not claim that code works unless the verifier exits with status code 0 and prints PASS.
|
|
16
|
+
5. If any required test fails, the final verdict must be exactly FAIL. Do not label untested code as successful.
|
|
17
|
+
6. Missing test suites, missing language toolchains, timeouts, compilation errors, and test-runner errors are failures under the default fail-closed policy.
|
|
18
|
+
7. Inspect the diagnostic report, fix the implementation or tests, and rerun the complete verification suite.
|
|
19
|
+
8. Never weaken, delete, skip, or rewrite a test merely to obtain PASS. Explain any necessary change to test expectations.
|
|
20
|
+
9. Preserve the test output and command details when the user asks for evidence.
|
|
21
|
+
|
|
22
|
+
## Output contract
|
|
23
|
+
|
|
24
|
+
- Successful verification: PASS
|
|
25
|
+
- Failed or incomplete verification: FAIL
|
|
26
|
+
|
|
27
|
+
Use --json when another tool or CI system needs the structured report. Use --details when debugging.
|
|
28
|
+
|
|
29
|
+
## Security
|
|
30
|
+
|
|
31
|
+
The verifier executes project build and test commands. Treat source code, build scripts, dependencies, and tests as untrusted. Run untrusted projects in a disposable container or isolated virtual machine without secrets, privileged access, or sensitive host mounts. Polyguard is a quality gate, not a security sandbox.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
The initial runner recognizes Python, Rust, Go, JavaScript/TypeScript, Java, C#, PHP, Ruby, Swift, and C/C++ CMake projects. A language is not marked PASS unless a test suite is detected and every configured command succeeds.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Command-line interface for Polyguard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .runner import render_report, run_checks
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="polyguard",
|
|
14
|
+
description="Run available language test suites and fail closed when proof is missing.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("verify", nargs="?", choices=["verify"], default="verify")
|
|
17
|
+
parser.add_argument("--path", default=".", help="Project directory to verify.")
|
|
18
|
+
parser.add_argument("--timeout", type=int, default=120, help="Timeout per command in seconds.")
|
|
19
|
+
parser.add_argument("--details", action="store_true", help="Show per-language diagnostics.")
|
|
20
|
+
parser.add_argument("--json", action="store_true", help="Emit a machine-readable JSON report.")
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
args = build_parser().parse_args()
|
|
26
|
+
if args.timeout < 1:
|
|
27
|
+
raise SystemExit("--timeout must be at least 1 second.")
|
|
28
|
+
report = run_checks(Path(args.path), timeout=args.timeout)
|
|
29
|
+
print(render_report(report, details=args.details, as_json=args.json))
|
|
30
|
+
return 0 if report["status"] == "PASS" else 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Language discovery and fail-closed test execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
IGNORED_DIRS = {
|
|
17
|
+
".git", ".hg", ".svn", ".venv", "venv", "env", "node_modules",
|
|
18
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".tox", "dist",
|
|
19
|
+
"build", ".polyguard-build", "target", ".next", ".nuxt",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CheckResult:
|
|
25
|
+
language: str
|
|
26
|
+
status: str
|
|
27
|
+
commands: list[list[str]]
|
|
28
|
+
exit_code: int | None
|
|
29
|
+
elapsed_seconds: float
|
|
30
|
+
reason: str = ""
|
|
31
|
+
output: str = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _project_files(root: Path) -> list[Path]:
|
|
35
|
+
found: list[Path] = []
|
|
36
|
+
for path in root.rglob("*"):
|
|
37
|
+
if any(part in IGNORED_DIRS for part in path.parts):
|
|
38
|
+
continue
|
|
39
|
+
if path.is_file():
|
|
40
|
+
found.append(path)
|
|
41
|
+
return found
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _in_test_tree(path: Path) -> bool:
|
|
45
|
+
return any(part.lower() in {"test", "tests", "spec", "specs"} for part in path.parts)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _has_test(files: Iterable[Path], language: str) -> bool:
|
|
49
|
+
"""Require a recognizable test declaration, not merely a file named like a test."""
|
|
50
|
+
all_files = list(files)
|
|
51
|
+
for path in all_files:
|
|
52
|
+
name = path.name.lower()
|
|
53
|
+
suffix = path.suffix.lower()
|
|
54
|
+
content = _safe_read(path)
|
|
55
|
+
|
|
56
|
+
if language == "python" and suffix == ".py":
|
|
57
|
+
is_test_file = _in_test_tree(path) or name.startswith("test_") or name.endswith("_test.py")
|
|
58
|
+
if is_test_file and re.search(r"\b(?:def\s+test_\w+\s*\(|class\s+Test\w*)", content):
|
|
59
|
+
return True
|
|
60
|
+
elif language == "rust" and suffix == ".rs":
|
|
61
|
+
if re.search(r"#\s*\[\s*(?:test|tokio::test|async_std::test)\s*\]", content):
|
|
62
|
+
return True
|
|
63
|
+
elif language == "javascript" and suffix in {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"}:
|
|
64
|
+
is_test_file = _in_test_tree(path) or re.search(r"\.(test|spec)\.", name)
|
|
65
|
+
if is_test_file and re.search(r"\b(?:test|it|specify)\s*(?:\.\w+)?\s*\(", content):
|
|
66
|
+
return True
|
|
67
|
+
elif language == "go" and name.endswith("_test.go"):
|
|
68
|
+
if re.search(r"\bfunc\s+Test[A-Z]\w*\s*\(", content):
|
|
69
|
+
return True
|
|
70
|
+
elif language == "java" and suffix == ".java":
|
|
71
|
+
is_test_file = _in_test_tree(path) or name.endswith(("test.java", "tests.java"))
|
|
72
|
+
if is_test_file and re.search(r"@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b", content):
|
|
73
|
+
return True
|
|
74
|
+
elif language == "csharp" and suffix == ".cs":
|
|
75
|
+
if re.search(r"\[(?:Fact|Theory|Test|TestCase|TestMethod)\b", content):
|
|
76
|
+
return True
|
|
77
|
+
elif language == "php" and suffix == ".php":
|
|
78
|
+
is_test_file = _in_test_tree(path) or name.startswith("test")
|
|
79
|
+
if is_test_file and re.search(r"(?:function\s+test\w*\s*\(|#\[Test\])", content, re.I):
|
|
80
|
+
return True
|
|
81
|
+
elif language == "ruby" and suffix == ".rb":
|
|
82
|
+
is_test_file = _in_test_tree(path) or name.startswith("test_") or name.endswith("_spec.rb")
|
|
83
|
+
if is_test_file and re.search(r"\b(?:it|specify|example)\s+['\"].+?\bdo\b|\bdef\s+test_\w+", content, re.S):
|
|
84
|
+
return True
|
|
85
|
+
elif language == "swift" and suffix == ".swift":
|
|
86
|
+
if _in_test_tree(path) and re.search(r"(?:\bfunc\s+test\w*\s*\(|@Test\b)", content):
|
|
87
|
+
return True
|
|
88
|
+
elif language == "cpp" and suffix in {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"}:
|
|
89
|
+
if (_in_test_tree(path) or "test" in name) and re.search(r"\b(?:TEST|TEST_F|TEST_P|TEST_CASE)\s*\(", content):
|
|
90
|
+
cmake_files = [p for p in all_files if p.name.lower() == "cmakelists.txt"]
|
|
91
|
+
cmake_text = "\n".join(_safe_read(p) for p in cmake_files)
|
|
92
|
+
enabled = re.search(r"\b(?:enable_testing)\s*\(|\binclude\s*\(\s*CTest\b", cmake_text, re.I)
|
|
93
|
+
registered = re.search(r"\b(?:add_test|gtest_discover_tests|catch_discover_tests)\s*\(", cmake_text)
|
|
94
|
+
if enabled and registered:
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _safe_read(path: Path) -> str:
|
|
100
|
+
try:
|
|
101
|
+
return path.read_text(encoding="utf-8", errors="ignore")[:1_000_000]
|
|
102
|
+
except OSError:
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def discover(root: Path) -> list[tuple[str, list[list[str]]]]:
|
|
107
|
+
"""Return supported language plans. A detected language without tests is still a failure."""
|
|
108
|
+
files = _project_files(root)
|
|
109
|
+
names = {p.name for p in files}
|
|
110
|
+
plans: list[tuple[str, list[list[str]]]] = []
|
|
111
|
+
|
|
112
|
+
py_files = [p for p in files if p.suffix == ".py"]
|
|
113
|
+
if py_files:
|
|
114
|
+
plans.append(("python", [
|
|
115
|
+
[sys.executable, "-m", "compileall", "-q", "."],
|
|
116
|
+
[sys.executable, "-m", "pytest", "-q"],
|
|
117
|
+
]))
|
|
118
|
+
|
|
119
|
+
if "Cargo.toml" in names or any(p.suffix == ".rs" for p in files):
|
|
120
|
+
plans.append(("rust", [["cargo", "test", "--all-targets"]]))
|
|
121
|
+
|
|
122
|
+
if "go.mod" in names:
|
|
123
|
+
plans.append(("go", [["go", "test", "./..."]]))
|
|
124
|
+
|
|
125
|
+
js_project = "package.json" in names and any(
|
|
126
|
+
p.suffix.lower() in {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"} for p in files
|
|
127
|
+
)
|
|
128
|
+
if js_project:
|
|
129
|
+
js_commands = [["npm", "test"]]
|
|
130
|
+
if "tsconfig.json" in names:
|
|
131
|
+
js_commands.append(["npx", "--no-install", "tsc", "--noEmit"])
|
|
132
|
+
plans.append(("javascript", js_commands))
|
|
133
|
+
|
|
134
|
+
java_build = names.intersection({"pom.xml", "build.gradle", "build.gradle.kts", "mvnw", "gradlew"})
|
|
135
|
+
if java_build:
|
|
136
|
+
if "pom.xml" in names:
|
|
137
|
+
cmd = ["./mvnw", "test"] if "mvnw" in names else ["mvn", "test"]
|
|
138
|
+
else:
|
|
139
|
+
cmd = ["./gradlew", "test"] if "gradlew" in names else ["gradle", "test"]
|
|
140
|
+
plans.append(("java", [cmd]))
|
|
141
|
+
|
|
142
|
+
cs_projects = [p for p in files if p.suffix.lower() in {".csproj", ".sln"}]
|
|
143
|
+
if cs_projects:
|
|
144
|
+
plans.append(("csharp", [["dotnet", "test", "--no-restore"]]))
|
|
145
|
+
|
|
146
|
+
if "composer.json" in names:
|
|
147
|
+
phpunit = root / "vendor" / "bin" / "phpunit"
|
|
148
|
+
plans.append(("php", [[str(phpunit)] if phpunit.exists() else ["phpunit"]]))
|
|
149
|
+
|
|
150
|
+
if "Gemfile" in names or any(p.suffix == ".gemspec" for p in files):
|
|
151
|
+
if any(p.parts[-2:-1] == ("spec",) for p in files):
|
|
152
|
+
plans.append(("ruby", [["bundle", "exec", "rspec"]]))
|
|
153
|
+
else:
|
|
154
|
+
plans.append(("ruby", [["bundle", "exec", "rake", "test"]]))
|
|
155
|
+
|
|
156
|
+
if "Package.swift" in names:
|
|
157
|
+
plans.append(("swift", [["swift", "test"]]))
|
|
158
|
+
|
|
159
|
+
if "CMakeLists.txt" in names:
|
|
160
|
+
cmake_text = _safe_read(root / "CMakeLists.txt")
|
|
161
|
+
if re.search(r"enable_testing\s*\(|include\s*\(\s*CTest", cmake_text, re.I):
|
|
162
|
+
plans.append(("cpp", [
|
|
163
|
+
["cmake", "-S", ".", "-B", ".polyguard-build", "-DBUILD_TESTING=ON"],
|
|
164
|
+
["cmake", "--build", ".polyguard-build", "--parallel"],
|
|
165
|
+
["ctest", "--test-dir", ".polyguard-build", "--output-on-failure"],
|
|
166
|
+
]))
|
|
167
|
+
else:
|
|
168
|
+
plans.append(("cpp", [["cmake", "-S", ".", "-B", ".polyguard-build"]]))
|
|
169
|
+
|
|
170
|
+
return plans
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def run_checks(root: Path, timeout: int = 120) -> dict:
|
|
174
|
+
root = root.resolve()
|
|
175
|
+
if not root.is_dir():
|
|
176
|
+
return {
|
|
177
|
+
"status": "FAIL",
|
|
178
|
+
"results": [asdict(CheckResult("project", "FAIL", [], 2, 0.0, "Project directory does not exist."))],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
plans = discover(root)
|
|
182
|
+
if not plans:
|
|
183
|
+
return {
|
|
184
|
+
"status": "FAIL",
|
|
185
|
+
"results": [asdict(CheckResult("project", "FAIL", [], 2, 0.0, "No supported language project was detected."))],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
results: list[CheckResult] = []
|
|
189
|
+
files = _project_files(root)
|
|
190
|
+
for language, commands in plans:
|
|
191
|
+
if not _has_test(files, language):
|
|
192
|
+
results.append(CheckResult(
|
|
193
|
+
language, "FAIL", commands, 2, 0.0,
|
|
194
|
+
"No test suite was detected; fail-closed policy rejects untested code.",
|
|
195
|
+
))
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
started = time.monotonic()
|
|
199
|
+
last_code: int | None = 0
|
|
200
|
+
output_parts: list[str] = []
|
|
201
|
+
failure_reason = ""
|
|
202
|
+
for command in commands:
|
|
203
|
+
executable = command[0]
|
|
204
|
+
if executable.startswith("./"):
|
|
205
|
+
executable_path = root / executable[2:]
|
|
206
|
+
if not executable_path.exists():
|
|
207
|
+
last_code = 127
|
|
208
|
+
failure_reason = f"Required test runner not found: {executable}"
|
|
209
|
+
break
|
|
210
|
+
elif not Path(executable).is_absolute() and shutil.which(executable) is None:
|
|
211
|
+
last_code = 127
|
|
212
|
+
failure_reason = f"Required tool not installed: {executable}"
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
completed = subprocess.run(
|
|
217
|
+
command,
|
|
218
|
+
cwd=root,
|
|
219
|
+
text=True,
|
|
220
|
+
stdout=subprocess.PIPE,
|
|
221
|
+
stderr=subprocess.STDOUT,
|
|
222
|
+
timeout=timeout,
|
|
223
|
+
check=False,
|
|
224
|
+
)
|
|
225
|
+
last_code = completed.returncode
|
|
226
|
+
if completed.stdout:
|
|
227
|
+
output_parts.append(completed.stdout[-12000:])
|
|
228
|
+
if last_code != 0:
|
|
229
|
+
failure_reason = f"Command failed with exit code {last_code}: {' '.join(command)}"
|
|
230
|
+
break
|
|
231
|
+
except subprocess.TimeoutExpired as exc:
|
|
232
|
+
last_code = 124
|
|
233
|
+
captured = exc.stdout or ""
|
|
234
|
+
if isinstance(captured, bytes):
|
|
235
|
+
captured = captured.decode("utf-8", errors="replace")
|
|
236
|
+
output_parts.append(str(captured)[-12000:])
|
|
237
|
+
failure_reason = f"Command timed out after {timeout} seconds: {' '.join(command)}"
|
|
238
|
+
break
|
|
239
|
+
except OSError as exc:
|
|
240
|
+
last_code = 126
|
|
241
|
+
failure_reason = f"Could not execute {' '.join(command)}: {exc}"
|
|
242
|
+
|
|
243
|
+
elapsed = time.monotonic() - started
|
|
244
|
+
passed = last_code == 0 and not failure_reason
|
|
245
|
+
results.append(CheckResult(
|
|
246
|
+
language,
|
|
247
|
+
"PASS" if passed else "FAIL",
|
|
248
|
+
commands,
|
|
249
|
+
last_code,
|
|
250
|
+
round(elapsed, 3),
|
|
251
|
+
failure_reason,
|
|
252
|
+
"\n".join(output_parts)[-12000:],
|
|
253
|
+
))
|
|
254
|
+
|
|
255
|
+
status = "PASS" if results and all(item.status == "PASS" for item in results) else "FAIL"
|
|
256
|
+
return {"status": status, "root": str(root), "results": [asdict(item) for item in results]}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def render_report(report: dict, details: bool = False, as_json: bool = False) -> str:
|
|
260
|
+
if as_json:
|
|
261
|
+
return json.dumps(report, indent=2, ensure_ascii=False)
|
|
262
|
+
if not details:
|
|
263
|
+
return report["status"]
|
|
264
|
+
lines = [report["status"]]
|
|
265
|
+
for item in report["results"]:
|
|
266
|
+
lines.append(f"{item['language']}: {item['status']}")
|
|
267
|
+
if item["reason"]:
|
|
268
|
+
lines.append(f" {item['reason']}")
|
|
269
|
+
if item["output"]:
|
|
270
|
+
lines.append(item["output"])
|
|
271
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from polyguard.runner import _has_test, discover, render_report, run_checks
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_python_project_without_tests_fails_closed(tmp_path: Path) -> None:
|
|
7
|
+
(tmp_path / "app.py").write_text("print('hello')\n", encoding="utf-8")
|
|
8
|
+
report = run_checks(tmp_path)
|
|
9
|
+
assert report["status"] == "FAIL"
|
|
10
|
+
assert report["results"][0]["status"] == "FAIL"
|
|
11
|
+
assert "No test suite" in report["results"][0]["reason"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_empty_python_test_file_is_not_accepted(tmp_path: Path) -> None:
|
|
15
|
+
empty_test = tmp_path / "test_app.py"
|
|
16
|
+
empty_test.write_text("# intentionally empty\n", encoding="utf-8")
|
|
17
|
+
assert not _has_test([empty_test], "python")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_python_test_file_is_detected(tmp_path: Path) -> None:
|
|
21
|
+
(tmp_path / "app.py").write_text("def add(a, b): return a + b\n", encoding="utf-8")
|
|
22
|
+
test_file = tmp_path / "test_app.py"
|
|
23
|
+
test_file.write_text("def test_add(): assert 1 + 1 == 2\n", encoding="utf-8")
|
|
24
|
+
assert _has_test([test_file], "python")
|
|
25
|
+
assert any(language == "python" for language, _ in discover(tmp_path))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_go_requires_a_test_function(tmp_path: Path) -> None:
|
|
29
|
+
empty_test = tmp_path / "main_test.go"
|
|
30
|
+
empty_test.write_text("package main\n", encoding="utf-8")
|
|
31
|
+
assert not _has_test([empty_test], "go")
|
|
32
|
+
empty_test.write_text("package main\nfunc TestAdd(t *testing.T) {}\n", encoding="utf-8")
|
|
33
|
+
assert _has_test([empty_test], "go")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_rust_test_annotation_is_detected(tmp_path: Path) -> None:
|
|
37
|
+
test_file = tmp_path / "lib.rs"
|
|
38
|
+
test_file.write_text("#[test]\nfn works() {}\n", encoding="utf-8")
|
|
39
|
+
assert _has_test([test_file], "rust")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cpp_requires_registered_ctest_case(tmp_path: Path) -> None:
|
|
43
|
+
cmake = tmp_path / "CMakeLists.txt"
|
|
44
|
+
test_file = tmp_path / "test_math.cpp"
|
|
45
|
+
test_file.write_text("TEST(Math, Adds) { EXPECT_EQ(1 + 1, 2); }\n", encoding="utf-8")
|
|
46
|
+
cmake.write_text("enable_testing()\nadd_test(NAME math COMMAND math_tests)\n", encoding="utf-8")
|
|
47
|
+
assert _has_test([cmake, test_file], "cpp")
|
|
48
|
+
cmake.write_text("add_test(NAME math COMMAND math_tests)\n", encoding="utf-8")
|
|
49
|
+
assert not _has_test([cmake, test_file], "cpp")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_no_supported_language_fails_closed(tmp_path: Path) -> None:
|
|
53
|
+
(tmp_path / "notes.txt").write_text("not source code", encoding="utf-8")
|
|
54
|
+
assert run_checks(tmp_path)["status"] == "FAIL"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_report_default_is_one_word() -> None:
|
|
58
|
+
report = {"status": "FAIL", "results": []}
|
|
59
|
+
assert render_report(report) == "FAIL"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_json_report_is_machine_readable() -> None:
|
|
63
|
+
import json
|
|
64
|
+
|
|
65
|
+
report = {"status": "PASS", "results": []}
|
|
66
|
+
assert json.loads(render_report(report, as_json=True)) == report
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_cli_accepts_verify_subcommand() -> None:
|
|
70
|
+
from polyguard.cli import build_parser
|
|
71
|
+
|
|
72
|
+
args = build_parser().parse_args(["verify", "--timeout", "12"])
|
|
73
|
+
assert args.verify == "verify"
|
|
74
|
+
assert args.timeout == 12
|