sagashield 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.
- sagashield-0.1.0/.claude-plugin/marketplace.json +30 -0
- sagashield-0.1.0/.dockerignore +32 -0
- sagashield-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
- sagashield-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- sagashield-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- sagashield-0.1.0/.github/workflows/ci.yml +111 -0
- sagashield-0.1.0/.github/workflows/release-binaries.yml +102 -0
- sagashield-0.1.0/.github/workflows/release-pypi.yml +81 -0
- sagashield-0.1.0/.gitignore +8 -0
- sagashield-0.1.0/BENCHMARK.md +94 -0
- sagashield-0.1.0/CHANGELOG.md +65 -0
- sagashield-0.1.0/CONTRIBUTING.md +46 -0
- sagashield-0.1.0/Cargo.lock +804 -0
- sagashield-0.1.0/Cargo.toml +33 -0
- sagashield-0.1.0/DISTRIBUTION.md +82 -0
- sagashield-0.1.0/Dockerfile +39 -0
- sagashield-0.1.0/LICENSE-APACHE +201 -0
- sagashield-0.1.0/LICENSE-MIT +21 -0
- sagashield-0.1.0/PKG-INFO +324 -0
- sagashield-0.1.0/README.md +308 -0
- sagashield-0.1.0/RELEASING.md +35 -0
- sagashield-0.1.0/SECURITY.md +102 -0
- sagashield-0.1.0/SPEC.md +432 -0
- sagashield-0.1.0/claude_desktop_config.example.json +14 -0
- sagashield-0.1.0/evals/scenarios.rs +196 -0
- sagashield-0.1.0/examples/demo.rs +137 -0
- sagashield-0.1.0/examples/otel_export.rs +61 -0
- sagashield-0.1.0/examples/python_agent_demo.py +91 -0
- sagashield-0.1.0/examples/run_evals.rs +521 -0
- sagashield-0.1.0/examples/security_demo.rs +148 -0
- sagashield-0.1.0/integrations/claude_code.md +38 -0
- sagashield-0.1.0/integrations/claude_desktop.json +9 -0
- sagashield-0.1.0/integrations/cursor.json +10 -0
- sagashield-0.1.0/pyproject.toml +26 -0
- sagashield-0.1.0/python/sagashield/__init__.py +181 -0
- sagashield-0.1.0/python/sagashield/integrations/__init__.py +1 -0
- sagashield-0.1.0/python/sagashield/integrations/langchain.py +93 -0
- sagashield-0.1.0/scripts/package_local.bat +34 -0
- sagashield-0.1.0/scripts/package_local.sh +44 -0
- sagashield-0.1.0/src/audit.rs +179 -0
- sagashield-0.1.0/src/bin/sagashield-mcp.rs +40 -0
- sagashield-0.1.0/src/dispatcher.rs +446 -0
- sagashield-0.1.0/src/error.rs +81 -0
- sagashield-0.1.0/src/fsm.rs +227 -0
- sagashield-0.1.0/src/lib.rs +102 -0
- sagashield-0.1.0/src/main.rs +5 -0
- sagashield-0.1.0/src/mcp/mod.rs +638 -0
- sagashield-0.1.0/src/python.rs +340 -0
- sagashield-0.1.0/src/replay.rs +154 -0
- sagashield-0.1.0/src/security/mod.rs +5 -0
- sagashield-0.1.0/src/security/policy.rs +322 -0
- sagashield-0.1.0/src/tools/mod.rs +278 -0
- sagashield-0.1.0/src/traits.rs +56 -0
- sagashield-0.1.0/src/types.rs +192 -0
- sagashield-0.1.0/src/wal.rs +727 -0
- sagashield-0.1.0/tests/concurrency_stress_test.rs +147 -0
- sagashield-0.1.0/tests/crash_recovery_test.rs +109 -0
- sagashield-0.1.0/tests/dispatcher_integration_test.rs +131 -0
- sagashield-0.1.0/tests/dlq_and_human_loop_test.rs +290 -0
- sagashield-0.1.0/tests/fsm_guardrail_test.rs +189 -0
- sagashield-0.1.0/tests/idempotency_test.rs +104 -0
- sagashield-0.1.0/tests/mcp_protocol_test.rs +371 -0
- sagashield-0.1.0/tests/python_binding_test.py +147 -0
- sagashield-0.1.0/tests/replay_and_audit_test.rs +149 -0
- sagashield-0.1.0/tests/security_fuzz_test.rs +234 -0
- sagashield-0.1.0/tests/security_sandbox_test.rs +278 -0
- sagashield-0.1.0/tests/wal_rollback_test.rs +213 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sagashield-marketplace",
|
|
3
|
+
"description": "ACID transactional Saga runtime, Step-0 security guardrail, and MCP server for autonomous AI agents.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "SagaShield Contributors"
|
|
7
|
+
},
|
|
8
|
+
"metadata": {
|
|
9
|
+
"description": "Claude Code marketplace for the sagashield MCP server (sagashield-mcp over stdio).",
|
|
10
|
+
"version": "0.1.0"
|
|
11
|
+
},
|
|
12
|
+
"plugins": [
|
|
13
|
+
{
|
|
14
|
+
"name": "sagashield",
|
|
15
|
+
"source": "./",
|
|
16
|
+
"description": "Transactional tool execution with SQLite WAL Saga rollback, FSM guardrails, Step-0 sandboxing, idempotency, replay and OpenTelemetry audit.",
|
|
17
|
+
"version": "0.1.0",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "SagaShield Contributors"
|
|
20
|
+
},
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"sagashield": {
|
|
23
|
+
"command": "sagashield-mcp",
|
|
24
|
+
"args": [],
|
|
25
|
+
"description": "Run `cargo build --release --bin sagashield-mcp` and ensure the binary is on PATH, or set an absolute command path."
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Rust build artifacts
|
|
2
|
+
target/
|
|
3
|
+
# VCS
|
|
4
|
+
.git/
|
|
5
|
+
.gitignore
|
|
6
|
+
# Python environments and caches
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.pyc
|
|
11
|
+
*.pyo
|
|
12
|
+
*.pyd
|
|
13
|
+
# Generated eval artifacts (reproducible via `cargo run --example run_evals`)
|
|
14
|
+
evals/results/
|
|
15
|
+
# Local run state (sandbox output, MCP databases, demo leftovers)
|
|
16
|
+
workspace/
|
|
17
|
+
*.db
|
|
18
|
+
*.db-wal
|
|
19
|
+
*.db-shm
|
|
20
|
+
*.db-journal
|
|
21
|
+
agent_demo_output.txt
|
|
22
|
+
agent_test_output.txt
|
|
23
|
+
# Local packaging output
|
|
24
|
+
dist/
|
|
25
|
+
# Fuzz corpora artifacts (reproducible)
|
|
26
|
+
fuzz/artifacts/
|
|
27
|
+
fuzz/corpus/
|
|
28
|
+
# Editors / OS
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Something behaves differently than specified
|
|
4
|
+
title: "fix: "
|
|
5
|
+
labels: bug
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Expected behavior
|
|
9
|
+
|
|
10
|
+
What should happen, per docs / `SPEC.md` / `BENCHMARK.md`?
|
|
11
|
+
|
|
12
|
+
## Observed behavior
|
|
13
|
+
|
|
14
|
+
What happens instead? Paste the exact error, WAL rows, or eval numbers.
|
|
15
|
+
|
|
16
|
+
## Minimal reproduction
|
|
17
|
+
|
|
18
|
+
```rust
|
|
19
|
+
// or: minimal MCP transcript / python snippet / eval scenario
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Steps: commands run, seed used, config (features, OS).
|
|
23
|
+
|
|
24
|
+
## Environment
|
|
25
|
+
|
|
26
|
+
- SagaShield version (`Cargo.toml` / `pip show sagashield`):
|
|
27
|
+
- OS:
|
|
28
|
+
- Rust (`rustc --version`) / Python (`python --version`):
|
|
29
|
+
|
|
30
|
+
## Security relevance
|
|
31
|
+
|
|
32
|
+
Does this touch `src/security/`, Step-0, rollback, or the WAL?
|
|
33
|
+
If it may be a vulnerability, use **Security → Report a vulnerability**
|
|
34
|
+
(private advisory) instead — see `SECURITY.md`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Propose a capability with its transactional implications
|
|
4
|
+
title: "feat: "
|
|
5
|
+
labels: enhancement
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Real problem
|
|
9
|
+
|
|
10
|
+
What concrete agent failure does this address? (orphaned charges, dirty
|
|
11
|
+
sagas, injection class, missing observability…)
|
|
12
|
+
|
|
13
|
+
## Proposed solution
|
|
14
|
+
|
|
15
|
+
API sketch (Rust and/or Python signatures), defaults, migration path.
|
|
16
|
+
|
|
17
|
+
## Transactional impact (HARD vs BEST-EFFORT)
|
|
18
|
+
|
|
19
|
+
Per `README.md` guarantees: does this change a **deterministic**
|
|
20
|
+
property (FSM, WAL, Step-0, local compensation) or a **best-effort**
|
|
21
|
+
one (remote compensations, planner behavior, OS confinement)?
|
|
22
|
+
How is a partial failure observed (WAL status, OTel span, MCP error)?
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
What changes, and why (link issues: `Fixes #…`).
|
|
4
|
+
|
|
5
|
+
## Contributor checklist (mandatory)
|
|
6
|
+
|
|
7
|
+
- [ ] `cargo fmt` executed (`cargo fmt --check` clean)
|
|
8
|
+
- [ ] `cargo clippy --all-targets --all-features -- -D warnings` clean
|
|
9
|
+
- [ ] Unit/integration tests added or updated — no regressions (32+ tests green)
|
|
10
|
+
- [ ] `docs.rs` documentation updated (`///` on new public items)
|
|
11
|
+
- [ ] Zero `.unwrap()` introduced in `src/` (typed `KernelError` only)
|
|
12
|
+
- [ ] Security surface considered (`SECURITY.md` note if the threat model changes)
|
|
13
|
+
- [ ] `Cargo.toml` / `pyproject.toml` versions still in sync (if touched)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
CARGO_TERM_COLOR: always
|
|
11
|
+
PYO3_PYTHON: python
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: fmt + clippy (zero tolerance)
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
20
|
+
with:
|
|
21
|
+
components: rustfmt, clippy
|
|
22
|
+
- uses: Swatinem/rust-cache@v2
|
|
23
|
+
- run: cargo fmt --check
|
|
24
|
+
- run: cargo clippy --all-targets --all-features -- -D warnings
|
|
25
|
+
|
|
26
|
+
test-rust:
|
|
27
|
+
name: rust ${{ matrix.os }}
|
|
28
|
+
runs-on: ${{ matrix.os }}
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest, windows-latest]
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
36
|
+
- uses: Swatinem/rust-cache@v2
|
|
37
|
+
- run: cargo test --all-targets --all-features
|
|
38
|
+
- run: cargo test --doc
|
|
39
|
+
|
|
40
|
+
test-python:
|
|
41
|
+
name: python ${{ matrix.python }} (${{ matrix.os }})
|
|
42
|
+
runs-on: ${{ matrix.os }}
|
|
43
|
+
strategy:
|
|
44
|
+
fail-fast: false
|
|
45
|
+
matrix:
|
|
46
|
+
os: [ubuntu-latest, windows-latest]
|
|
47
|
+
python: ["3.10", "3.11", "3.12"]
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: ${{ matrix.python }}
|
|
53
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
54
|
+
- uses: Swatinem/rust-cache@v2
|
|
55
|
+
- run: python -m venv .venv
|
|
56
|
+
# maturin develop requires an active virtualenv: drive it explicitly
|
|
57
|
+
# per-OS (venv layouts differ between Unix and Windows).
|
|
58
|
+
- name: build + test (Unix)
|
|
59
|
+
if: runner.os != 'Windows'
|
|
60
|
+
shell: bash
|
|
61
|
+
run: |
|
|
62
|
+
.venv/bin/pip install maturin
|
|
63
|
+
VIRTUAL_ENV="$PWD/.venv" .venv/bin/maturin develop --features python
|
|
64
|
+
.venv/bin/python tests/python_binding_test.py
|
|
65
|
+
- name: build + test (Windows)
|
|
66
|
+
if: runner.os == 'Windows'
|
|
67
|
+
shell: pwsh
|
|
68
|
+
run: |
|
|
69
|
+
.venv/Scripts/pip install maturin
|
|
70
|
+
$env:VIRTUAL_ENV = "$PWD/.venv"
|
|
71
|
+
.venv/Scripts/maturin develop --features python
|
|
72
|
+
.venv/Scripts/python tests/python_binding_test.py
|
|
73
|
+
|
|
74
|
+
eval-benchmarks:
|
|
75
|
+
name: evals (zero residual corruption)
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
80
|
+
- uses: Swatinem/rust-cache@v2
|
|
81
|
+
- run: cargo run --example run_evals --release
|
|
82
|
+
- name: assert zero corruption
|
|
83
|
+
shell: python
|
|
84
|
+
run: |
|
|
85
|
+
import csv
|
|
86
|
+
with open("evals/results/summary.csv") as fh:
|
|
87
|
+
rows = list(csv.DictReader(fh))
|
|
88
|
+
bad = [r for r in rows if int(r["residual_corruption"]) != 0
|
|
89
|
+
or int(r["breaches"]) != 0 or int(r["duplicates"]) != 0]
|
|
90
|
+
# Baseline is *expected* to fail; only sagashield rows must be clean.
|
|
91
|
+
bad = [r for r in bad if r["config"] == "sagashield"]
|
|
92
|
+
assert not bad, f"sagashield regressions: {bad}"
|
|
93
|
+
print("sagashield evals clean: 0 residual, 0 breaches, 0 duplicates")
|
|
94
|
+
|
|
95
|
+
fuzz-smoke:
|
|
96
|
+
name: fuzz smoke (nightly, time-boxed)
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
- uses: dtolnay/rust-toolchain@nightly
|
|
101
|
+
- uses: Swatinem/rust-cache@v2
|
|
102
|
+
- run: cargo install cargo-fuzz --locked
|
|
103
|
+
- name: build fuzz targets
|
|
104
|
+
working-directory: fuzz
|
|
105
|
+
run: cargo +nightly fuzz build -O
|
|
106
|
+
- name: smoke run path_guard (60s)
|
|
107
|
+
working-directory: fuzz
|
|
108
|
+
run: cargo +nightly fuzz run path_guard -- -max_total_time=60 -max_len=512 -print_final_stats=1
|
|
109
|
+
- name: smoke run net_guard (60s)
|
|
110
|
+
working-directory: fuzz
|
|
111
|
+
run: cargo +nightly fuzz run net_guard -- -max_total_time=60 -max_len=256 -print_final_stats=1
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
name: release-binaries
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
CARGO_TERM_COLOR: always
|
|
9
|
+
BIN: sagashield-mcp
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: ${{ matrix.target }} (${{ matrix.os }})
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
include:
|
|
19
|
+
- os: windows-latest
|
|
20
|
+
target: x86_64-pc-windows-msvc
|
|
21
|
+
archive: sagashield-VERSION-windows-x64.zip
|
|
22
|
+
bin_name: sagashield-mcp.exe
|
|
23
|
+
- os: ubuntu-latest
|
|
24
|
+
target: x86_64-unknown-linux-gnu
|
|
25
|
+
archive: sagashield-VERSION-linux-x64.tar.gz
|
|
26
|
+
bin_name: sagashield-mcp
|
|
27
|
+
- os: macos-14
|
|
28
|
+
target: aarch64-apple-darwin
|
|
29
|
+
archive: sagashield-VERSION-darwin-arm64.tar.gz
|
|
30
|
+
bin_name: sagashield-mcp
|
|
31
|
+
- os: macos-13
|
|
32
|
+
target: x86_64-apple-darwin
|
|
33
|
+
archive: sagashield-VERSION-darwin-x64.tar.gz
|
|
34
|
+
bin_name: sagashield-mcp
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
38
|
+
with:
|
|
39
|
+
targets: ${{ matrix.target }}
|
|
40
|
+
- uses: Swatinem/rust-cache@v2
|
|
41
|
+
with:
|
|
42
|
+
key: release-${{ matrix.target }}
|
|
43
|
+
- name: build release binary
|
|
44
|
+
run: cargo build --release --bin ${{ env.BIN }} --target ${{ matrix.target }}
|
|
45
|
+
- name: stage package dir
|
|
46
|
+
shell: bash
|
|
47
|
+
run: |
|
|
48
|
+
VERSION="${GITHUB_REF_NAME#v}"
|
|
49
|
+
ARCHIVE="$(echo "${{ matrix.archive }}" | sed "s/VERSION/${VERSION}/")"
|
|
50
|
+
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
|
|
51
|
+
rm -rf stage && mkdir -p stage
|
|
52
|
+
cp "target/${{ matrix.target }}/release/${{ matrix.bin_name }}" stage/
|
|
53
|
+
cp README.md LICENSE-MIT stage/
|
|
54
|
+
- name: pack zip (Windows)
|
|
55
|
+
if: runner.os == 'Windows'
|
|
56
|
+
shell: pwsh
|
|
57
|
+
run: Compress-Archive -LiteralPath stage/* -DestinationPath "${{ env.ARCHIVE }}"
|
|
58
|
+
- name: pack tarball (Unix)
|
|
59
|
+
if: runner.os != 'Windows'
|
|
60
|
+
shell: bash
|
|
61
|
+
run: tar -czf "${{ env.ARCHIVE }}" -C stage .
|
|
62
|
+
- name: sha256
|
|
63
|
+
shell: bash
|
|
64
|
+
run: |
|
|
65
|
+
if command -v sha256sum >/dev/null; then
|
|
66
|
+
sha256sum "${{ env.ARCHIVE }}" > "${{ env.ARCHIVE }}.sha256"
|
|
67
|
+
else
|
|
68
|
+
certutil -hashfile "${{ env.ARCHIVE }}" SHA256 | sed -n '2p' | awk '{print $1 " '"${{ env.ARCHIVE }}"'"}' > "${{ env.ARCHIVE }}.sha256"
|
|
69
|
+
fi
|
|
70
|
+
cat "${{ env.ARCHIVE }}.sha256"
|
|
71
|
+
- uses: actions/upload-artifact@v4
|
|
72
|
+
with:
|
|
73
|
+
name: ${{ env.ARCHIVE }}
|
|
74
|
+
path: |
|
|
75
|
+
${{ env.ARCHIVE }}
|
|
76
|
+
${{ env.ARCHIVE }}.sha256
|
|
77
|
+
|
|
78
|
+
release:
|
|
79
|
+
name: github release + SHA256SUMS
|
|
80
|
+
needs: [build]
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
permissions:
|
|
83
|
+
contents: write
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/download-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
path: dist
|
|
88
|
+
merge-multiple: true
|
|
89
|
+
- name: aggregate SHA256SUMS.txt
|
|
90
|
+
shell: bash
|
|
91
|
+
run: |
|
|
92
|
+
cd dist
|
|
93
|
+
cat *.sha256 > SHA256SUMS.txt
|
|
94
|
+
cat SHA256SUMS.txt
|
|
95
|
+
sha256sum -c SHA256SUMS.txt
|
|
96
|
+
- uses: softprops/action-gh-release@v2
|
|
97
|
+
with:
|
|
98
|
+
draft: false
|
|
99
|
+
prerelease: ${{ contains(github.ref_name, '-') }}
|
|
100
|
+
generate_release_notes: true
|
|
101
|
+
files: |
|
|
102
|
+
dist/*
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: release-pypi
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
wheels:
|
|
9
|
+
name: wheel ${{ matrix.os }}
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
include:
|
|
15
|
+
# Windows x64 (cp38-abi3 via maturin)
|
|
16
|
+
- os: windows-latest
|
|
17
|
+
maturin-args: --release --features python --interpreter python3.10 python3.11 python3.12
|
|
18
|
+
# Linux x64 manylinux (cp38-abi3 via maturin)
|
|
19
|
+
- os: ubuntu-latest
|
|
20
|
+
maturin-args: --release --features python --interpreter python3.10 python3.11 python3.12
|
|
21
|
+
manylinux: auto
|
|
22
|
+
# macOS universal2 (Intel + Apple Silicon, single universal wheel)
|
|
23
|
+
- os: macos-latest
|
|
24
|
+
maturin-args: --release --features python --target universal2-apple-darwin --interpreter python3.10 python3.11 python3.12
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
28
|
+
with:
|
|
29
|
+
# universal2 needs both Apple toolchains present
|
|
30
|
+
targets: ${{ matrix.os == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
31
|
+
- uses: Swatinem/rust-cache@v2
|
|
32
|
+
with:
|
|
33
|
+
key: pypi-${{ matrix.os }}
|
|
34
|
+
- name: build wheels
|
|
35
|
+
uses: PyO3/maturin-action@v1
|
|
36
|
+
with:
|
|
37
|
+
command: build
|
|
38
|
+
manylinux: ${{ matrix.manylinux || 'off' }}
|
|
39
|
+
args: ${{ matrix.maturin-args }}
|
|
40
|
+
- uses: actions/upload-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: wheels-${{ matrix.os }}
|
|
43
|
+
path: target/wheels/*
|
|
44
|
+
|
|
45
|
+
sdist:
|
|
46
|
+
name: source distribution
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- name: build sdist
|
|
51
|
+
uses: PyO3/maturin-action@v1
|
|
52
|
+
with:
|
|
53
|
+
command: sdist
|
|
54
|
+
- uses: actions/upload-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: wheels-sdist
|
|
57
|
+
path: target/wheels/*
|
|
58
|
+
|
|
59
|
+
publish:
|
|
60
|
+
name: publish to PyPI
|
|
61
|
+
needs: [wheels, sdist]
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
environment:
|
|
64
|
+
name: pypi
|
|
65
|
+
url: https://pypi.org/p/sagashield
|
|
66
|
+
permissions:
|
|
67
|
+
# Trusted Publishing (OIDC): no token needed when the PyPI project
|
|
68
|
+
# is configured with this repo as publisher. Falls back to PYPI_API_TOKEN.
|
|
69
|
+
id-token: write
|
|
70
|
+
contents: read
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
path: dist
|
|
75
|
+
merge-multiple: true
|
|
76
|
+
- name: publish to PyPI (OIDC preferred, token fallback)
|
|
77
|
+
# If PYPI_API_TOKEN is unset, the empty password makes the action
|
|
78
|
+
# fall back to OIDC Trusted Publishing (id-token: write above).
|
|
79
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
80
|
+
with:
|
|
81
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SagaShield Evals — BASELINE vs SAGASHIELD (50 scenarios, seed=42)
|
|
2
|
+
|
|
3
|
+
Reproducible benchmark in the style of SWE-bench / Statewright evals:
|
|
4
|
+
one command, raw data, no hidden steps.
|
|
5
|
+
|
|
6
|
+
## Reproducibility Guide
|
|
7
|
+
|
|
8
|
+
Prerequisites: Rust 1.75+, a C compiler (for `rusqlite` bundled SQLite).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git clone https://github.com/sebastianmechno-sys/sagashield sagashield
|
|
12
|
+
cd sagashield
|
|
13
|
+
cargo run --example run_evals --release
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
What the command does:
|
|
17
|
+
|
|
18
|
+
1. Generates the 50-task suite deterministically (`evals/scenarios.rs`,
|
|
19
|
+
xorshift64* with `EVAL_SEED = 42` — no `rand` dependency, same suite on
|
|
20
|
+
every machine).
|
|
21
|
+
2. Runs every task twice in an isolated temp jail: once as a vanilla ReAct
|
|
22
|
+
agent (direct `std::fs` + in-memory ledger, exceptions caught but never
|
|
23
|
+
compensated), once through `AgentKernel` (Step-0 guard, FSM, SQLite WAL,
|
|
24
|
+
LIFO rollback, idempotency engine).
|
|
25
|
+
3. Writes `evals/results/raw_eval_data.json` (per-task input, both outcomes,
|
|
26
|
+
timestamps, nanosecond durations) and `evals/results/summary.csv`
|
|
27
|
+
(per-category aggregates), and prints the ASCII table below.
|
|
28
|
+
|
|
29
|
+
## Threat & Failure Model
|
|
30
|
+
|
|
31
|
+
Formal definitions used for grading (see `examples/run_evals.rs`):
|
|
32
|
+
|
|
33
|
+
- **Success (goal met).**
|
|
34
|
+
- *normal*: every step executed and every expected effect present.
|
|
35
|
+
- *crash*: the injected failure is reported **and** zero residuals remain.
|
|
36
|
+
- *adversarial*: the hostile write is rejected with nothing persisted
|
|
37
|
+
outside policy (rejection *is* the goal).
|
|
38
|
+
- *flaky*: the charge+write pair applied **exactly once** across the retry.
|
|
39
|
+
- **Residual Corruption (`residual`).** After a crash: any pre-crash file
|
|
40
|
+
still on disk, or any pre-crash charge not `REFUNDED` in the ledger.
|
|
41
|
+
- **Security Breach (`breach`).** A hostile payload (traversal escaping the
|
|
42
|
+
jail, `.env`/`credentials`/reserved-device/ADS name) accepted — i.e. not
|
|
43
|
+
rejected — by the agent layer. Baseline never rejects by construction.
|
|
44
|
+
- **Duplicate (`dupl`).** Redundant side-effect applications beyond the first
|
|
45
|
+
full pass (extra charges on retry).
|
|
46
|
+
- **Latency.** Wall time per tool step, p50/p99 in milliseconds. Includes
|
|
47
|
+
SQLite I/O for SagaShield, raw syscalls for baseline.
|
|
48
|
+
|
|
49
|
+
## The Numbers
|
|
50
|
+
|
|
51
|
+
Measured with `cargo run --example run_evals --release` (Windows 11,
|
|
52
|
+
SQLite bundled, seed=42). Raw data: `evals/results/raw_eval_data.json`.
|
|
53
|
+
|
|
54
|
+
| category | config | tasks | succ | rate% | residual | breach | dupl | p50ms | p99ms |
|
|
55
|
+
|-------------|------------|------:|-----:|-------:|---------:|-------:|-----:|------:|-------:|
|
|
56
|
+
| normal | baseline | 15 | 15 | 100.0 | 0 | 0 | 0 | 0.359 | 0.628 |
|
|
57
|
+
| normal | sagashield | 15 | 15 | 100.0 | 0 | 0 | 0 | 6.953 | 11.508 |
|
|
58
|
+
| crash | baseline | 15 | 0 | 0.0 | 15 | 0 | 0 | 0.002 | 0.655 |
|
|
59
|
+
| crash | sagashield | 15 | 15 | 100.0 | 0 | 0 | 0 | 9.449 | 17.335 |
|
|
60
|
+
| adversarial | baseline | 10 | 0 | 0.0 | 0 | 10 | 0 | 0.346 | 0.909 |
|
|
61
|
+
| adversarial | sagashield | 10 | 10 | 100.0 | 0 | 0 | 0 | 0.155 | 0.198 |
|
|
62
|
+
| flaky | baseline | 10 | 0 | 0.0 | 0 | 0 | 10 | 0.283 | 0.976 |
|
|
63
|
+
| flaky | sagashield | 10 | 10 | 100.0 | 0 | 0 | 0 | 5.304 | 10.478 |
|
|
64
|
+
| TOTAL | baseline | 50 | 15 | 30.0 | 15 | 10 | 10 | 0.332 | 0.909 |
|
|
65
|
+
| TOTAL | sagashield | 50 | 50 | 100.0 | 0 | 0 | 0 | 6.554 | 16.753 |
|
|
66
|
+
|
|
67
|
+
Reading: the vanilla agent scores 30% — perfect on the happy path, zero
|
|
68
|
+
everywhere else (15 dirty sagas, 10 accepted attacks, 10 double charges).
|
|
69
|
+
SagaShield scores 100% with zero residuals, zero breaches, zero duplicates.
|
|
70
|
+
Note the adversarial row: rejecting in 0.16 ms is *faster* than writing,
|
|
71
|
+
because Step-0 blocks before any I/O.
|
|
72
|
+
|
|
73
|
+
## Overhead Analysis
|
|
74
|
+
|
|
75
|
+
Honest cost accounting (release profile, per tool step):
|
|
76
|
+
|
|
77
|
+
- **SQLite WAL path** (`BEGIN`→`COMMITTED` + FSM transitions): p50 ≈ 6.6 ms
|
|
78
|
+
total vs 0.33 ms baseline — roughly one fsync-dominated SQLite transaction
|
|
79
|
+
per step on this machine. p99 ≈ 16.8 ms includes full LIFO rollbacks
|
|
80
|
+
(compensating writes + deletes), i.e. the p99 *does useful recovery work*
|
|
81
|
+
the baseline never performs.
|
|
82
|
+
- **Path screening** is sub-millisecond: adversarial rejections complete at
|
|
83
|
+
p50 0.155 ms / p99 0.198 ms — lexical normalization plus a handful of
|
|
84
|
+
substring checks, no syscalls on the hot path (canonicalization only
|
|
85
|
+
touches the FS best-effort for existing paths).
|
|
86
|
+
- **Idempotent retries** skip execution entirely (WAL lookup + cached return),
|
|
87
|
+
so the steady-state cost of a retry storm is one indexed `SELECT`, not a
|
|
88
|
+
re-execution.
|
|
89
|
+
|
|
90
|
+
Limitations: single-node SQLite (no distributed consensus — see
|
|
91
|
+
“Best-Effort Guarantees” in `README.md`); latencies measured on one Windows
|
|
92
|
+
11 host, re-run on your hardware via the command above; the suite uses
|
|
93
|
+
synthetic tools (`fs.write`, `mock.pay`, `crash.tool`), not live LLM
|
|
94
|
+
planners, so planner-quality variance is out of scope by design.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased] — v0.2.0-unreleased
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Claude Code plugin marketplace manifest (`.claude-plugin/marketplace.json`,
|
|
12
|
+
installable via `/plugin marketplace add`).
|
|
13
|
+
- `examples/otel_export.rs`: in-memory mini-saga (2 OK + 1 crash) printing
|
|
14
|
+
the OpenTelemetry audit document to stdout.
|
|
15
|
+
- `fuzz-smoke` CI job: nightly `cargo-fuzz` build plus time-boxed smoke run
|
|
16
|
+
of the `path_guard` / `net_guard` targets.
|
|
17
|
+
|
|
18
|
+
## [0.1.0] — 2026-09-08
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- SQLite WAL Saga engine: `sessions`/`actions` tables, LIFO rollback,
|
|
22
|
+
idempotency keys with `UNIQUE(session_id, idempotency_key)` index,
|
|
23
|
+
`busy_timeout` + WAL mode, dangling-session crash recovery.
|
|
24
|
+
- FSM guardrail: deterministic `Idle → Planning → ExecutingTool →
|
|
25
|
+
Verifying → Completed`, `Compensating → Failed`, default-deny with
|
|
26
|
+
`InvalidStateTransition`.
|
|
27
|
+
- Tool dispatcher: `AgentKernel` unifying Step-0 security, FSM, WAL and
|
|
28
|
+
automatic rollback; `ToolRegistry`, real `FsWriteTool` / `MockPaymentTool`
|
|
29
|
+
/ `CrashTool`.
|
|
30
|
+
- Step-0 security sandbox: lexical + symlink-aware path containment,
|
|
31
|
+
blocklist, Windows reserved names, 8.3 short names, ADS blocking,
|
|
32
|
+
trailing-dot/space normalization, domain whitelist with IP-literal
|
|
33
|
+
rejection.
|
|
34
|
+
- MCP server (`sagashield-mcp`, JSON-RPC 2.0 over stdio): `initialize`,
|
|
35
|
+
`tools/list`, `tools/call` (`fs_write`, `mock_pay`, `kernel_status`,
|
|
36
|
+
`agent_kernel_exec` gateway, `kernel_replay_session`,
|
|
37
|
+
`kernel_export_audit`).
|
|
38
|
+
- Deterministic session replay (`SessionReplay`) with formal FSM
|
|
39
|
+
re-validation, and OpenTelemetry audit export (`AuditExporter`).
|
|
40
|
+
- Python bindings (`pip install sagashield`, maturin + PyO3 abi3):
|
|
41
|
+
`SagaKernel`, `SecurityPolicy`, `@transactional_tool`,
|
|
42
|
+
`SecurityViolationError`, LangChain adapter.
|
|
43
|
+
- Eval suite: 50 deterministic scenarios (seed=42) with raw JSON + CSV
|
|
44
|
+
artifacts (`cargo run --example run_evals --release`).
|
|
45
|
+
- Coverage-guided fuzz targets (`fuzz/`: `path_guard`, `net_guard`) plus
|
|
46
|
+
a 1.300+ hostile-input corpus in `security_fuzz_test`.
|
|
47
|
+
- Docs: `README.md`, `SPEC.md`, `SECURITY.md`, `BENCHMARK.md`,
|
|
48
|
+
`CONTRIBUTING.md`, `RELEASING.md`, docs.rs API comments.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- `FsWriteTool` creates missing parent directories (recursive write).
|
|
52
|
+
- Crate re-branded from `agent-kernel` to `sagashield`
|
|
53
|
+
(binary `sagashield-mcp`, Python package `sagashield`).
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
- Documented application-level threat model and TOCTOU assumptions
|
|
57
|
+
(`SECURITY.md`); responsible-disclosure procedure via private advisories.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- SQLite `CURRENT_TIMESTAMP` defaults (previously inverted `datetime` args
|
|
61
|
+
returned `NULL` and violated `NOT NULL` constraints).
|
|
62
|
+
- `async fn` trait object-safety via `async-trait` for the tool registry.
|
|
63
|
+
|
|
64
|
+
[Unreleased]: https://github.com/sebastianmechno-sys/sagashield/compare/v0.1.0...HEAD
|
|
65
|
+
[0.1.0]: https://github.com/sebastianmechno-sys/sagashield/releases/tag/v0.1.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Contributing to SagaShield
|
|
2
|
+
|
|
3
|
+
## Commit convention
|
|
4
|
+
|
|
5
|
+
[Conventional Commits](https://www.conventionalcommits.org/):
|
|
6
|
+
|
|
7
|
+
- `feat:` new user-facing capability (minor bump on `0.x`)
|
|
8
|
+
- `fix:` bug fix with regression test (patch bump)
|
|
9
|
+
- `perf:` measurable performance change (include before/after numbers)
|
|
10
|
+
- `docs:` docs, README, SPEC, comments only
|
|
11
|
+
- `chore:` tooling, CI, deps, refactors with no behavior change
|
|
12
|
+
|
|
13
|
+
Breaking changes: use `feat!:` / `fix!:` and call them out in the PR body;
|
|
14
|
+
they require a CHANGELOG entry and maintainer approval.
|
|
15
|
+
|
|
16
|
+
## Non-negotiable invariants
|
|
17
|
+
|
|
18
|
+
1. **Zero `.unwrap()` / zero `.expect()` in library code (`src/`).**
|
|
19
|
+
Every failure is a typed `KernelError`. Tests and examples may use
|
|
20
|
+
`expect` with a message, never bare `unwrap`.
|
|
21
|
+
2. **Every public type and method documents itself for `docs.rs`.**
|
|
22
|
+
`///` comments must cover parameters, return values, and error cases.
|
|
23
|
+
`cargo doc --no-deps` must stay warning-free.
|
|
24
|
+
3. **Every feature ships with regression tests.** New behavior without a
|
|
25
|
+
failing-before/passing-after test is not merged. The suite
|
|
26
|
+
(`cargo test`: 32 integration tests + doctests) must stay green.
|
|
27
|
+
4. **`cargo clippy --all-targets --all-features -- -D warnings` is clean.**
|
|
28
|
+
No exceptions, no `#[allow]` without a linked issue explaining why.
|
|
29
|
+
5. **Security-sensitive changes** (anything under `src/security/`,
|
|
30
|
+
`src/dispatcher.rs` Step-0, new tool surface) additionally require:
|
|
31
|
+
- hostile-case tests (traversal, blocklist, spoofing variants),
|
|
32
|
+
- a `SECURITY.md` impact note when the threat model changes.
|
|
33
|
+
|
|
34
|
+
## Workflow
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cargo fmt --check # or: cargo fmt
|
|
38
|
+
cargo clippy --all-targets --all-features -- -D warnings
|
|
39
|
+
cargo test # 32 integration + doctests
|
|
40
|
+
cargo test --test security_fuzz_test # hostile corpus
|
|
41
|
+
cargo run --example run_evals --release # 0 residual corruption
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Python bindings: `maturin develop --features python`, then
|
|
45
|
+
`python tests/python_binding_test.py`. Keep `Cargo.toml` and
|
|
46
|
+
`pyproject.toml` versions in sync (checked at release time).
|