The Python Probe SDK for Sentinel — instrument a Python service as a Sentinel Probe: the in-process component that reports what your service really did, and asks Sentinel whether to proceed before an action becomes irreversible.
Sentinel verifies cross-system action sequences against rules you declare, using recorded evidence rather than logs and dashboards. A Probe is how your code joins that contract.
- Python 3.9 or later.
- A running Sentinel deployment to connect to.
The package is not yet on PyPI. Until it is, install it from this repository:
python3 -m venv .venv
.venv/bin/python -m pip install --editable .The generated protobuf types are committed, so a bare clone builds with no code generator.
import time
from sentinel_probe_sdk import (
Config,
Deps,
GateOutcomeKind,
Options,
ProbeClient,
gate,
generate_idempotency_key,
generate_request_id,
new_probe_tracer,
)
# Connect to Sentinel's decision endpoint and identify this Probe.
# The decider carries requests to Sentinel — see sentinel_probe_sdk.client.transport.
probe = ProbeClient(Config(source_handle="checkout-service"), decider)
# The tracer your service emits evidence with. The TracerProvider (and its
# exporter) stay yours — pass span processors via Options.provider_options.
tracer = new_probe_tracer(
Options(tracer_name="checkout-service", service_name="checkout-service")
)
with tracer.tracer.start_as_current_span("charge.card") as span:
... # the work the Specification watches
# Convert the ended span into the event Sentinel reasons about.
event = tracer.to_event(conversion) # SpanConversion: the ended span + its identity
# Ask before the action becomes irreversible.
outcome = gate(
event,
probe.current_filter(),
None, # deadline_ns: a monotonic deadline, if you set one
Deps(
decide=probe.decide_func(),
now_monotonic_ns=time.monotonic_ns,
accepted_fail_mode_for=accepted_fail_mode, # what you contracted to
),
Options(
source_handle=probe.source_handle(),
request_id=generate_request_id(),
idempotency_key=generate_idempotency_key(),
),
)
if outcome.kind not in (
GateOutcomeKind.PERMIT,
GateOutcomeKind.FAIL_OPEN_PERMIT,
GateOutcomeKind.NO_FILTER, # no filter held: the conservative default, proceed
):
rollback() # deny | defer | fail-closed: block, or retry within the budget
else:
commit_the_action()When Sentinel publishes a new Event Filter epoch for this source, swap it in:
probe.set_filter(new_filter) # a no-op when the epoch is unchanged| Module | Duty |
|---|---|
sentinel_probe_sdk.client |
Holds the versioned Event Filter for your source; builds decision requests (ProbeClient, FilterStore, the Connect transport) |
sentinel_probe_sdk.filter |
Relevance projection before shipping — drops attributes no Specification needs. Never samples (apply_filter) |
sentinel_probe_sdk.emission |
OTel spans become Sentinel events (new_probe_tracer, span_to_event) |
sentinel_probe_sdk.enforcement |
The blocking decision — permit / deny / defer, with per-Specification fail modes and a monotonic latency budget (gate) |
sentinel_probe_sdk.config |
Source-tier resolution from deployment config, never hard-coded (load_source_tier_config, tier_for_handle) |
Early, pre-1.0. The wire protocol is versioned per release, but the API surface may still change.
See CONTRIBUTING.md. This repository is a published mirror; pull requests are reviewed and re-landed upstream.
This repository pins its own toolchain — devbox.json + devbox.lock — and every task
runs inside it, the same convention as the Sentinel source repository:
devbox install # once; resolves devbox.lock
devbox shell # then `just --list` for the recipes
build, test, lint and fmt-check reuse the canonical gate names of the source
repository, scoped to this one language.