Thanks for helping. This page covers setting up, running the tests, adding a detector, and keeping the docs accurate. For anything bigger than a bug fix or a detector, open an issue first so we can agree on the approach.
- A detector request is the easiest first PR: one module in
interlock/detectors/, one test file, one catalog line. See Adding a detector. good first issueandhelp wantedare triaged to be self-contained.- A docs correction can go straight to a PR; if you would rather report it, use the documentation issue form.
- Anything larger — a new adapter, a policy-language change, a console screen — open an issue first, or check ROADMAP.md to see whether it is already planned.
| Surface | Path | Needs | Local gate |
|---|---|---|---|
| Python SDK | interlock/, tests/ |
Python 3.9–3.13 | make check |
| Node client | js/ |
Node 20, 22 or 24 | make check-js |
| Console UI | web/ |
Node ≥ 20.19 | make check-web |
| Docs site | docs/ |
Python ≥ 3.11 (Zensical) | the Docs workflow |
| Native scope store | rust/ |
Rust + maturin (optional) | make check-rust |
You only need the toolchain for the surface you change. The gates are the same
ones CI runs, and make check-all runs the first three.
git clone https://github.com/SourceShift/InterLock.git
cd InterLock
pip install -e ".[dev]"The package is pure Python with no build step and no runtime dependencies. The
dev extra adds pytest, langchain-core (for the LangChain adapter tests),
and the ruff version CI pins. Install the Node dependencies only for the
surface you touched:
npm --prefix js ci
npm --prefix web cipython -m pytest tests -q # the full Python suite
python -m pytest tests/test_guard.py -q # one module
python -m pytest tests -q -rs # also show why tests were skippedconftest.py puts the repo root on sys.path and resets the global engine
between tests, so tests don't leak rules into each other. Some sandbox tests skip
on hosts without the backend they need (bubblewrap on macOS, for example), and
-rs names the reason.
Before you push, run the same gate CI runs:
make check # ruff (E9,F), detector catalog --check, full pytest suitemake check needs only the Python toolchain. If your change touches js/ or
web/, run make check-js or make check-web too — both install from the
lockfile, test, and build the artifact that ships:
make check-js # npm ci, test, build for the Node client
make check-web # npm ci, typecheck, test, build for the console UI
make check-all # all threeThe console UI is a React/Vite app whose built bundle ships inside the Python package, so a type error is as user-visible as a Python bug. The dev server, the generated design screens, and how each screen binds to the API are covered in the console guide.
npm --prefix web run dev # dev server against a running console
npm --prefix web run typecheck # every binding checked against the view model
npm --prefix web test # vitest
npm --prefix web run build # web/dist/, the bundle the server servesThe Node.js client has its own suite: see js/README.md.
Use the Conventional Commits style the history already follows, with a scope where it helps:
feat(mcp): capability leases that expire, enforced at call time
fix(guard): apply MODIFY rewrites to positional arguments
docs: explain rule order in concepts
The pull request title is the squash commit on main, so give the PR the
title you want in the history. A maintainer edits the title if it is not in this
form rather than asking you to rename the branch.
-
Squash and merge is the default: one PR becomes one Conventional-Commits commit on
main, which keepsgit log --oneline mainreadable and a revert one command. If you split a series into commits that are each worth keeping on their own, say so and we will rebase-merge instead. Merge commits are not used onmain. -
The required check is
ci-ok, the single job that aggregates Python, Rust, Node, console and lint. A redci-okis not mergeable; the individual job names tell you which half broke. -
One approving review and resolved conversations, for anything that touches
interlock/,js/,web/or a workflow. Documentation-only PRs can land with checks green. -
DCO; there is no CLA. Sign off your commits with
git commit -s(Signed-off-by:line) to certify you wrote or have the right to contribute the change. There is no separate paperwork. -
Data comes with a warranty. A change that adds data to the mesh commons (indicators, fingerprints, redacted samples, or the sample strings in a pack's
*.tests.json) is covered by the sameSigned-off-by:, which then also warrants that the data:- was lawfully obtained;
- contains no personal data;
- contains no third party's confidential data.
You warrant that you have the data, not that you wrote it: attackers wrote the payloads. That data is CC0-1.0 under LICENSE-DATA; code and packs stay Apache-2.0 under LICENSE. A sample later found to hold personal data is erased as GOVERNANCE.md says.
-
Auto-merge is welcome once
ci-okis green and the PR has its review.
A detector is one module in interlock/detectors/ with a POLICY_ID and a
factory that returns a rule:
"""What attack this catches, why it's visible at this point, and what is matched.
The first sentence becomes the detector's description in the docs catalog, so
make it a plain summary.
"""
from typing import Optional
from ..enforce import Decision
from ..event import SensorEvent
from ..policy.engine import Rule
POLICY_ID = "my_guard"
def my_guard(*, extra_patterns=None) -> Rule:
def rule(event: SensorEvent) -> Optional[Decision]:
# return None for "no opinion", Decision.block(...) or Decision.modify(...) to act
return None
return ruleThe conventions every detector follows:
- Never raise on odd input. If
argsisn't a dict, a value isNone, or a type is unexpected, returnNone. In production a raising rule stops the call. - Prefer precision over recall. Match shapes that ordinary values won't contain, and explain the reasoning in the module docstring. A detector that fires on normal traffic gets switched off.
- One concern per detector, standard library only. Detectors stay dependency-free, like the core.
- Name arguments, never values. Set
attributed_toto the argument's name. - Scope it on purpose. If the detector only makes sense for certain tools,
keep their names in a module-level constant whose name contains
ACTIONS(for exampleEGRESS_ACTIONS). The catalog generator reads it to fill in "Scoped to". Accepting anactions=parameter lets users match their own tool names. - Test both sides. Add
tests/test_<name>.pycovering a match and a non-match. The non-match test is what keeps a detector from becoming noise. - Don't auto-register. Users import and install the detectors they want.
Then put it in the docs catalog:
-
Add the module name to the right category in
CATEGORIESinscripts/gen_detector_catalog.py. The script refuses to run while any detector is uncategorised. -
Regenerate the page:
python scripts/gen_detector_catalog.py
If the detector depends on an argument convention the generator can't see (a
special key like __origin__), add a line to SCOPE_NOTES in the same script.
The optional Rust crate in rust/ reimplements ScopeTree. Build and test it
with:
pip install maturin
cd rust && maturin develop --release && cd ..
python -c "from interlock.policy import SCOPE_BACKEND; print(SCOPE_BACKEND)" # -> rust
python -m pytest tests/test_scopes.py tests/test_scope_backend.py -qtests/test_scope_backend.py checks that the Python and Rust trees resolve
identically. If tests behave as though your change isn't there, look for a stale
interlock_scopes*.so in the repo root from an older build. It's git-ignored, so
git status won't show it, and it can shadow the module you just built. See
rust/README.md.
These docs have drifted from the code before, so treat a docs claim like code:
-
Run what you write. Every code snippet in
README.mdanddocs/was run, and the output shown is what it printed. If you change a snippet, run it again and paste the new output. -
Regenerate the catalog after touching a detector, and check it's current:
python scripts/gen_detector_catalog.py --check
-
Check numbers before you change them:
ls interlock/detectors/*.py | grep -v __init__ | wc -l # detector count python -m pytest tests -q | tail -1 # test count
-
Follow the docs layout. Tutorials go in
docs/getting-started.md, explanations indocs/concepts.mdanddocs/security-model.md, task-focused how-tos indocs/guides/, and lookup material indocs/reference/.docs/index.mdindexes them all. -
Match the voice. Second person, short sentences, plain words. Explain why before how. Lead a guide with the problem it solves, show output after code, and end with an honest "Limits" section. Say what doesn't work as clearly as what does.
CHANGELOG.md follows Keep a Changelog.
A PR that changes user-visible behaviour adds a line under ## [Unreleased]
in the matching heading (Added, Changed, Fixed, Deprecated, Removed,
Security) as part of the same PR. Internal refactors, tests and CI do not need
an entry.
While the version is 0.x, breaking changes land in the minor number; from 1.0
the project follows Semantic Versioning. Maintainers cut a release from main
by moving the Unreleased entries under a new version heading, bumping
version in pyproject.toml, js/package.json and web/package.json, and
tagging that commit. The tag publishes both packages; the steps and the
one-time trusted-publisher setup on PyPI and npm are in
docs/guides/releasing.md. Contributors do not bump
versions.
Two bits of this repo exist for the maintainer's agent-assisted workflow and are not part of a contributor's setup:
.githooks/plusscripts/install-worktree-guards.shkeep the primary checkout a fast-forward ofmainfor the maintainer's worktree loop. Do not run the installer: thepre-commitguard refuses every commit made in the primary checkout, which is not what you want on your own clone..mini-ork/, the vendored mini-ork engine, is git-ignored and local only.
Your normal fork → branch → pull request flow is the supported path.
Please don't open a public issue. See the security model.