Thanks for your interest in Sponsio. This doc covers the practical bits:
how to set up a dev environment, where the seams are, and what we ask
of a patch before it lands on main.
Anything not covered here (design decisions, invariants, gotchas) lives in CLAUDE.md and docs/concepts/architecture.md. Skim those first if you plan to touch the runtime or add a pattern.
- Apache 2.0. By submitting a patch you agree your contribution is licensed under the repo's LICENSE.
- DCO sign-off required. See Developer Certificate of
Origin below. Every commit
must end with a
Signed-off-by:line.git commit -sadds it for you. - Be kind. See the Code of Conduct.
- Forks & brand. Apache 2.0 covers the code. The Sponsio name and logo are separate; you may fork freely if you rename, don't reuse the logo as your project's brand, and don't imply Sponsio Labs endorsement. Email hello@sponsio.dev with questions.
- Small PRs beat big PRs. One concern per PR. If a change is unavoidably large, split it into a stack and link the commits.
- Tests are not optional for any change that touches
sponsio/orts/packages/sdk/. Docs-only and CI-only changes are exempt.
Sponsio uses the Developer Certificate of Origin (DCO) v1.1 to track contribution provenance. We do not require a CLA; the DCO is a lightweight per-commit attestation that you have the right to contribute the code under Apache 2.0.
To sign off your commits, add -s to git commit:
git commit -s -m "feat(runtime): your change"This appends a line like:
Signed-off-by: Your Name <your.email@example.com>
By signing off, you certify the DCO terms: you wrote it (or have rights to it) and you're contributing it under the project's open-source licence.
If you forget the sign-off, amend the commit with git commit --amend -s and force-push to your branch. CI will block PRs that contain
unsigned commits.
Python 3.10+ is required. 3.12 is what CI runs on.
git clone https://github.com/SponsioLabs/Sponsio.git
cd Sponsio
pip install -e ".[all]" # core + every optional integration
pip install ruff pytest pytest-covOptional. If you'll be touching the TypeScript SDK:
cd ts/packages/sdk && npm installRun the full suite before you start, to make sure your environment is green:
pytest -v # 789+ tests, ~30s
ruff check sponsio/ tests/ # lint
ruff format --check sponsio/ tests/If ruff is not on your PATH, python -m ruff ... works the same.
High-level map. The full tour is in CLAUDE.md.
sponsio/
├── core.py entrypoint: sponsio.Sponsio()
├── config.py YAML loader
├── cli.py sponsio scan|validate|check|serve|demo|patterns
├── formulas/ LTL AST + evaluators
├── models/ Agent, Contract, System, Trace, Event
├── patterns/ deterministic pattern library
├── runtime/ RuntimeMonitor, strategies, terminal reporter
├── generation/ NL → contract (rules + optional LLM)
├── tracer/ event collection + grounding
├── integrations/ LangGraph, MCP, OpenAI, CrewAI, Agents, Vercel, Claude Agent
└── discovery/ docs/traces/code → proposed contracts
ts/packages/sdk/ TypeScript engine + integrations
tests/ pytest
docs/ user-facing documentation
Cross-cutting invariants. These MUST hold across any change; reviewers will reject PRs that break them:
sponsio/core has zero external dependencies. Framework deps go in[project.optional-dependencies].- All framework integrations inherit from
BaseGuard. No duplicated pre-check / post-check logic. - Det violations route to
DetBlockorEscalateToHumanonly.RuntimeMonitorenforces this routing. Don't bypass it. - The trace is append-only during a session. Rollback is only
permitted on a hard block, and only in
mode="enforce".
For anything larger than a typo fix, please open an issue before you start. That gives us a chance to steer, especially for new patterns, new integrations, or changes to the runtime.
Three issue templates exist:
- Bug Report: unexpected behavior, crashes, wrong verdicts.
- Feature Request: new capability or ergonomic improvement.
- New Constraint Pattern: proposal for a new deterministic pattern.
git checkout -b feat/short-descriptive-name
# ... make your change ...
pytest -v
ruff check sponsio/ tests/
ruff format sponsio/ tests/Branch naming is loose; these prefixes help reviewers scan:
feat/, fix/, docs/, refactor/, perf/, test/, ci/.
If you added a user-visible behavior, the task isn't done until you've touched these:
| Change | Update |
|---|---|
| New pattern | sponsio/patterns/library.py + sponsio/generation/dsl_to_contract.py + README.md Pattern Library table + docs/reference/patterns.md |
| New integration | sponsio/integrations/ + README.md Integrations table + docs/integrations/index.md |
| New CLI subcommand | sponsio/cli.py + docs/reference/cli.md + README.md |
| Public API change | CHANGELOG.md under [Unreleased] with ### Changed or ### Added |
| Bug fix | CHANGELOG.md under [Unreleased] with ### Fixed |
We follow Keep a Changelog for
CHANGELOG.md and SemVer for versioning.
We use Conventional Commits:
feat(runtime): shadow mode — observe contracts without blocking
fix(patterns): rate_limit off-by-one on sliding window
docs: clarify assume/enforce semantics in contracts.md
refactor(integrations): consolidate pre_check into BaseGuard
Scope is optional but encouraged. The body (when present) should explain why, not what. The diff already shows the what.
Use the PR template (it auto-populates). Fill in:
- What changed and why, in 1–3 sentences.
- Any invariants or design decisions worth calling out.
- Test plan: how you verified it.
- Docs touched (README, CHANGELOG, etc.), or "N/A" if none apply.
- Linked issue(s).
CI runs on every push: pytest across Python 3.10/3.11/3.12, TS SDK tests, ruff lint + format check. A green CI is required before review.
The mechanical path for a det pattern, end to end. The worked example is sanitized_before_sink(source, sanitizer, sink): after an untrusted source is read, the sanitizer must run before the sink does.
Add a factory to sponsio/patterns/library.py that returns a DetFormula. Use existing atoms (called, count, arg_has, arg_paths_within, ...) when you can.
def sanitized_before_sink(
source: str,
sanitizer: str,
sink: str,
desc: str = "",
) -> DetFormula:
"""After an untrusted source is read, require sanitization before sink use."""
_ensure_distinct(source, sanitizer, pattern="sanitized_before_sink",
arg_a="source", arg_b="sanitizer")
_ensure_distinct(sanitizer, sink, pattern="sanitized_before_sink",
arg_a="sanitizer", arg_b="sink")
_ensure_distinct(source, sink, pattern="sanitized_before_sink",
arg_a="source", arg_b="sink")
formula = G(Implies(
_called(source),
X(_forbidden_until(_called(sanitizer), _called(sink))),
))
return DetFormula(
formula=formula,
desc=desc or f"`{source}` must be sanitized by `{sanitizer}` before `{sink}`",
pattern_name="sanitized_before_sink",
args=(source, sanitizer, sink),
)Two conventions matter:
pattern_namemust match the function name. The pattern store, NL parser, andcustomized:overrides all key off this string.args=(...)captures the raw call arguments so the pattern store can round-trip them. Without it,sponsio packs,sponsio explain, and dashboard exports lose the user's original parameter values.
Most patterns compose existing atoms. Skip this step if yours does. If you do need a new atom, add it to sponsio/tracer/grounding.py with extraction logic, and register it in _CONTENT_PREDICATES if it takes parameters (regex, prefixes, ...).
sanitized_before_sink only uses called(...), so step 2 is N/A.
Add it to sponsio/generation/dsl_to_contract.py so users can express the pattern in the Sponsio contract DSL (the small set of phrasings the rule-based parser recognizes). Free-form NL beyond the DSL goes through the optional LLM extractor, not new regex rules here.
# top-of-file import
from sponsio.patterns.library import (
...,
sanitized_before_sink,
)
# _PATTERN_REGISTRY (structured form: `pattern: sanitized_before_sink`)
_PATTERN_REGISTRY = {
...,
"sanitized_before_sink": sanitized_before_sink,
}
# NL trigger rule (regex list + pattern name + expected arg count)
(
[
r"sanit(?:ize|ation).*before",
r"(?:source|input).*sanitizer.*sink",
],
"sanitized_before_sink",
3,
),
# In the pattern dispatch (after action extraction):
if pattern_name == "sanitized_before_sink":
if len(actions) < 3:
return _build_error(
nl_line, "sanitized_before_sink",
"sanitized_before_sink needs source, sanitizer, and sink actions",
)
formula = sanitized_before_sink(actions[0], actions[1], actions[2], desc=text)Two test files. Both are required.
tests/test_patterns.py covers formula correctness:
def test_sanitized_before_sink_requires_sanitizer_after_source():
af = sanitized_before_sink("web_fetch", "sanitize_input", "send_email")
assert af.pattern_name == "sanitized_before_sink"
assert evaluate(af.formula, [_called("web_fetch"), _called("send_email")]) is False
assert evaluate(
af.formula,
[_called("web_fetch"), _called("sanitize_input"), _called("send_email")],
)tests/test_nl_parser.py covers the NL round-trip:
def test_sanitized_before_sink(self):
r = parse_nl_rule_based(
"`web_fetch` input must be sanitized by `sanitize_input` before `send_email`"
)
assert r.ok
assert r.pattern_name == "sanitized_before_sink"For end-to-end (NL → guard → block / allow), tests/test_pattern_e2e.py is the right home.
The TS engine lives at ts/packages/sdk/src/core/patterns.ts. If your pattern composes atoms TS already grounds (called, count, arg_has, arg_field_has, arg_paths_within), mirror the factory there and add a TS test in ts/packages/sdk/src/__tests__/patterns.test.ts.
If your pattern uses an atom TS does not ground (LLM-observation atoms, data-flow predicates), add a row to docs/reference/ts-sdk-parity.md so TS users know the pattern is Python-only.
Three places. All required.
| File | What to add |
|---|---|
docs/reference/patterns.md |
Row in the appropriate category table (Safety / Compliance / Operational / Approval and audit / ...) with NL example and one-line "what it enforces". |
README.md |
Pattern Library mention if the pattern is high-leverage enough to feature. Ask in the PR. |
CHANGELOG.md |
Entry under [Unreleased] ### Added ("New pattern: sanitized_before_sink(source, sanitizer, sink) for taint-tracking gates."). |
Before opening the PR:
- Factory in
sponsio/patterns/library.pyreturnsDetFormulawith correctpattern_nameandargs. - Pattern registered in
dsl_to_contract.py(import, registry, dispatch). - Formula test in
tests/test_patterns.py. - NL test in
tests/test_nl_parser.py. - TS mirror landed, OR row added to
ts-sdk-parity.md. - Row added to
docs/reference/patterns.md. -
[Unreleased]entry inCHANGELOG.md. -
pytest -vandruff check sponsio/ tests/both green.
- Create
sponsio/integrations/<framework>.pywith aGuardclass that inherits fromBaseGuard. - Implement only the framework-specific interception.
BaseGuardalready owns pre-check, post-check, rollback, trace management, contract compilation, mode resolution, and session logging. - Register the framework name in
sponsio/core.pysosponsio.Sponsio(framework="<name>")picks up the new class. - Add an optional dep to
[project.optional-dependencies]inpyproject.toml. - Update the integrations table in
README.mdanddocs/integrations/index.md.
Do not open a public issue for security vulnerabilities. Instead,
email security@sponsio.dev with a description and a reproduction
path. We'll acknowledge within 72 hours and coordinate disclosure.
- GitHub Discussions for open-ended questions and ideas.
- GitHub Issues for bugs and concrete feature requests.
docs/for anything that's already been written up. Please check before filing.
Ship with open source: user-facing guides, contract and architecture reference, and design notes (e.g. docs/cost-based-thresholds.md).
Keep out of the public tree (or redact before publishing):
- Roadmaps, launch checklists, and status dashboards (
STATUS.md,PLAN.md,LAUNCH_*.md). They go stale and can imply commitments. - Narration scripts for a specific demo or video (
demo-video-script.mdis gitignored for that reason), not end-user documentation. - Benchmark result tables and eval lab notebooks. Headline figures may be published in the root
README.md; raw tables and model-by-model numbers stay private. Paths underdocs/that match those names are in.gitignore; nevergit add -f. - Internal agent/project notes under
agent_docs/or similar. - Anything with real customer names, private URLs, API keys, or unreleased product detail.
Runtime data: the whole data/ tree is local-only except the stub README; see data/README.md.
Thanks for contributing.