Add community scope gate
This commit is contained in:
@@ -37,12 +37,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify runner toolchain
|
- name: Verify runner toolchain
|
||||||
run: |
|
run: |
|
||||||
|
python3 --version
|
||||||
rustc --version
|
rustc --version
|
||||||
cargo --version
|
cargo --version
|
||||||
rustfmt --version
|
rustfmt --version
|
||||||
cargo clippy --version
|
cargo clippy --version
|
||||||
docker --version
|
docker --version
|
||||||
|
|
||||||
|
- name: Run tooling unit tests
|
||||||
|
run: python3 -m unittest discover -s tests/unit
|
||||||
|
|
||||||
|
- name: Check Community scope
|
||||||
|
run: scripts/check-community-scope.sh
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: cargo fmt --all --check
|
run: cargo fmt --all --check
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ apps/ui/test-results
|
|||||||
apps/ui/vite.config.js
|
apps/ui/vite.config.js
|
||||||
apps/ui/vite.config.d.ts
|
apps/ui/vite.config.d.ts
|
||||||
*.log
|
*.log
|
||||||
|
__pycache__/
|
||||||
__*.md
|
__*.md
|
||||||
diploma/
|
diploma/
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|||||||
@@ -164,11 +164,7 @@ mod tests {
|
|||||||
use tracing_subscriber::{filter::LevelFilter, fmt::MakeWriter, prelude::*};
|
use tracing_subscriber::{filter::LevelFilter, fmt::MakeWriter, prelude::*};
|
||||||
|
|
||||||
use crank_community_mcp::{
|
use crank_community_mcp::{
|
||||||
auth::{
|
auth::{CommunityMachineCredentialVerifier, SharedMachineCredentialVerifier},
|
||||||
CommunityMachineCredentialVerifier, MachineCredentialVerifier,
|
|
||||||
MachineCredentialVerifierError, SharedMachineCredentialVerifier,
|
|
||||||
VerifiedMachineCredential,
|
|
||||||
},
|
|
||||||
build_app,
|
build_app,
|
||||||
catalog::PublishedToolCatalog,
|
catalog::PublishedToolCatalog,
|
||||||
session::{InMemorySessionStore, SharedSessionStore, TransportSessionStore},
|
session::{InMemorySessionStore, SharedSessionStore, TransportSessionStore},
|
||||||
@@ -272,28 +268,6 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct StubMachineCredentialVerifier {
|
|
||||||
token: String,
|
|
||||||
credential: VerifiedMachineCredential,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl MachineCredentialVerifier for StubMachineCredentialVerifier {
|
|
||||||
async fn verify_bearer_token(
|
|
||||||
&self,
|
|
||||||
_workspace_slug: &str,
|
|
||||||
_agent_slug: &str,
|
|
||||||
token: &str,
|
|
||||||
) -> Result<Option<VerifiedMachineCredential>, MachineCredentialVerifierError> {
|
|
||||||
if token == self.token {
|
|
||||||
return Ok(Some(self.credential.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn initializes_lists_and_calls_published_tool_via_mcp() {
|
async fn initializes_lists_and_calls_published_tool_via_mcp() {
|
||||||
let registry = test_registry().await;
|
let registry = test_registry().await;
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ fmt:
|
|||||||
fmt-check:
|
fmt-check:
|
||||||
cargo fmt --all --check
|
cargo fmt --all --check
|
||||||
|
|
||||||
|
tooling-test:
|
||||||
|
python3 -m unittest discover -s tests/unit
|
||||||
|
|
||||||
|
community-scope-check:
|
||||||
|
scripts/check-community-scope.sh
|
||||||
|
|
||||||
check:
|
check:
|
||||||
cargo check --workspace
|
cargo check --workspace
|
||||||
|
|
||||||
@@ -20,6 +26,8 @@ sqlx-check:
|
|||||||
SQLX_OFFLINE=true cargo check --workspace
|
SQLX_OFFLINE=true cargo check --workspace
|
||||||
|
|
||||||
verify:
|
verify:
|
||||||
|
just tooling-test
|
||||||
|
just community-scope-check
|
||||||
just fmt-check
|
just fmt-check
|
||||||
just clippy
|
just clippy
|
||||||
just test
|
just test
|
||||||
|
|||||||
@@ -71,3 +71,18 @@ shell env file.
|
|||||||
```bash
|
```bash
|
||||||
./scripts/check-rust-code-health.sh
|
./scripts/check-rust-code-health.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `check-community-scope.sh`
|
||||||
|
|
||||||
|
Проверяет, что в community-репозиторий не попали функции и тексты за пределами
|
||||||
|
REST -> MCP сценария.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/check-community-scope.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Unit-тесты checker-а лежат в `tests/unit`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m unittest discover -s tests/unit
|
||||||
|
```
|
||||||
|
|||||||
Executable
+129
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_EXCLUDED_PATHS = {
|
||||||
|
"LICENSE",
|
||||||
|
"Cargo.lock",
|
||||||
|
"apps/ui/package-lock.json",
|
||||||
|
"scripts/check-community-scope.py",
|
||||||
|
"tests/unit/test_check_community_scope.py",
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_EXCLUDED_PREFIXES = {
|
||||||
|
"target/",
|
||||||
|
"apps/ui/node_modules/",
|
||||||
|
"apps/ui/dist/",
|
||||||
|
}
|
||||||
|
|
||||||
|
FORBIDDEN_PATTERNS = [
|
||||||
|
("enterprise", re.compile(r"\benterprise\b", re.IGNORECASE)),
|
||||||
|
("cloud", re.compile(r"\bcloud\b", re.IGNORECASE)),
|
||||||
|
("commercial", re.compile(r"\bcommercial\b|коммер", re.IGNORECASE)),
|
||||||
|
("cloud-russian", re.compile(r"облач", re.IGNORECASE)),
|
||||||
|
("graphql", re.compile(r"\bgraphql\b", re.IGNORECASE)),
|
||||||
|
("grpc", re.compile(r"\bgrpc\b", re.IGNORECASE)),
|
||||||
|
("soap", re.compile(r"\bsoap\b", re.IGNORECASE)),
|
||||||
|
("websocket", re.compile(r"\bwebsocket\b", re.IGNORECASE)),
|
||||||
|
("short-lived-token", re.compile(r"short[-_ ]?lived", re.IGNORECASE)),
|
||||||
|
("one-time-token", re.compile(r"one[-_ ]?time", re.IGNORECASE)),
|
||||||
|
("sso", re.compile(r"\bsso\b", re.IGNORECASE)),
|
||||||
|
("two-factor", re.compile(r"two[-_ ]?factor|\b2fa\b|\bmfa\b", re.IGNORECASE)),
|
||||||
|
("billing", re.compile(r"\bbilling\b", re.IGNORECASE)),
|
||||||
|
("tenant", re.compile(r"\btenant\b", re.IGNORECASE)),
|
||||||
|
("stream-session", re.compile(r"stream session", re.IGNORECASE)),
|
||||||
|
("async-job", re.compile(r"async job", re.IGNORECASE)),
|
||||||
|
("mcp-auth", re.compile(r"mcp-auth", re.IGNORECASE)),
|
||||||
|
("protocol-capabilities", re.compile(r"protocol-capabilities", re.IGNORECASE)),
|
||||||
|
("machine-token", re.compile(r"machine token", re.IGNORECASE)),
|
||||||
|
("token-issuer", re.compile(r"token issuer", re.IGNORECASE)),
|
||||||
|
("request-variables", re.compile(r"request\.variables", re.IGNORECASE)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Fail if Community repository contains out-of-scope product markers."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--root",
|
||||||
|
type=Path,
|
||||||
|
default=Path(__file__).resolve().parents[1],
|
||||||
|
help="Repository root. Defaults to parent of scripts/.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--files",
|
||||||
|
nargs="*",
|
||||||
|
help="Optional explicit file list, relative to root. Defaults to git ls-files.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def git_tracked_files(root: Path) -> list[str]:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "ls-files"],
|
||||||
|
cwd=root,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
return [line for line in result.stdout.splitlines() if line]
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip_path(path: str) -> bool:
|
||||||
|
if path in DEFAULT_EXCLUDED_PATHS:
|
||||||
|
return True
|
||||||
|
return any(path.startswith(prefix) for prefix in DEFAULT_EXCLUDED_PREFIXES)
|
||||||
|
|
||||||
|
|
||||||
|
def is_binary(data: bytes) -> bool:
|
||||||
|
return b"\0" in data[:4096]
|
||||||
|
|
||||||
|
|
||||||
|
def scan_file(root: Path, relative_path: str) -> list[str]:
|
||||||
|
if should_skip_path(relative_path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
path = root / relative_path
|
||||||
|
if not path.is_file():
|
||||||
|
return []
|
||||||
|
|
||||||
|
data = path.read_bytes()
|
||||||
|
if is_binary(data):
|
||||||
|
return []
|
||||||
|
|
||||||
|
text = data.decode("utf-8", errors="replace")
|
||||||
|
findings: list[str] = []
|
||||||
|
for line_no, line in enumerate(text.splitlines(), start=1):
|
||||||
|
for label, pattern in FORBIDDEN_PATTERNS:
|
||||||
|
if pattern.search(line):
|
||||||
|
findings.append(f"{relative_path}:{line_no}: forbidden marker `{label}`")
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
root = args.root.resolve()
|
||||||
|
files = args.files if args.files is not None else git_tracked_files(root)
|
||||||
|
findings: list[str] = []
|
||||||
|
|
||||||
|
for relative_path in sorted(files):
|
||||||
|
findings.extend(scan_file(root, relative_path))
|
||||||
|
|
||||||
|
if findings:
|
||||||
|
print("Community scope check failed:", file=sys.stderr)
|
||||||
|
for finding in findings:
|
||||||
|
print(f"error: {finding}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"Community scope check passed ({len(files)} tracked files scanned)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
python3 "$ROOT_DIR/scripts/check-community-scope.py" --root "$ROOT_DIR"
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
CHECKER = ROOT / "scripts" / "check-community-scope.py"
|
||||||
|
|
||||||
|
|
||||||
|
class CommunityScopeCheckTests(unittest.TestCase):
|
||||||
|
def run_checker(self, root: Path, files: list[str]) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(CHECKER), "--root", str(root), "--files", *files],
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_passes_for_clean_community_content(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "README.md").write_text(
|
||||||
|
"Crank publishes REST API endpoints as MCP tools.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.run_checker(root, ["README.md"])
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn("Community scope check passed", result.stdout)
|
||||||
|
|
||||||
|
def test_fails_for_forbidden_product_marker(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "apps").mkdir()
|
||||||
|
(root / "apps" / "ui.md").write_text(
|
||||||
|
"Hidden GraphQL protocol card must not be here.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.run_checker(root, ["apps/ui.md"])
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("apps/ui.md:1", result.stderr)
|
||||||
|
self.assertIn("graphql", result.stderr.lower())
|
||||||
|
|
||||||
|
def test_ignores_default_excluded_paths(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "LICENSE").write_text(
|
||||||
|
"The license text may mention commercial distribution terms.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.run_checker(root, ["LICENSE"])
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
|
def test_skips_binary_files(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
(root / "logo.png").write_bytes(b"\x89PNG\x00enterprise\x00")
|
||||||
|
|
||||||
|
result = self.run_checker(root, ["logo.png"])
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user