ferroguard 0.3.1__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.
- ferroguard-0.3.1/.agents/skills/git-workflow/SKILL.md +35 -0
- ferroguard-0.3.1/.agents/skills/stack-guardrails/SKILL.md +25 -0
- ferroguard-0.3.1/.claude/agents/api-dev.md +9 -0
- ferroguard-0.3.1/.claude/agents/backend-dev.md +9 -0
- ferroguard-0.3.1/.claude/agents/code-reviewer.md +14 -0
- ferroguard-0.3.1/.claude/commands/commit.md +9 -0
- ferroguard-0.3.1/.claude/commands/create-pr.md +7 -0
- ferroguard-0.3.1/.claude/commands/implement.md +11 -0
- ferroguard-0.3.1/.claude/commands/verify.md +17 -0
- ferroguard-0.3.1/.claude/hooks/scripts/commit-style.py +59 -0
- ferroguard-0.3.1/.claude/hooks/scripts/commit-style.sh +3 -0
- ferroguard-0.3.1/.claude/hooks/scripts/git-guardrails.sh +3 -0
- ferroguard-0.3.1/.claude/hooks/scripts/git_guardrails.py +60 -0
- ferroguard-0.3.1/.claude/hooks/scripts/stack-guardrails.sh +3 -0
- ferroguard-0.3.1/.claude/hooks/scripts/stack_guardrails.py +80 -0
- ferroguard-0.3.1/.claude/rules/coding-standards.md +15 -0
- ferroguard-0.3.1/.claude/rules/security.md +13 -0
- ferroguard-0.3.1/.claude/settings.json +48 -0
- ferroguard-0.3.1/.claude/skills/git-workflow/SKILL.md +35 -0
- ferroguard-0.3.1/.claude/skills/stack-guardrails/SKILL.md +25 -0
- ferroguard-0.3.1/.claude/stack-guardrails.enabled +0 -0
- ferroguard-0.3.1/.commitlintrc.json +42 -0
- ferroguard-0.3.1/.cursor/rules/coding-standards.mdc +9 -0
- ferroguard-0.3.1/.cursor/rules/security.mdc +9 -0
- ferroguard-0.3.1/.cursor/skills/git-workflow/SKILL.md +35 -0
- ferroguard-0.3.1/.cursor/skills/stack-guardrails/SKILL.md +25 -0
- ferroguard-0.3.1/.env-template +22 -0
- ferroguard-0.3.1/.github/CODEOWNERS +2 -0
- ferroguard-0.3.1/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- ferroguard-0.3.1/.github/ISSUE_TEMPLATE/config.yml +5 -0
- ferroguard-0.3.1/.github/ISSUE_TEMPLATE/feature_request.yml +32 -0
- ferroguard-0.3.1/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- ferroguard-0.3.1/.github/copilot-instructions.md +5 -0
- ferroguard-0.3.1/.github/dependabot.yml +56 -0
- ferroguard-0.3.1/.github/instructions/coding-standards.instructions.md +7 -0
- ferroguard-0.3.1/.github/instructions/security.instructions.md +7 -0
- ferroguard-0.3.1/.github/workflows/ci.yml +164 -0
- ferroguard-0.3.1/.github/workflows/codeql.yml +40 -0
- ferroguard-0.3.1/.github/workflows/release.yml +276 -0
- ferroguard-0.3.1/.github/workflows/scorecard.yml +42 -0
- ferroguard-0.3.1/.github/workflows/security.yml +59 -0
- ferroguard-0.3.1/.gitignore +51 -0
- ferroguard-0.3.1/.pre-commit-config.yaml +40 -0
- ferroguard-0.3.1/.python-version +1 -0
- ferroguard-0.3.1/.version +1 -0
- ferroguard-0.3.1/AGENTS.md +139 -0
- ferroguard-0.3.1/CHANGELOG.md +83 -0
- ferroguard-0.3.1/CLAUDE.md +12 -0
- ferroguard-0.3.1/CODE_OF_CONDUCT.md +133 -0
- ferroguard-0.3.1/CONTRIBUTING.md +110 -0
- ferroguard-0.3.1/Cargo.lock +260 -0
- ferroguard-0.3.1/Cargo.toml +20 -0
- ferroguard-0.3.1/Dockerfile +48 -0
- ferroguard-0.3.1/LICENSE +21 -0
- ferroguard-0.3.1/PKG-INFO +308 -0
- ferroguard-0.3.1/README.md +280 -0
- ferroguard-0.3.1/RELEASING.md +117 -0
- ferroguard-0.3.1/SECURITY.md +27 -0
- ferroguard-0.3.1/api/__init__.py +0 -0
- ferroguard-0.3.1/api/main.py +244 -0
- ferroguard-0.3.1/api/pii_store.py +58 -0
- ferroguard-0.3.1/deny.toml +28 -0
- ferroguard-0.3.1/justfile +36 -0
- ferroguard-0.3.1/llm_guardrails/__init__.py +19 -0
- ferroguard-0.3.1/llm_guardrails/_core.pyi +25 -0
- ferroguard-0.3.1/llm_guardrails/bias_scorer.py +39 -0
- ferroguard-0.3.1/llm_guardrails/injection_detector.py +71 -0
- ferroguard-0.3.1/llm_guardrails/output_validator.py +76 -0
- ferroguard-0.3.1/llm_guardrails/pii_redactor.py +87 -0
- ferroguard-0.3.1/llm_guardrails/pipeline.py +189 -0
- ferroguard-0.3.1/llm_guardrails/py.typed +0 -0
- ferroguard-0.3.1/pyproject.toml +78 -0
- ferroguard-0.3.1/rust-toolchain.toml +5 -0
- ferroguard-0.3.1/scripts/commit-types.json +18 -0
- ferroguard-0.3.1/scripts/install-harness-from.sh +24 -0
- ferroguard-0.3.1/scripts/release_publish.py +428 -0
- ferroguard-0.3.1/scripts/release_subject.py +1068 -0
- ferroguard-0.3.1/scripts/test_guardrails.py +159 -0
- ferroguard-0.3.1/src/bias_scorer.rs +164 -0
- ferroguard-0.3.1/src/injection_detector.rs +382 -0
- ferroguard-0.3.1/src/lib.rs +21 -0
- ferroguard-0.3.1/src/output_validator.rs +236 -0
- ferroguard-0.3.1/src/pii_redactor.rs +463 -0
- ferroguard-0.3.1/tests/__init__.py +0 -0
- ferroguard-0.3.1/tests/test_api.py +284 -0
- ferroguard-0.3.1/tests/test_bias_scorer.py +44 -0
- ferroguard-0.3.1/tests/test_injection_detector.py +125 -0
- ferroguard-0.3.1/tests/test_packaging.py +79 -0
- ferroguard-0.3.1/tests/test_pii_redactor.py +229 -0
- ferroguard-0.3.1/tests/test_pipeline.py +94 -0
- ferroguard-0.3.1/tests/test_types.py +76 -0
- ferroguard-0.3.1/uv.lock +1138 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-workflow
|
|
3
|
+
description: Commit subjects, pull request shape, and direct releases from main. Use when committing, opening a PR, or changing release automation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git workflow
|
|
7
|
+
|
|
8
|
+
## Commit subject
|
|
9
|
+
|
|
10
|
+
`<emoji> <type>(<scope>): <lower-case imperative>`. Under 72 characters. No trailing period. The body says why. Footers are `Closes #N`, `Fixes #N`, `Refs #N`, or `BREAKING CHANGE:`. No `Co-authored-by` and no "Generated with" trailer.
|
|
11
|
+
|
|
12
|
+
Types and emoji are the table in `AGENTS.md` and `scripts/commit-types.json`. Extra emoji that keep the same type: 🔥 🚚 🚨 💚 🎉 🔖.
|
|
13
|
+
|
|
14
|
+
## Pull request
|
|
15
|
+
|
|
16
|
+
Title is the subject. One label from `feature`, `bug`, `refactor`, `upgrade`, `docs`, `internal`, `breaking`, `security`.
|
|
17
|
+
|
|
18
|
+
Body:
|
|
19
|
+
|
|
20
|
+
- **What and why**, with `Closes #N`
|
|
21
|
+
- **Evidence**: Before, After, Not verified
|
|
22
|
+
- **Merge danger**: Door (one-way or two-way), Blast radius, Rollback
|
|
23
|
+
- **Breaking changes, migrations, config**
|
|
24
|
+
|
|
25
|
+
Add a Mermaid diagram when the architecture or the data flow changes.
|
|
26
|
+
|
|
27
|
+
Branch from `main` as `feat/issue-N-short-name` or `fix/issue-N-short-name`. Do not force-push a shared branch.
|
|
28
|
+
|
|
29
|
+
## Release
|
|
30
|
+
|
|
31
|
+
A releasable push to `main` publishes in one workflow run. `scripts/release_subject.py` takes the highest bump since the last tag and consumes `## Unreleased`. The job commits `🔖 chore(release): vX.Y.Z`, pushes `main` without force, and tags that commit. The release commit does not publish again. The same run builds the abi3 wheels and sdist from that tag and publishes them to PyPI.
|
|
32
|
+
|
|
33
|
+
## Merge
|
|
34
|
+
|
|
35
|
+
Only when the human asks: `gh pr merge <number> --squash --delete-branch`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stack-guardrails
|
|
3
|
+
description: Opt-in blocks for publish and deploy, plus the uv, ruff, and cargo checks this library expects. Use before publishing, deploying, or changing the toolchain.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stack guardrails
|
|
7
|
+
|
|
8
|
+
Opt in by committing an empty `.claude/stack-guardrails.enabled` at the git root, or export `SDLC_STACK_GUARDRAILS=1`. This repository commits the marker, so the blocks are on.
|
|
9
|
+
|
|
10
|
+
The hook exits 2, in every profile, for:
|
|
11
|
+
|
|
12
|
+
- `npm publish`, `pnpm publish`, `yarn publish`, `uv publish`, `changeset publish` (a `--dry-run` is allowed)
|
|
13
|
+
- `railway up`, `deploy`, `redeploy`, `down`, `variables --set`, `config apply`, `config migrate --apply`
|
|
14
|
+
|
|
15
|
+
It warns on a hand edit of `uv.lock` or `pnpm-lock.yaml`. `Cargo.lock` is committed on purpose. Do not gitignore it. Regenerate `uv.lock` with `uv lock`, do not hand-edit it.
|
|
16
|
+
|
|
17
|
+
## Python and Rust
|
|
18
|
+
|
|
19
|
+
Python is 3.11 in CI and 3.14 in `.python-version`. Do not select a newer interpreter. Do not raise `requires-python`, the setup-uv `python-version`, or Ruff `target-version` past 3.14. Ruff's `target-version` is `py311`. Tooling is `uv`.
|
|
20
|
+
|
|
21
|
+
Lint and format with `uv run ruff check .` and `uv run ruff format --check .`. Test with `uv run pytest`.
|
|
22
|
+
|
|
23
|
+
Rust's bar is `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test`. The stable channel is `rust-toolchain.toml`. CI passes that channel into `dtolnay/rust-toolchain` because the action is pinned by commit SHA.
|
|
24
|
+
|
|
25
|
+
There is no TypeScript app and no Node toolchain.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-dev
|
|
3
|
+
description: Implement a change in the Python wrappers or the FastAPI demo.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Work in `llm_guardrails/` or `api/`. Wrappers stay thin. The demo calls `GuardrailsPipeline` and does not load environment variables. Run `uv run ruff check .`, `uv run ruff format --check .`, and `uv run pytest` before you claim the change is done.
|
|
8
|
+
|
|
9
|
+
Follow `AGENTS.md`. There is no frontend.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-dev
|
|
3
|
+
description: Implement a Rust change in the guardrail core.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Work in `src/`. Export new behaviour through PyO3 in `src/lib.rs`. Run `cargo fmt`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` before you claim the change is done.
|
|
8
|
+
|
|
9
|
+
Follow `AGENTS.md`. Do not expand into the next issue.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-reviewer
|
|
3
|
+
description: Review a change on two axes, standards and spec fidelity.
|
|
4
|
+
model: opus
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Review the diff against `AGENTS.md`.
|
|
8
|
+
|
|
9
|
+
Report two axes separately:
|
|
10
|
+
|
|
11
|
+
1. Standards. fmt, clippy, ruff, tests, secrets, commit and PR shape. The import name stays `llm_guardrails`.
|
|
12
|
+
2. Spec fidelity. Does the change do what was asked, and nothing it forbids?
|
|
13
|
+
|
|
14
|
+
Lead with findings, by severity. Do not rewrite the change unless asked.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Commit the current change with a hybrid subject.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Follow `.claude/skills/git-workflow/SKILL.md`.
|
|
6
|
+
|
|
7
|
+
Stage one logical change. The subject is `<emoji> <type>(<scope>): <lower-case imperative>`, under 72 characters, no trailing period. The body says why. No attribution trailer.
|
|
8
|
+
|
|
9
|
+
Do not use `--no-verify` unless the human asked.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Open a pull request. Do not merge it.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Follow `.claude/skills/git-workflow/SKILL.md`.
|
|
6
|
+
|
|
7
|
+
Title is the hybrid subject. Body has what and why, `Closes #N`, evidence, merge danger, and breaking changes. One label. Push the feature branch. Do not push `main`. Do not merge.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Implement one issue with tests, then stop.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Read the issue. Read `AGENTS.md`.
|
|
6
|
+
|
|
7
|
+
Agree the seams, then make one failing test pass at a time. Rust tests live in `src/`. Python tests live in `tests/`. Mock only at process boundaries.
|
|
8
|
+
|
|
9
|
+
Verify with the commands in `AGENTS.md` that the diff can affect. Read the output.
|
|
10
|
+
|
|
11
|
+
Commit a meaningful unit when it is green. Open or update the pull request. Do not merge. Do not start the next issue.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the library verification loop and report each phase.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run, in order, and record pass or fail from the command output:
|
|
6
|
+
|
|
7
|
+
1. `cargo fmt --check`
|
|
8
|
+
2. `cargo clippy --all-targets -- -D warnings`
|
|
9
|
+
3. `cargo test`
|
|
10
|
+
4. `uv run ruff check .`
|
|
11
|
+
5. `uv run ruff format --check .`
|
|
12
|
+
6. `uv run pytest`
|
|
13
|
+
7. `python3 scripts/release_subject.py --self-test`
|
|
14
|
+
8. `python3 scripts/release_publish.py --self-test`
|
|
15
|
+
9. `python3 scripts/test_guardrails.py`
|
|
16
|
+
|
|
17
|
+
Say what you did not run. Do not claim a phase passed without its output.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Warn or deny a git commit whose subject misses the hybrid convention."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
13
|
+
TYPES_PATH = ROOT / "scripts" / "commit-types.json"
|
|
14
|
+
MESSAGE = re.compile(r"""(?:-m|--message)(?:=|\s+)(?:"([^"]+)"|'([^']+)'|(\S+))""")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def subject_pattern() -> re.Pattern[str]:
|
|
18
|
+
"""Subject regex built from scripts/commit-types.json."""
|
|
19
|
+
names = [item["name"] for item in json.loads(TYPES_PATH.read_text(encoding="utf-8"))["types"]]
|
|
20
|
+
joined = "|".join(names)
|
|
21
|
+
return re.compile(rf"^\S+\s+(?:{joined})(\([a-z0-9][a-z0-9._-]*\))?(!)?: [a-z].*")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def commit_subject(command: str) -> str | None:
|
|
25
|
+
"""Return the -m subject when this command is a git commit."""
|
|
26
|
+
if not re.search(r"\bgit\s+commit\b", command):
|
|
27
|
+
return None
|
|
28
|
+
match = MESSAGE.search(command)
|
|
29
|
+
if match is None:
|
|
30
|
+
return None
|
|
31
|
+
return next(group for group in match.groups() if group)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def decision(command: str, profile: str | None = None) -> tuple[int, str | None]:
|
|
35
|
+
"""Return (exit code, warning). Exit 2 denies. Standard only warns."""
|
|
36
|
+
subject = commit_subject(command)
|
|
37
|
+
if subject is None or subject_pattern().match(subject):
|
|
38
|
+
return 0, None
|
|
39
|
+
message = f"commit subject does not match <emoji> <type>(<scope>): <imperative>: {subject}"
|
|
40
|
+
mode = profile if profile is not None else os.environ.get("SDLC_HOOK_PROFILE", "standard")
|
|
41
|
+
if mode == "strict":
|
|
42
|
+
return 2, message
|
|
43
|
+
if mode == "minimal":
|
|
44
|
+
return 0, None
|
|
45
|
+
return 0, message
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> int:
|
|
49
|
+
"""Read the hook payload and apply the profile."""
|
|
50
|
+
payload = json.load(sys.stdin)
|
|
51
|
+
command = str(payload.get("tool_input", {}).get("command", ""))
|
|
52
|
+
code, warning = decision(command)
|
|
53
|
+
if warning:
|
|
54
|
+
print(warning, file=sys.stderr)
|
|
55
|
+
return code
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Ask before destructive git commands. A plain git push is allowed."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
FORCE_PUSH = re.compile(r"\bgit\s+push\b[^\n]*--force\b|\bgit\s+push\b[^\n]*--force-with-lease\b")
|
|
11
|
+
FORCE_FLAG = re.compile(r"\bgit\s+push\b[^\n]*(?:^|\s)-f(?:\s|$)")
|
|
12
|
+
PLUS_REF = re.compile(r"\bgit\s+push\b[^\n]*\s\+\S+")
|
|
13
|
+
RESET_HARD = re.compile(r"\bgit\s+reset\b[^\n]*--hard\b")
|
|
14
|
+
CLEAN_FORCE = re.compile(r"\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f")
|
|
15
|
+
BRANCH_DELETE = re.compile(r"\bgit\s+branch\b[^\n]*(?:^|\s)-D(?:\s|$)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def needs_ask(command: str) -> str | None:
|
|
19
|
+
"""Return a reason to ask, or None when the command may run."""
|
|
20
|
+
if "git" not in command:
|
|
21
|
+
return None
|
|
22
|
+
if "--no-verify" in command:
|
|
23
|
+
return "git command skips hooks (--no-verify)"
|
|
24
|
+
if FORCE_PUSH.search(command) or FORCE_FLAG.search(command) or PLUS_REF.search(command):
|
|
25
|
+
return "force-push"
|
|
26
|
+
if RESET_HARD.search(command):
|
|
27
|
+
return "git reset --hard"
|
|
28
|
+
if CLEAN_FORCE.search(command):
|
|
29
|
+
return "git clean -f"
|
|
30
|
+
if BRANCH_DELETE.search(command):
|
|
31
|
+
return "git branch -D"
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def hook_response(command: str) -> dict | None:
|
|
36
|
+
"""Claude Code PreToolUse payload, or None to allow."""
|
|
37
|
+
reason = needs_ask(command)
|
|
38
|
+
if reason is None:
|
|
39
|
+
return None
|
|
40
|
+
return {
|
|
41
|
+
"hookSpecificOutput": {
|
|
42
|
+
"hookEventName": "PreToolUse",
|
|
43
|
+
"permissionDecision": "ask",
|
|
44
|
+
"permissionDecisionReason": f"git guardrail: {reason}",
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> int:
|
|
50
|
+
"""Read the hook payload on stdin and print a decision when needed."""
|
|
51
|
+
payload = json.load(sys.stdin)
|
|
52
|
+
command = str(payload.get("tool_input", {}).get("command", ""))
|
|
53
|
+
response = hook_response(command)
|
|
54
|
+
if response is not None:
|
|
55
|
+
print(json.dumps(response))
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Block publish and deploy commands when stack guardrails are opted in.
|
|
3
|
+
|
|
4
|
+
Opt in with `.claude/stack-guardrails.enabled` at the git root, or
|
|
5
|
+
`SDLC_STACK_GUARDRAILS=1`. The block exits 2 in every profile, including
|
|
6
|
+
standard. Dry-run publishes are allowed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
PUBLISH = re.compile(r"\b(?:npm|pnpm|yarn|uv)\s+publish\b|\bchangeset\s+publish\b")
|
|
18
|
+
RAILWAY = re.compile(
|
|
19
|
+
r"\brailway\s+(?:up|deploy|redeploy|down)\b"
|
|
20
|
+
r"|\brailway\s+variables\s+--set\b"
|
|
21
|
+
r"|\brailway\s+config\s+apply\b"
|
|
22
|
+
r"|\brailway\s+config\s+migrate\b[^\n]*--apply\b"
|
|
23
|
+
)
|
|
24
|
+
LOCKFILE = re.compile(r"(?:^|/)(?:uv\.lock|pnpm-lock\.yaml)$")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def enabled(start: Path | None = None) -> bool:
|
|
28
|
+
"""True when the repo opted in or the environment variable is set."""
|
|
29
|
+
if os.environ.get("SDLC_STACK_GUARDRAILS") == "1":
|
|
30
|
+
return True
|
|
31
|
+
here = start or Path.cwd()
|
|
32
|
+
for parent in [here, *here.parents]:
|
|
33
|
+
marker = parent / ".claude" / "stack-guardrails.enabled"
|
|
34
|
+
if marker.is_file():
|
|
35
|
+
return True
|
|
36
|
+
if (parent / ".git").exists():
|
|
37
|
+
return marker.is_file()
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def blocked_reason(command: str) -> str | None:
|
|
42
|
+
"""Return why a shell command is blocked, ignoring publish dry-runs."""
|
|
43
|
+
if not enabled():
|
|
44
|
+
return None
|
|
45
|
+
if PUBLISH.search(command) and "--dry-run" not in command:
|
|
46
|
+
return "package publish is blocked by stack guardrails"
|
|
47
|
+
if RAILWAY.search(command):
|
|
48
|
+
return "railway deploy is blocked by stack guardrails"
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def lockfile_warning(path: str) -> str | None:
|
|
53
|
+
"""Warn on a hand edit of a lockfile this harness does not regenerate."""
|
|
54
|
+
if not enabled():
|
|
55
|
+
return None
|
|
56
|
+
if LOCKFILE.search(path):
|
|
57
|
+
return f"hand edit of {path}; prefer the package manager that owns it"
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> int:
|
|
62
|
+
"""Apply the pre-bash block or the post-edit lockfile warning."""
|
|
63
|
+
payload = json.load(sys.stdin)
|
|
64
|
+
tool = str(payload.get("tool_name", ""))
|
|
65
|
+
tool_input = payload.get("tool_input", {})
|
|
66
|
+
if tool == "Bash":
|
|
67
|
+
reason = blocked_reason(str(tool_input.get("command", "")))
|
|
68
|
+
if reason:
|
|
69
|
+
print(reason, file=sys.stderr)
|
|
70
|
+
return 2
|
|
71
|
+
return 0
|
|
72
|
+
path = str(tool_input.get("file_path", ""))
|
|
73
|
+
warning = lockfile_warning(path)
|
|
74
|
+
if warning:
|
|
75
|
+
print(warning, file=sys.stderr)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Size, typing, and test shape for Python and Rust
|
|
3
|
+
paths: "**/*.py, **/*.rs"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coding standards
|
|
7
|
+
|
|
8
|
+
- Functions stay within about 30 lines. Files stay within about 300 lines.
|
|
9
|
+
- No magic numbers. Name them.
|
|
10
|
+
- `TODO` comments name an issue: `TODO(#123)`.
|
|
11
|
+
- Python tooling is `uv` only. Ruff lints and formats. Scripts under `scripts/` that ship in CI are stdlib, so CI does not need a venv for those scripts.
|
|
12
|
+
- Rust: `cargo fmt` and `cargo clippy --all-targets -- -D warnings`. Guard logic stays in `src/`.
|
|
13
|
+
- Tests are arrange, act, assert. One assertion. No logic in the test.
|
|
14
|
+
|
|
15
|
+
There is no TypeScript in this repository.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Secrets, injection, and browser sinks
|
|
3
|
+
paths: "**/*.py, **/*.rs, **/*.yaml, **/*.yml, **/*.json"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security
|
|
7
|
+
|
|
8
|
+
- No hard-coded secrets. No secrets in logs.
|
|
9
|
+
- No `eval`. No unsanitized `innerHTML` or `dangerouslySetInnerHTML`.
|
|
10
|
+
- No `*` CORS on a deployment that is not loopback-only.
|
|
11
|
+
- The guards are regex and heuristic checks. Do not describe them as a complete injection, bias, or compliance control.
|
|
12
|
+
- PII mappings stay in memory and are returned to the caller. Do not log the mapping or the original text.
|
|
13
|
+
- The output validator checks top-level JSON `type` and `required` only. It is not full JSON Schema.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"attribution": {
|
|
3
|
+
"commit": "",
|
|
4
|
+
"pr": ""
|
|
5
|
+
},
|
|
6
|
+
"includeCoAuthoredBy": false,
|
|
7
|
+
"hooks": {
|
|
8
|
+
"PreToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": ".claude/hooks/scripts/git-guardrails.sh"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": ".claude/hooks/scripts/commit-style.sh"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": ".claude/hooks/scripts/stack-guardrails.sh"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"matcher": "Edit|Write",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": ".claude/hooks/scripts/stack-guardrails.sh"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"PostToolUse": [
|
|
37
|
+
{
|
|
38
|
+
"matcher": "Edit|Write",
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": ".claude/hooks/scripts/stack-guardrails.sh"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-workflow
|
|
3
|
+
description: Commit subjects, pull request shape, and direct releases from main. Use when committing, opening a PR, or changing release automation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git workflow
|
|
7
|
+
|
|
8
|
+
## Commit subject
|
|
9
|
+
|
|
10
|
+
`<emoji> <type>(<scope>): <lower-case imperative>`. Under 72 characters. No trailing period. The body says why. Footers are `Closes #N`, `Fixes #N`, `Refs #N`, or `BREAKING CHANGE:`. No `Co-authored-by` and no "Generated with" trailer.
|
|
11
|
+
|
|
12
|
+
Types and emoji are the table in `AGENTS.md` and `scripts/commit-types.json`. Extra emoji that keep the same type: 🔥 🚚 🚨 💚 🎉 🔖.
|
|
13
|
+
|
|
14
|
+
## Pull request
|
|
15
|
+
|
|
16
|
+
Title is the subject. One label from `feature`, `bug`, `refactor`, `upgrade`, `docs`, `internal`, `breaking`, `security`.
|
|
17
|
+
|
|
18
|
+
Body:
|
|
19
|
+
|
|
20
|
+
- **What and why**, with `Closes #N`
|
|
21
|
+
- **Evidence**: Before, After, Not verified
|
|
22
|
+
- **Merge danger**: Door (one-way or two-way), Blast radius, Rollback
|
|
23
|
+
- **Breaking changes, migrations, config**
|
|
24
|
+
|
|
25
|
+
Add a Mermaid diagram when the architecture or the data flow changes.
|
|
26
|
+
|
|
27
|
+
Branch from `main` as `feat/issue-N-short-name` or `fix/issue-N-short-name`. Do not force-push a shared branch.
|
|
28
|
+
|
|
29
|
+
## Release
|
|
30
|
+
|
|
31
|
+
A releasable push to `main` publishes in one workflow run. `scripts/release_subject.py` takes the highest bump since the last tag and consumes `## Unreleased`. The job commits `🔖 chore(release): vX.Y.Z`, pushes `main` without force, and tags that commit. The release commit does not publish again. The same run builds the abi3 wheels and sdist from that tag and publishes them to PyPI.
|
|
32
|
+
|
|
33
|
+
## Merge
|
|
34
|
+
|
|
35
|
+
Only when the human asks: `gh pr merge <number> --squash --delete-branch`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stack-guardrails
|
|
3
|
+
description: Opt-in blocks for publish and deploy, plus the uv, ruff, and cargo checks this library expects. Use before publishing, deploying, or changing the toolchain.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stack guardrails
|
|
7
|
+
|
|
8
|
+
Opt in by committing an empty `.claude/stack-guardrails.enabled` at the git root, or export `SDLC_STACK_GUARDRAILS=1`. This repository commits the marker, so the blocks are on.
|
|
9
|
+
|
|
10
|
+
The hook exits 2, in every profile, for:
|
|
11
|
+
|
|
12
|
+
- `npm publish`, `pnpm publish`, `yarn publish`, `uv publish`, `changeset publish` (a `--dry-run` is allowed)
|
|
13
|
+
- `railway up`, `deploy`, `redeploy`, `down`, `variables --set`, `config apply`, `config migrate --apply`
|
|
14
|
+
|
|
15
|
+
It warns on a hand edit of `uv.lock` or `pnpm-lock.yaml`. `Cargo.lock` is committed on purpose. Do not gitignore it. Regenerate `uv.lock` with `uv lock`, do not hand-edit it.
|
|
16
|
+
|
|
17
|
+
## Python and Rust
|
|
18
|
+
|
|
19
|
+
Python is 3.11 in CI and 3.14 in `.python-version`. Do not select a newer interpreter. Do not raise `requires-python`, the setup-uv `python-version`, or Ruff `target-version` past 3.14. Ruff's `target-version` is `py311`. Tooling is `uv`.
|
|
20
|
+
|
|
21
|
+
Lint and format with `uv run ruff check .` and `uv run ruff format --check .`. Test with `uv run pytest`.
|
|
22
|
+
|
|
23
|
+
Rust's bar is `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test`. The stable channel is `rust-toolchain.toml`. CI passes that channel into `dtolnay/rust-toolchain` because the action is pinned by commit SHA.
|
|
24
|
+
|
|
25
|
+
There is no TypeScript app and no Node toolchain.
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": {
|
|
3
|
+
"type-enum": [
|
|
4
|
+
2,
|
|
5
|
+
"always",
|
|
6
|
+
[
|
|
7
|
+
"feat",
|
|
8
|
+
"fix",
|
|
9
|
+
"docs",
|
|
10
|
+
"style",
|
|
11
|
+
"refactor",
|
|
12
|
+
"perf",
|
|
13
|
+
"test",
|
|
14
|
+
"build",
|
|
15
|
+
"ci",
|
|
16
|
+
"chore",
|
|
17
|
+
"deps",
|
|
18
|
+
"security",
|
|
19
|
+
"revert"
|
|
20
|
+
]
|
|
21
|
+
],
|
|
22
|
+
"scope-enum": [
|
|
23
|
+
1,
|
|
24
|
+
"always",
|
|
25
|
+
[
|
|
26
|
+
"core",
|
|
27
|
+
"api",
|
|
28
|
+
"docs",
|
|
29
|
+
"deps",
|
|
30
|
+
"ci",
|
|
31
|
+
"release",
|
|
32
|
+
"repo",
|
|
33
|
+
"agents",
|
|
34
|
+
"commands",
|
|
35
|
+
"hooks",
|
|
36
|
+
"rules",
|
|
37
|
+
"security"
|
|
38
|
+
]
|
|
39
|
+
],
|
|
40
|
+
"header-max-length": [2, "always", 72]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Size, typing, and test shape for Python and Rust
|
|
3
|
+
globs: "**/*.{py,rs}"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Follow `.claude/rules/coding-standards.md` and `AGENTS.md`.
|
|
8
|
+
|
|
9
|
+
Python tooling is uv and ruff. Rust guard logic stays in `src/`. Do not rename `llm_guardrails`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-workflow
|
|
3
|
+
description: Commit subjects, pull request shape, and direct releases from main. Use when committing, opening a PR, or changing release automation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git workflow
|
|
7
|
+
|
|
8
|
+
## Commit subject
|
|
9
|
+
|
|
10
|
+
`<emoji> <type>(<scope>): <lower-case imperative>`. Under 72 characters. No trailing period. The body says why. Footers are `Closes #N`, `Fixes #N`, `Refs #N`, or `BREAKING CHANGE:`. No `Co-authored-by` and no "Generated with" trailer.
|
|
11
|
+
|
|
12
|
+
Types and emoji are the table in `AGENTS.md` and `scripts/commit-types.json`. Extra emoji that keep the same type: 🔥 🚚 🚨 💚 🎉 🔖.
|
|
13
|
+
|
|
14
|
+
## Pull request
|
|
15
|
+
|
|
16
|
+
Title is the subject. One label from `feature`, `bug`, `refactor`, `upgrade`, `docs`, `internal`, `breaking`, `security`.
|
|
17
|
+
|
|
18
|
+
Body:
|
|
19
|
+
|
|
20
|
+
- **What and why**, with `Closes #N`
|
|
21
|
+
- **Evidence**: Before, After, Not verified
|
|
22
|
+
- **Merge danger**: Door (one-way or two-way), Blast radius, Rollback
|
|
23
|
+
- **Breaking changes, migrations, config**
|
|
24
|
+
|
|
25
|
+
Add a Mermaid diagram when the architecture or the data flow changes.
|
|
26
|
+
|
|
27
|
+
Branch from `main` as `feat/issue-N-short-name` or `fix/issue-N-short-name`. Do not force-push a shared branch.
|
|
28
|
+
|
|
29
|
+
## Release
|
|
30
|
+
|
|
31
|
+
A releasable push to `main` publishes in one workflow run. `scripts/release_subject.py` takes the highest bump since the last tag and consumes `## Unreleased`. The job commits `🔖 chore(release): vX.Y.Z`, pushes `main` without force, and tags that commit. The release commit does not publish again. The same run builds the abi3 wheels and sdist from that tag and publishes them to PyPI.
|
|
32
|
+
|
|
33
|
+
## Merge
|
|
34
|
+
|
|
35
|
+
Only when the human asks: `gh pr merge <number> --squash --delete-branch`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stack-guardrails
|
|
3
|
+
description: Opt-in blocks for publish and deploy, plus the uv, ruff, and cargo checks this library expects. Use before publishing, deploying, or changing the toolchain.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stack guardrails
|
|
7
|
+
|
|
8
|
+
Opt in by committing an empty `.claude/stack-guardrails.enabled` at the git root, or export `SDLC_STACK_GUARDRAILS=1`. This repository commits the marker, so the blocks are on.
|
|
9
|
+
|
|
10
|
+
The hook exits 2, in every profile, for:
|
|
11
|
+
|
|
12
|
+
- `npm publish`, `pnpm publish`, `yarn publish`, `uv publish`, `changeset publish` (a `--dry-run` is allowed)
|
|
13
|
+
- `railway up`, `deploy`, `redeploy`, `down`, `variables --set`, `config apply`, `config migrate --apply`
|
|
14
|
+
|
|
15
|
+
It warns on a hand edit of `uv.lock` or `pnpm-lock.yaml`. `Cargo.lock` is committed on purpose. Do not gitignore it. Regenerate `uv.lock` with `uv lock`, do not hand-edit it.
|
|
16
|
+
|
|
17
|
+
## Python and Rust
|
|
18
|
+
|
|
19
|
+
Python is 3.11 in CI and 3.14 in `.python-version`. Do not select a newer interpreter. Do not raise `requires-python`, the setup-uv `python-version`, or Ruff `target-version` past 3.14. Ruff's `target-version` is `py311`. Tooling is `uv`.
|
|
20
|
+
|
|
21
|
+
Lint and format with `uv run ruff check .` and `uv run ruff format --check .`. Test with `uv run pytest`.
|
|
22
|
+
|
|
23
|
+
Rust's bar is `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test`. The stable channel is `rust-toolchain.toml`. CI passes that channel into `dtolnay/rust-toolchain` because the action is pinned by commit SHA.
|
|
24
|
+
|
|
25
|
+
There is no TypeScript app and no Node toolchain.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ──────────────────────────────────────────────
|
|
2
|
+
# LLM Guardrails -- Environment Configuration
|
|
3
|
+
# ──────────────────────────────────────────────
|
|
4
|
+
# Copy this file to .env and fill in the values.
|
|
5
|
+
|
|
6
|
+
# Guardrails behaviour --------------------------------------------------------
|
|
7
|
+
# Injection-detection threshold (0.0 = permissive, 1.0 = strict)
|
|
8
|
+
GUARDRAILS_INJECTION_THRESHOLD=0.5
|
|
9
|
+
|
|
10
|
+
# Enable / disable individual guards (true | false)
|
|
11
|
+
GUARDRAILS_PII_ENABLED=true
|
|
12
|
+
GUARDRAILS_INJECTION_ENABLED=true
|
|
13
|
+
GUARDRAILS_BIAS_ENABLED=true
|
|
14
|
+
GUARDRAILS_OUTPUT_VALIDATION_ENABLED=true
|
|
15
|
+
|
|
16
|
+
# Maximum allowed output length (characters)
|
|
17
|
+
GUARDRAILS_MAX_OUTPUT_LENGTH=4096
|
|
18
|
+
|
|
19
|
+
# API server -------------------------------------------------------------------
|
|
20
|
+
API_HOST=0.0.0.0
|
|
21
|
+
API_PORT=8000
|
|
22
|
+
API_LOG_LEVEL=info
|