All notable changes to this project are documented here.
The format is based on Keep a Changelog. Granular per-release notes (commits, PRs, individual fix lines) live in GitHub Releases. This file keeps the high-level shape: what was added, what changed, what broke.
-
Tier 2:
sudo sponsio install --system(macOS and Linux). A_sponsioservice user runs the daemon under launchd or systemd; the engine and hook shims are root-owned; the rules, history, audit log and config live in a directory only_sponsiocan open; Claude Code gets its hooks from managed settings (managed-settings.d), withallowManagedHooksOnlyon unless--allow-user-hooks. Libraries under~/.sponsio/pluginsare imported once.sponsio uninstall --systemremoves it. The install stops if a shared directory above Sponsio's own (/opt,/usr/local, ...) is writable by a user other than root. See running Sponsio on a host. -
Tier 3: complete mediation. The MCP gateway runs the upstream MCP servers with their credentials and gives the host one MCP server whose tool list follows the session's contract state.
sponsio run --sandboxstarts an agent in a sandbox (bubblewrap on Linux, Seatbelt on macOS, or a container recipe underdeploy/tier3) whose only route out is an egress proxy that decides every connection againstegress.allowand the_egresslibrary. -
SDK
backend="daemon". A guard can hand its decisions to the daemon, and every adapter offers the model the tools the session's state allows on each turn. -
Hash-chained audit log. Every record carries
seq,prev_hashandhash;sponsio admin audit verifychecks the chain against the daemon's head, and records can be exported over OTLP. The integrity of the log while the daemon is stopped depends on that remote copy. -
Signed rules. With
signing:indaemon.yaml, a covered bucket serves only rule files with a valid Ed25519 signature by the pinned key; anything unverified (an unsigned edit, a removal, a file that does not load) leaves the last signed rules enforcing, or holds the bucket. Needs thesigningextra. -
Project layer. A repository's
.sponsio/host.yamladds contracts for hook calls made in it, and can never weaken a protected one; attempts are recorded astamper_attempt. -
Standing approvals reach the runtime. "Always allow" in the console was recorded server-side only, so the SDK refused the same call on every run and re-asked a question a person had answered. A guard built from a
sponsio://rulebook now loads the project's standing approvals once at construction, andEscalateToHumanreleases a covered call (exact match on agent, tool and contract) asobserved, naming the approval, without paging anyone. Any fetch failure loads nothing and every escalation waits on a human as before. A standing approval can only release an escalation, never aDetBlockrule.SPONSIO_STANDING=0opts out. This matters since 0.2.0a17, whenEscalateToHumanstarted refusing the call: before that there was nothing to release. -
Proposal review on the admin socket (
sponsio admin proposals), and the daemon's own cloud key (sponsio admin cloud-key). -
A fleet of daemons paired with Sponsio Cloud.
sponsio pull -o FILEwrites the signature the cloud made over a published book toFILE.sig, and--public-keyrefuses to write anything that key did not sign;sponsio signing-keyprints thesigning:block to pin. Withcloud.proposals: truea daemon uploads its proposals and applies what a person decides in the console, only when its own copy matches what that person saw. Exported audit records carry a stream id kept in the state dir, so the cloud checks each daemon's chain on its own and catches a log cut while the daemon was stopped. See the daemon reference.
- A check costs the same on a long session. Contracts are evaluated incrementally: at 5,000 events a check takes about as long as at 500.
sponsio doctor --tierreports Tier 2 only when every guarantee holds on the machine as Claude Code will load it: managed settings and every directory above the shim and engine root-owned and not writable by your user, nodisableAllHooks, and a sandbox that covers this platform.- A call denied because its library would not load no longer tells the
agent how to let it through. The reason used to end "set
SPONSIO_HOOK_ON_ERROR=allow to fail open": the agent reads that reason,
and under the daemon that variable has no effect anyway (the switch is
on_errorindaemon.yaml). It now says the call is denied until the contract library is fixed. - A bucket whose rules must be signed refuses changes made on the
machine.
sponsio plugin append, a proposal, or approving one from before signing was turned on now fails withsigned: an appended file is no longer signed, so the daemon kept the last signed rules and the change was never enforced, while the proposal read approved. Change a signed bucket by publishing its book and pulling it. sponsio rules statusreads the files as they are now, not as the last hook call saw them, and shows a signed bucket whose file on disk is not what is enforced.
- Fixes from a second review of the Tier 2 and Tier 3 work: the audit log, rule signing, the MCP gateway, the system install and doctor (27 issues, each with a regression test).
sponsio pull -ocreates the bucket's directory on a first pull.
- Tamper evidence. Every compiled contract gets a stable id. A rule
library change that keeps every loaded contract applies at once and is
recorded as
ruleset_strengthened. One that removes or changes a contract is held: the daemon keeps enforcing what it loaded, recordsconfig_drift, and adds a note to the next deny.sponsio rules status | diff | reloadreviews and applies it; a daemon that restarts onto rules weaker than the last applied set denies that library until a reload. The daemon also hashes rule files, packs and its own modules every 2 s, restores a deleted session log from memory (trace_deleted), and records a call denied by the self-protection packs astamper_attempt.sponsio daemon run --watchapplies every change, for working on Sponsio's own rules. sponsio doctorreports the enforcement tier. One line in the report, andsponsio doctor --tierfor every guarantee of Tier 1, 2 and 3 with pass or fail and the fix. A default install is Tier 1. See the threat model.sponsio rules verifycompares each host bucket linked undercloud:indaemon.yamlwith the book the cloud publishes for its agent, and the daemon does so on its own everyverify_interval_s. Missing contracts are recorded ascloud_drift. Needs Sponsio Cloud's/v1/rulebook/verify.- Host config is backed up before every change.
host installandhost uninstallcopy the host's config file to<file>.sponsio-backup-<timestamp>before changing it, and write atomically; a symlinked config is written through.host uninstall --restore-backupputs the most recent backup back. - Docs: threat model, daemon reference, running Sponsio on a host, and the host hook path in benchmarks.
- Hook decisions come from
daemon.yaml, not the environment. The mode per host, the on-error policy, the policy for tools without rules, the plugin root and the trace TTL are read from<state-dir>/daemon.yaml.SPONSIO_GUARD_MODE,SPONSIO_MODE,SPONSIO_HOOK_ON_ERROR,SPONSIO_UNCONFIGURED,SPONSIO_SHIELD_TRACE_TTL_HOURSandSPONSIO_PLUGIN_ROOTno longer affect hooks, and a mode on the hook command or in the OpenClaw plugin's config is ignored. They still apply to the in-process SDK andhost guard --direct. Migration:sponsio host install <host> --mode observesets observe mode; anenventry in the host's settings no longer does. - Every hook path fails closed. An unknown host name, empty or
malformed input, a daemon error or an unreadable reply now denies, on
the shim, the daemon,
host guardand--direct. The OpenClaw plugin blocks a call it gets no verdict for unless itsfailOpenoption is set.on_error: allowstill lets an evaluation error through and each such call is recorded asfail_open. - Two daemon sockets. State-changing calls (
mode.set,rules.reload,plugin.append) move to an admin socket beside the agent socket (sponsio.admin.sock, mode 0600); every admin call is recorded with the caller's uid, gid and pid read from the kernel.plugin.appendfrom the agent side becomes a proposal. - A deny names the rule. The reason leads with the contract's own
desc, with the pattern's description after it in parentheses; a contract without a desc is named by its pattern. host installno longer creates the legacy_host,_host_subagentand_host_openclawbuckets and reports a bucket it just created as created.sponsio plugin initstill creates the legacy buckets.- Installed hooks run a shim that talks to the daemon, and fail closed.
sponsio host install claude-code/cursornow write a hook command that runssponsio_shim.py(standard library only) under the installing interpreter with-I -S, with the socket, plugin root, state dir and a 2 s deadline pinned on the command line. The shim forwards the event to the daemon, starts one (sponsio daemon run --detach) when none answers, and denies with the host's deny body and exit 2 when no daemon can be reached. It reads no environment variable, so anenvblock in the host settings no longer changes the mode or where the hook connects. Claude CodePreToolUseentries carry"onFailure": "block"and"timeout": 10, so a hook that cannot start blocks the call (Claude Code v2.1.295 or later; older releases ignore the key and the shim's own exit 2 is the only signal). Reinstall to switch existing hooks over. sponsio host guardandsponsio plugin guardgo through the daemon too, with the same fail-closed behaviour;--directkeeps the old in-process evaluation for debugging. The marketplace plugin'shooks.jsonaddsonFailureandtimeout.- The OpenClaw plugin talks to the daemon over its socket. It no longer
spawns
sponsio plugin guardper tool call.sponsio host install openclawwritessponsio-config.jsonbeside the deployed plugin (socket, plugin root, state dir, how to start the daemon); the plugin reads no environment variable, soSPONSIO_GUARD_BIN,SPONSIO_PLUGIN_ROOTandSPONSIO_GUARD_MODEno longer affect it, and theguardBin/pluginRoot/guardModemanifest keys are ignored. When no daemon answers the plugin starts one, and blocks the call if it still cannot be reached. Reinstall with--forceto replace an already deployed plugin. sponsio doctorchecks the host hooks. "Hook wiring" reports, per host, a hook whose interpreter or shim is gone, whose socket path cannot be bound, or that has no daemon and no way to start one, with the command that fixes it. "Claude Code" warns when the installed release predates v2.1.295 and so ignoresonFailure.sponsio host installrefuses to write a hook pinned to a socket path too long to bind.- One daemon per state dir. The daemon holds
<state-dir>/daemon.lockand never removes a socket another daemon answers on, so hooks that start it concurrently end with one daemon and one trace. Starts and restarts are recorded in<state-dir>/audit.jsonl.
- A fresh install held every default contract twice. The Claude Code and Cursor installers wrote the starter library without its bundle stamp, and the refresh after it kept every shipped contract as the user's beside a stamped copy. Each check ran every default twice. Reinstall to clean an affected library: duplicates of a shipped contract are dropped on upgrade.
- Session history is rebuilt in the order calls were admitted. Events
that tied on their timestamp could come back in a different order after
a rebuild, which flipped
cooldownand other ordering verdicts. The daemon now numbers each session's events (seq) and rebuilds by it. - Uninstall leaves nothing behind for a file Sponsio created. A host
config that held only Sponsio's entries is deleted without a backup,
with its earlier Sponsio-only backups, and an emptied skills directory
is removed. Cursor's uninstall no longer deletes a
hooks.jsonthat still holds the user's other keys. - A burst of hooks starting the daemon is not refused. The daemon listens with the system's full backlog.
- Each Claude Code session keeps its own history. Claude Code's hook
payload carries
session_id, notconversation_id, so every Claude Code session on a machine shared one trace. The history key is nowconversation_id, elsesession_id, prefixed with the host (claude-code:<session_id>) so two hosts cannot share one. Behaviour change: count-based rules (rate_limit,idempotent,bounded_retry) and ordering state now apply per session instead of accumulating across every session. A Task sub-agent carries its parent'ssession_idand stays in the parent's history. Cursor traces move to the prefixed key too; traces recorded under the old key are not carried over (they expire with the usual TTL). - The installer tests no longer write the developer's own host config.
Host config, skill and credential paths were fixed at import time, so a
test that moved
HOMEstill wrote the real~/.claude/settings.jsonand~/.cursor/hooks.json. They now resolve per call, and the suite stops and restores the file if any test writes under the real home.
- A rule written in YAML/LTL binds every spelling of its tool. The
pattern factories canonicalised their tool argument in a17; a rule
written as LTL (
called(Issue_Refund),Var(count, Issue_Refund)) and theArgValueterm keyed on the exact spelling, so they missedissue_refundandmcp__finance__issue_refund. The join now lives in the predicate key itself, on both runtimes.loop_detectionfollows a tool across an MCP prefix (search, thenmcp__web__search, used to reset the run), and a counter is no longer overwritten by a stale spelling. sponsio host installnames its own binary. A relative invocation (./venv/bin/sponsio host install) wrote whateversponsiowas first onPATHinto the hook, which on a machine with a second install was a different, possibly broken, binary; Claude Code runs the tool when a hook exits non-zero. The command now carries the absolute path of the install that ran, orpython -m sponsio.clifor a module invocation.--modeonhost installdoes what it says. It wrotedefaults.modeinto the host library, which the hook never read, so a default install printedobserveand blocked anyway. The hook now defaults toenforceopenly, and--mode observerides on the hook command (env SPONSIO_GUARD_MODE=observe ...), where the agent under guard cannot change it. Amode:line already in a host library is ignored, as it always was.
-
Second round on the kta1kri report, from the reporter's retest of 0.2.0a17. Three findings were not fully closed and two residuals remained.
--strictnever reached the Claude Code hook. Claude Code's hook payload carries nohostfield, so the hook routed every call to the legacy_hostbucket whilesponsio host install claude-code --stricthad writtendefaults.unconfigured: denyinto_host_claude_code. The posture was on disk and never read, andmcp__acme__*still ran. The hook now names the host it was installed for (host guard claude-coderoutes to_host_claude_code; ahostthe payload carries wins). Installs that only ever had_host/sponsio.yamlkeep enforcing through the existing legacy fallback, with its one-time notice; their trace history starts a new file under the per-host bucket.- The hook-wiring rule missed every path with a prefix. The redirect
pattern required
.claude/settings.jsonright after>, so> .claude/settings.jsonwas denied and> ~/.claude/settings.json,> $HOME/...and an absolute path were not.truncate -s0,ln -sf /dev/null,chmod 000andrm -rf ~/.claudewere not mutators at all, though each leaves the IDE loading no hooks.capability/host-config- integritynow matches a mutator verb anywhere before the path on the same simple command, adds those verbs, and refuses removing, moving or locking the directory that holds the wiring.capability/self-modifyand the per-host templates get the same verbs for the rule file (where the hook already fails closed on an empty or unreadable library). - The stateless exfiltration rule kept the ordered regex. 0.2.0a17
made the trace-aware rules order-free and left the per-event rule as
sender-then-flag-then-capture, so
SECRETS=$(env); curl -d "$SECRETS",curl -F "data=$(env)"andwget --post-data="$(env)"were denied only after an earlierprintenvin the same conversation. The stateless rule now matches the three parts independently with-F,--formand--post-dataas data flags; a captured dump ($(env),`printenv`) also arms the trace-aware window, which the trigger used to miss. - Two spellings still slipped the name join.
MCP__finance__issue_refund(prefix re-cased) andfinance__issue_refund(OpenClaw's unprefixed form) now answer toissue_refund, in both runtimes; the hook also routes a re-cased prefix to the server's own library instead of the host bucket. 5.000and€5.000read as 5. European grouping is now read when it is unambiguous (5.000,00,1.000.000), and one dot group behind a currency symbol or unit (€5.000,5.000 EUR) is refused as unevaluable rather than guessed either way. A bare5.000stays the float it is in every programming language.@sponsio/sdk0.2.0-alpha.7 was never published; npm still serves alpha.4, so the TypeScript side of a17 could not be retested.
-
The OpenAI integration says what it does not cover. Its docstring claimed every tool call was monitored; the Assistants (
client.beta.threads), Realtime and Batches APIs never were. The module docs and the integrations guide now name them. Reported by kta1kri.
EscalateToHumanrefuses the call by default. The docs described it as "refuse and notify", the outcome table said the tool does not run, and the agent was told the action was "paused awaiting human approval", butBaseGuardlet it run: a policy of "refunds over $10k need CFO approval" issued the refund and paged the CFO afterwards. The strategy now returnsaction="blocked"withescalation=True, so every adapter's stop gate refuses it andCheckResult.escalatedstill reports it.EscalateToHuman(hold=False)keeps the notify-only behaviour, and is what the monitor uses by default for a violated assumption. Reported as a naming concern by kta1kri.
sponsio host install <host> --strict. Writesdefaults.unconfigured: denyinto the host library, so a tool whose namespace has no contract library is refused instead of running with a warning. UnlikeSPONSIO_UNCONFIGURED=denyalone it survives a new shell, and the agent cannot flip it because the host library is undercapability/self-modify. The environment variable (deny/allow) still overrides it. Allowing remains the default.
-
Follow-ups to the kta1kri report, found while finishing it and while porting the fixes below to TypeScript.
- The library's numeric cap read
'$5,000'as 0. The fix below normalisedArgValuecomparisons only.arg_value_range, the cap YAML and the pattern factory produce, readsarg_numeric, whichint()/float()(parseFloatin TypeScript, which read'5,000'as 5) failed on; the variable stayed unset and the evaluator read it as 0, under every upper bound. Both runtimes now use the same normaliser, and a value that is present but not a number is refused asargs:unevaluableinstead of compared as 0. - Changing a guard's state behind its API is refused. Flipping
guard._monitor._mode, emptying the contract list, rebinding a formula or swapping a strategy forWarnOnlynow makes the next check refuse withguard:tampered, instead of enforcing the weakened rulebook. Code in the same interpreter can still defeat the check itself; this makes tampering loud, not impossible. - Removing the rules directory is refused.
capability/self-modifynamed the rule files, sorm -rf ~/.sponsioormv ~/.sponsio/plugins /tmptook every library with it and each tool then ran as unconfigured. - A cached cloud rulebook is verified before use. The copy used when the cloud is unreachable now carries an HMAC keyed with the API key; an edited copy, or one pulled before this release, is not enforced.
- OpenAI raw responses are checked.
responses.with_raw_response.createreturned a forbidden tool call unchecked, and the chat variant andwith_streaming_responseraisedAttributeError. The guard now parses the wrapper and raisesToolCallBlockedwhen a call must stop, since the caller can read the raw body. - A host reinstall keeps
defaults:. The smart merge rebuilt the file from the bundle and dropped the operator's block, so the next mode stamp could move an enforcing host back toobserve. - Missing-argument refusal sees MCP spellings.
mcp__shell__Bashwith no arguments was let through by a rule written forBash. sponsio.__version__was stuck at0.2.0a11through a16, sosponsio --versionandsponsio doctormisreported every release since. It is correct again and a test now pins it topyproject.toml.- TypeScript catches up with the fixes below. Tool-name canonicalisation, refusal of missing and unreadable arguments, and the once-per-value warning for a non-numeric value against a numeric guard were Python-only; the contract and prompt mirror is resynced.
- The library's numeric cap read
-
The deterministic layer no longer reads "cannot evaluate" as "not violated". An external report (kta1kri, 2026-09) confirmed that every framework adapter gates correctly on
stop_original, and found the weaknesses one layer down: a rule that could not be evaluated produced no violation, which is indistinguishable from a rule that passed. Nine findings, each reproduced before the fix and pinned by a test intests/test_fail_open_layer.py.- Tool names now meet on one spelling. Predicate keys are dict
lookups, so a rule written against
issue_refundwas silently inert against a call that arrived asIssue_Refund, with a trailing space, or asmcp__finance__issue_refund— our own documented MCP wire format. Withmust_precedecompiling toOr(order-holds, never-called), that read as "satisfied". Contracts now canonicalise their tool name and a call is grounded under every spelling a rule could have used, socalled,count,consecutive_countand the argument atoms all bind. Affectsmust_precede,always_followed_by,never_together,rate_limit,cooldown,destructive_action_gateand every othercalled-based pattern. - Numeric guards read the shapes a model writes. A cap that stopped
5000waved through'$5,000','5,000'and'5000 USD'. Currency symbols, unambiguous thousands grouping and trailing unit codes are normalised before coercion;'5,50'stays uncoerced rather than being guessed at. A non-numeric value against a numeric guard now warns instead of passing in silence. - Overflow agrees across runtimes.
'1e400'blocked on Python and passed on TypeScript for the identical contract. Both now keep the overflow and compare with it, so the cap fires on each. - A call whose arguments never arrived is refused. With
argsempty no argument atom is written, andarg_blacklist(Not(arg_field_has)) reported satisfied for a call nothing inspected. A tool some rule reads the arguments of is now refused when they are missing;SPONSIO_ALLOW_MISSING_ARGS=1restores the old behaviour. Tools with no argument rules are unaffected. guard.modereports the monitor's live value. It returned a cached copy, so it could sayenforcewhile the monitor had been moved toobserveand nothing was being enforced.- Every shipped host template protects its own hook wiring.
capability/host-config-integrityexisted but was included by no_host*.yaml, socapability/self-modifyguarded the rule files while.claude/settings.jsonand.cursor/hooks.jsonstayed writable. Emptying one of those removes the enforcer and leaves every other rule enforced by nobody. - The secret-exfiltration rule dropped its ordering assumption. It
required sender, then data flag, then capture, so the most natural way
to write the attack walked through:
SECRETS=$(env); curl -d "$SECRETS"puts the capture first. The three conditions are now matched independently, and-F/--form,--post-data,--post-fileand-T/--upload-filejoined the flag set.curl -d @./payload.jsonstays allowed. - A tool with no contract library says so. Any third-party MCP
server outside the shipped examples was allowed in silence, unlike the
sibling legacy-bucket fallback which warns. Allowing stays the
default posture, but it is now announced once per namespace on stderr,
and
SPONSIO_UNCONFIGURED=denyrefuses instead. - Count-based rules survive concurrency. The host hook did
load-history, decide, append with no lock, so parallel tool batches or
concurrent sub-agents each read the same count and each appended:
rate_limit(Bash, 1)admitted three to six of twenty concurrent calls. The whole cycle now holds an exclusive lock, and admits one. - Private vulnerability reporting is enabled on the repository, which
SECURITY.mdhad been advertising while the link returned 403.
- Tool names now meet on one spelling. Predicate keys are dict
lookups, so a rule written against
-
LangGraph
wrap_graph()/monitor_graph()now gate each node before it runs. The whole-graph wrappers iterated the inner graph's stream, ranguard_beforeonly after a node's update had been produced, and discarded the verdict, so a contract violation was recorded but nothing was stopped. Enforcement is now a callback merged into every run's config: LangGraph fires it before the node body executes, and a stopping verdict raisesToolCallBlockedso the node never runs. The gate coversinvoke/ainvoke/stream/astream/astream_events/batch/abatch(the async and batch entry points previously bypassed the wrapper entirely).invokealso no longer requires a checkpointer.monitor_graph()gained amode=argument. Reported by Trenyx (independent verification, 2026-09). -
The shell capability bundle's
rmrules now match long-form flags.rm --recursive --force /,rm -r --force ~,rm -rf --no-preserve-root /and similar spellings walked past the "Ban recursive deletes of sensitive roots" rule, whose regexes only accepted a single-[rRf]+cluster. The line-continuation and undefined-variablermrules had the same gap. All three now accept any option order with at least one recursive/force flag in short or long form, and anchorrmon a word boundary soperform -rf /no longer trips them. Reported by Trenyx. -
dangerous_bash_commands()default now catches every spelling of a recursiverm. The preset's default list carried the literal"rm -rf", sorm -fr,rm -Rf,rm -r,rm -f -randrm --recursive --forcewere not banned. The entry is now the regexRM_RECURSIVE_PATTERN(exported fromsponsio.patterns.libraryand@sponsio/sdk), which matches a recursive flag in short or long form in any option order. Non-recursiverm -f filestays allowed. The TypeScript default list now mirrors the Python one exactly (it was missingsudoand used a different order). -
Every adapter now gates the call the framework actually makes. A sweep of the other integrations for the LangGraph bug class found the same shape in several places; each is fixed and covered by a test that asserts the tool body never ran, and the TypeScript adapters were verified end to end against the real
ai,@openai/agentsand@langchain/langgraphpackages.- CrewAI (Python):
on_tool_startreturned a dict to refuse a call, but CrewAI only honoursFalse, so the tool ran. It now returnsFalseandon_tool_endswaps CrewAI's generic "blocked by hook" result for the contract violation.on_tool_endtakes the context alone (CrewAI passes nothing else),register_global_hooks()imports fromcrewai.hooks, a guard error refuses the call instead of being swallowed, and the docs no longer show theCrew(before_tool_call=)keyword, which CrewAI silently drops. - OpenAI Agents (Python): the SDK passes tool parameters positionally,
so the guard saw
{}and every argument contract passed. Arguments are now bound to parameter names. A@function_toolobject is wrapped aton_invoke_tool(it used to crash at wrap time). - OpenAI (Python):
guard.wrap(client)was a no-op inherited fromBaseGuard; it now guards that client.patch_openai()andwrap()coverchat.completions.parse()and the Responses API, and refusestream=Truewith a clear error instead of crashing. An evidence verdict that stops a response now also drops its tool calls. - MCP proxy: a refused call stayed in the trace and counted toward
rate limits; it is rolled back like
BaseGuard.guard_beforedoes. - Claude Agent (Python and TS): the PostToolUse hook read
tool_result; the SDK sendstool_response. - Claude Code / Cursor hook:
PostToolUseevents ran the pre-check and appended a second trace event per execution; they now record the tool output and gate nothing. A guard that cannot evaluate (a YAML syntax error in the library, an internal error) now denies instead of silently allowing;SPONSIO_HOOK_ON_ERROR=allowrestores the old behaviour. - Vercel AI (TS): with
aiv5 and later the middleware readresult.toolCalls, which no longer exists, so nothing was checked; both thecontentandtoolCallsshapes are handled, andwrapStreamgates streamed tool calls instead of passing them through. - OpenAI Agents (TS):
tool()exposesinvoke(runContext, input), so the guard was handed the run context as the arguments; it now reads the JSON input. - LangChain.js (TS): LangGraph's
ToolNodepasses the tool-call envelope toinvoke, soarg_field_haslooked for fields on the envelope; the envelope is unwrapped. - OpenAI (TS):
stream: truereturned the stream unchecked; it is refused.parse()and the Responses API are covered. Malformed arguments no longer record a phantom allowed call, and respect observe mode. - TS core: a contract pattern that is not a valid regular expression is refused at construction (as Python does) instead of never matching.
- All TS adapters gate on
stopOriginal, so a redirect verdict refuses the original call as it does in Python.
- CrewAI (Python):
Two more rules that looked armed and were not, and the answer to the one question an audit trail exists for.
-
A run records which rulebook version it enforced. Every ingested run showed no book at all, so the console could not say which rules were in force when it ran; replays showed a version because the server assigns theirs. The mechanism was already complete and read one source: the env var set while resolving
config="sponsio://project". An app that pulls the rulebook itself and handsSponsio()the resulting file — the shape the console's own wiring instructions produce — never went through it. The stamp is the checkout's first line, which the YAML parse drops as a comment;load_configkeeps it now and the bridge sends it. A hand-written yaml still records nothing, which is honest. -
The last agent in a checkout list lost its version. The list is bracketed, so
_own_bookreturnedzulu@v9], which parses as no version — a book nobody published. It only bit the agent that sorted last. -
**
toolbashcommand must not containrm -rfnever fired.** The NL parser took the banned shape as the FIELD NAME, building `arg_field_has('bash', 'rm -rf', 'rm -rf')` — a rule asking whether an argument *named* `rm -rf` contains `rm -rf`. No such argument exists. The field is read from the sentence's cue word now. `query` was also missing from the cue list, sotoolrun_sqlquery must not containDROP`` did not parse at all.
-
TypeScript refuses to enforce a book it could not parse. A contract that does not parse is a rule that is not there. Python raises; TypeScript logged one line and ran on, so a TS agent enforced a book with holes in it and reported success. Enforce mode throws now, naming every contract it could not read. Observe still runs, and says the rules are NOT armed.
-
arg_blacklist,requires_permissionandsegregation_of_dutyparse in TypeScript, using Python's own phrasings. Measured over fifteen realistic sentences TypeScript went from 11 to 13, level with Python. Note that TypeScript already supported the structuredpattern:form for the whole library — the gap was only in NL strings.
Found by running seven agent applications against the stack — a support desk, an ops bot, a load harness, one that passes hostile arguments, a TypeScript agent — rather than by adding tests to code already believed correct. Three of these are the same shape: a rule that loads, arms, shows in the console, and does not do what it says.
-
scope_limitwas a string prefix, not confinement. A rule reading "writes stay under/safe" allowed/safe/../../root/.ssh/id_rsa, because the check waspath.startswith(prefix)— the oldest escape there is, against a rule whose entire purpose is confinement. The same line also counted/safeguard/evil.txtas inside/safe: a prefix of the text is not a prefix of the path. Paths are normalised now and containment requires a segment boundary. Identical fix in TypeScript, which had the identical line. -
never call A after Bcompiled the opposite rule.no_reversal(commitment, contradiction)takes the committing action first, and English puts it last whenever the sentence opens with the prohibition. The contract appeared in the console carrying the user's own sentence while checking the reverse, and passed on exactly the sequence it was written to stop. Word order decides by position now, not by a list of fixed phrases. TypeScript had the same inversion with no swap at all. -
drop table userswalked pastdangerous_sql_verbs. The preset matched its verbs case-sensitively, and SQL keywords do not have a case. Word boundaries went on at the same time, soDELETEno longer matches the word "deleted" in a comment or a column nameddrop_date. -
An enforcing run could start with a rule missing. Strict compile read
defaults.modeand nothing else, so a project that said enforce in any of the four other places the runtime honors dropped the broken contract, printed oneUserWarning, and enforced the rest. Strictness follows the effective mode now, and a contract carrying its ownmode: enforceraises either way. -
sponsio doctorgreen-ticked a config that cannot load, because it validated the schema and never compiled the contracts. It compiles them now and grades by the project's mode. ItsRuntime modeline also read two of the three places a yaml can sayenforce, so a blocking project was reported asobserve (shadow — safe default). -
sponsio doctorfailed on a minimal install.find_specon a dotted name imports the parent package and raises when it is absent, so the check whose only job is to report what is missing crashed on a machine with nogoogle— which is whatpip install sponsiogives you. Doctor opened with a red mark on a healthy install. -
A pattern given the wrong number of arguments raised a bare
TypeErrorfrom inside the compiler, naming the factory's missing parameter and nothing about the contract. Errors now name the agent, the contract's own description, what it passed, and the signature. -
attach(guard, agents=["name"])— the obvious reading of the parameter — raisedstring indices must be integersfrom inside the bridge. A bare name is the shorthand now.
- A contract row sent to a console carries the
patternandargsit was built from, so a run whose agent has no published book is no longer offered the rules it was just checked against. tool_allowlistreads the same in both runtimes; TypeScript's NL parser understandsscope_limit, which it did not before.- CI runs the TypeScript half of
tests/cross_language/scenarios.json. The step was named "Run cross-language tests" and ran the unit tests, so the shared file gated Python alone — which is how two of the inversions above survived.
- The SMT hooks.
base.pyaccepted ansmt=config and importedsponsio.integrations.smt_middleware;CloudClient.smtimportedsponsio.cloud.smt. Neither module is in this build, so opting in raisedModuleNotFoundErrorat construction while the public docstring documented the feature. They return with the modules that make them work.
Follow-up to 0.2.0a8, all of it found by running a real multi-agent app
against a console that already held other agents' books.
- An agent's first run puts its book in the cloud. Nothing pushed
before: the checkout only pulls, and the one push an app tends to own
fires on a 404 — which stops happening the moment the project holds any
sibling's book. A project's second agent onward ran on rules the console
had never seen, so every screen built to show what governs an agent had
nothing to show. When the checkout does not carry the agent you named
and
./sponsio.yamldoes, that book now goes up: only that agent's block, as a draft, once — the next checkout carries it, and an identical push is a server-side no-op.SPONSIO_NO_AUTO_PUSH=1turns it off; a failed upload prints the manual command and never stops the run.
- A broken
./sponsio.yamlsays so. The fallback swallowed the load error, so a typo surfaced as "no ./sponsio.yaml defines it" — sending the user to look for a file sitting in the working directory defining exactly that agent. Both error paths now quote the parse failure.
docs/reference/config-yaml.mdnames all five places the mode comes from and which one wins. A contract's ownmode:beats the mode your code passes, so a rule armed in the console stops calls inside a run started withmode="observe".@sponsio/sdkreadsruntime.modeonly, sodefaults.modegoverns the Python runtime and not the TypeScript one.- Every install line in the READMEs, QUICKSTART and
docs/passes--pre. Without it pip resolves to0.1.1, the last stable — a build that predates the cloud console, the evidence lane and the current CLI.
The output lane ships. Alongside the action lane — every tool call checked before it executes — a run's claims are now checked too: typed assertions compared against an authority by deterministic comparators, with no LLM anywhere in the hot path.
Two of the fixes below were found by running a real multi-agent app against a console that already had other agents in it, which is the configuration that broke.
- Evidence middleware.
observe_llm_callextracts the typed claims a model turn makes, checks each against its authority, and folds the verdict into the trace beside the tool calls. Deterministic comparators; a claim whose source cannot answer is reported as such rather than guessed at.
- A project's second agent could not run. A cloud checkout carries
the whole project, so when a new agent joined a project that already
had books, the checkout answered
200with the siblings' books — nothing 404'd, nothing got pushed, and the agent's own rules sat on disk unused while the run died on "agent not found". The localsponsio.yamlfallback now applies whenever that file defines the agent, not only when the config was asponsio://ref. The multi-agent error also stopped telling a caller who passed anagent_idto pass anagent_id. - Multi-agent runs lost their claim verdicts.
attach(auto=False)returned before wiring the output lane.autosays who attributes the tool steps — a statement about the action lane — and never meant "drop my evidence", but a multi-agent run (the only reason to pass it) rendered as a clean trace while the model stated something false. - A cloud checkout never rebinds an explicit agent to a sibling.
Running under another agent's rules and reporting as that agent had
been a
UserWarning; it is now an error with the fix in hand. - A 32-bit run id silently merged two runs. Bridge run ids are 64-bit.
- A long run uploaded the run squared, then went silent. The bridge coalesces sends under an interval and a byte-rate ceiling.
- A run names its own book, not the whole project's checkout stamp.
- Singular/plural agreement in
rate_limitandbounded_retrylabels ("limited to 1 invocation"), Python and TypeScript in step.
- Enforcement routes through a canonical stopping set behind
stop_original, so blocks, redirects and escalations answer one question the same way. - Shadow-mode assumption spans record what actually happened.
- Ordered comparisons now agree across Python and TypeScript on numeric
string arguments (#108). Raw tool arguments are grounded as strings, so
a naturally-written numeric guard like
Not(Gt(ArgValue("pay","amount"), Const(1000)))reached the evaluator ascompare("gt", "5000", 1000). Python raisedTypeErrorand fell through toFalse(guard held → the5000payment was allowed) while TypeScript coerced and returnedtrue(guard violated → blocked) — the same contract failed open on one runtime and closed on the other. Both runtimes now coerce a plain-numeric string operand to a number forLt/Le/Gt/Gewhen the other operand is numeric, so numeric guards compare numerically and fail closed identically. Non-numeric strings stay incomparable (False), andEqis unchanged. @sponsio/sdkis now edge-runtime safe. Marked the packagesideEffects(narrowed to the CLI entry) so bundlers can tree-shake the Node-only YAML/config-loading path out of edge bundles (Cloudflare Workers), complementing thecreateRequiredeferral in0.2.0a3.- Trace mining fails open when its extension isn't bundled.
CodeAnalyzerimportedTraceMinerunguarded, crashing the trace-mining path withModuleNotFoundErrorin builds without the optionaltrace_miningextension; it now degrades to "no contracts mined", matching the other call sites.
- Added an explicit
[tool.ruff]config topyproject.tomlso local lint matches CI, and synceddocs/reference/cli.mdwith the real CLI surface (onboard/serve/daemon/cursornow documented). - The CLI now centers on code and policy scanning.
sponsio scanreads source code and policy docs;sponsio check --traceandsponsio evalstill replay traces. Trace-derived contract mining (thesponsio refreshcommand andsponsio scan --trace) is no longer part of this distribution.
Security-relevant fix on top of 0.2.0a2. If you are on 0.2.0a2 and
use any adapter OTHER than LangGraph with a redirect_to_safe
contract, you should upgrade.
-
redirect_to_safenow fails closed in non-LangGraph adapters (sponsio/integrations/base.py,crewai.py,agents.py,claude_agent.py,google_adk.py,vercel_ai.py,mcp.py). Previously, aredirect_to_safeviolation returnedaction="redirected"withblocked=False, and every adapter except LangGraph gated onif check.blocked— meaning the guard rolled the unsafe call out of the trace AND THEN the adapter executed the original unsafe tool anyway. A newCheckResult.stop_originalproperty (blocked OR redirected) is wired through every non-substituting adapter, so a redirect now refuses the unsafe call. LangGraph still branches onredirectedfirst and performs the substitution. The Cursor adapter takes a separateevaluate_eventpath and is tracked as follow-up. Regression test added attests/test_redirect_to_safe.py. -
TS
Eqnow matches Python==for composite values (ts/packages/sdk/src/core/evaluator.ts). The previous===comparison was reference equality for arrays and objects, soEq(ArgValue("tool", "field"), CtxValue("expected"))on list- or object-valued args could pass in Python and fail in TS on the same trace. NewvaluesEqualdoes element-/key-wise deep comparison; parity test added atts/packages/sdk/src/__tests__/parity.test.ts. -
TS SDK no longer crashes on Cloudflare Workers at import time (
ts/packages/sdk/src/core/config-loader.ts,pack-loader.ts). The eager top-levelcreateRequire(import.meta.url)threw whenimport.meta.urlwas undefined (Workers, some edge runtimes). Now built lazily on first YAML load with a?? "file:///sponsio-noop.js"fallback, so a Worker bundle that never loads YAML never callscreateRequire. -
Suite-wide pytest setup errors cleared up (
tests/conftest.py). The autouse rich-style cache reset invokedisinstance(obj, Style)on every live object; lazy proxies from optional SDK imports (notably OpenAI'ssounddevice-pulling submodules) raised from their__class__getter and errored 1684 of 2312 test setups. Now swallows introspection failures.
filter_toolsdocuments O(candidates × trace_length) re-grounding cost (sponsio/integrations/base.py).workflow_stepdocuments the end-of-trace weak-next vacuity caveat for batch verify / replay paths (sponsio/patterns/library.py).Var.__eq__,_warned_missing_vars, andarg_valueretention all get explicit footgun notes (sponsio/formulas/evaluator.py,sponsio/formulas/formula.py,sponsio/tracer/grounding.py).- Test infrastructure moves off the deprecated
asyncio.get_event_loop().run_until_completetoasyncio.run(tests/test_claude_agent_integration.py).
- Several docstrings repaired (artifacts left over from the v0.2
em-dash sweep, mostly first-line typos that surfaced in
help()and IDE hover popups).
No breaking API changes. The CheckResult shape is unchanged
(stop_original is a new derived property, computed from
existing fields). Existing tests against blocked /
redirected still hold.
Thanks to @donalddellapietra for the review pass that surfaced
the fail-open bug, the TS Eq parity gap, and the Worker
runtime crash. PR
#78.
-
Termabstraction in the formula AST (sponsio/formulas/formula.py). The arithmetic comparison family (Eq,Le,Lt,Ge,Gt) now accepts anyTerm, not justVarorConst. Four new term subclasses unlock contracts that compare runtime values against each other:ArgValue(tool, field): raw value ofargs[field]when the current event is a call totool.CtxValue(key): raw value of an externally pushed context fact (guard.observe_context).ArgLength(tool, field):len(args[field])shorthand.UnaryFn(fn, term): apply a Python callable to another term.
VarandConstbecomeTermsubclasses, so their existing counter-style semantics (default0for missing, numeric-only coercion) are preserved.ArithExpris now an alias ofTermso existing type hints keep working. -
workflow_step(trigger, next_action)pattern (sponsio/patterns/library.py). Prescriptive counterpart to the block-style patterns: whentriggerholds at the current event, the next event must satisfynext_action. Both arguments are arbitrary atoms, so the same factory covers tool-ordering, ctx-driven remediation, and arg-conditional follow-ups. Compiles toG(trigger -> X(next_action)). -
Five benchmark contract libraries (
sponsio/contracts/benchmark/*.yaml). Hand-curated YAML libraries that reproduce Sponsio's published benchmark numbers on RedCode-Exec, ODCV-Bench, τ²-bench, AgentDojo, and SWE-bench. Loadable viainclude: [sponsio:benchmark/<name>]like a capability pack but kept separate in intent (benchmark-reproduction artefacts, not auto-selected byonboard). Documented indocs/reference/benchmark-libraries.md. -
NL DSL extensions for the new primitives (
sponsio/generation/dsl_to_contract.py). The natural-language parser recognisesworkflow_stepand the newTermcomparison forms so YAML hand-authoring andsponsio validatereach the new surface.
- Pattern count is now 46 (was 45). Catalog tables and README callouts are updated to match.
- TypeScript SDK parity gap. The
Termabstraction, theworkflow_stepfactory, and the five benchmark YAML libraries are Python-only in this release. TS will catch up in a follow-up. Seedocs/reference/ts-sdk-parity.mdfor the tracked gap list.
PyPI-render fix on top of 0.2.0a0. No runtime changes; if you are
already on 0.2.0a0 there is no functional reason to upgrade.
- README image references are now absolute GitHub raw URLs
(
https://raw.githubusercontent.com/SponsioLabs/Sponsio/main/assets/...). The PyPI / TestPyPI README renderer does not resolve relative paths, so the banner / architecture diagram / freeze comparison were missing on the project page. Three READMEs (en / zh-CN / ja) are updated for consistency; onlyREADME.mdis what PyPI actually serves. - CI lint regex updated to accept either relative or absolute URL
for the banner check, so the old
WYSIWYG-stripped-the-bannerwarning keeps working under both URL forms.
Three new enforcement primitives plus a sharper failure-strategy surface. The story: agents shouldn't have to fail catastrophically when a contract fires. Block is one option, but it's the harshest one. This release ships three softer-landing options that keep the agent making progress while still gating the unsafe behavior.
tool_policyblock (YAML + inline kwarg): declarative default-deny posture.default: deny+approved: [search, …]synthesizes atool_allowlistcontract automatically. Adding a new tool to your framework does not auto-trust it: the policy is the single source of truth for which tools the agent can reach. Available insponsio.yamland onSponsio(tool_policy={…}). Both paths share one synthesis point so the resulting contract is identical.enforcement: proactivemode: wrap-time tool filtering on LangGraph, CrewAI, OpenAI Agents SDK, and Google ADK adapters. Denied tools never reach the agent's bound toolset. Prompt injection that tries to call them silently no-ops because the model literally cannot name them.enforcement: reactive(the default) keeps the legacy "block at call time" behavior.filter_tools(candidates): pure-probe API onBaseGuardthat returns the subset of tool names legal to call given the live trace. Custom agent loops (no framework) call this before each model turn to pre-filter the tool menu and avoid wasted attempts on temporal-precondition tools (must_precede(A, B)only allows B after A has fired). Side-effect free: no log entry, no callback fanout, no perf sample, no observe-mode wrapping. Implemented via adry_runflag onRuntimeMonitor.check_actionthat suppresses every observable side effect under a depth counter.redirect_to_safe(unsafe, safe)pattern +RedirectToSafestrategy: substitute a forbidden tool call with a pre-declared safe one (issue_refund→log_refund_request,run_sql_destructive→select_only_dryrun). The model keeps making progress; it just can't do the unsafe thing. Trace honestly records the substitute call, not the original. LangGraph adapter dispatches the substitute transparently; other adapters surfaceresult.redirected_tofor the application loop to invoke.EscalateToHuman(notify=[…]): strategy now accepts a callable or a list of notifier callables that fire synchronously on each violation. Each notifier gets(violation, context, reason). Notifier failures are isolated per-callback: a broken Slack webhook does not crash the agent loop and does not silence the remaining notifiers; the exception becomes aRuntimeWarningnaming the offending callable.- Cross-integration verification script.
scripts/verify_v0_2.pyruns 15 checks across the core runtime and four adapters. Skip-on-missing-SDK rather than fail. Run before any release to catch the kind of cross-mode bug thatpytestmisses (conftest pinsSPONSIO_MODE=enforce, production default isobserve). - Three workflow case studies.
examples/integrations/python/v0_2_*.py. Refund agent (LangGraph +redirect_to_safe+filter_tools), coding agent (CrewAI +tool_policydefault-deny + proactive), AP automation (vanillaSponsio+EscalateToHumanwith Slack / email / PagerDuty notifiers). Each exits 0 on success and surfaces FAIL with detail on regression.
sponsio mode <observe|enforce>CLI is now parent-aware. Prefers updatingruntime.mode(the only line the TS loader reads), falls back todefaults.mode, refuses to append a freshenforceblock out of thin air on a yaml without an existing mode line, allows appendingobserveonly. CI scripts that relied on the old exit-1 behavior for malformed configs keep working. Walk-and-track replaces the naïvere.subn.EscalateToHumanaction semantics documented. The class docstring now spells out the two patterns: notify-only (agent continues, useful for high-stakes-action telemetry) and theDetBlock+register_callbackpairing for notify-and-refuse. The runtime layer does NOT gateCheckResult.allowedonaction="escalated"because the monitor usesEscalateToHuman()as the default strategy for unfired-assumption verdicts; gating on it would break every conditional contract whose assumption hasn't fired yet.- All pattern factories accept a
desc=keyword.redirect_to_safewas the lonely exception; LLM extraction (llm_extraction.py:535) always passesdesc=nlto the pattern factory, so the previous signature silently failed any LLM-extractedredirect_to_saferule. Now uniform. - TS SDK gets a
redirectToSafefactory. Formula side only: same LTL semantics (G(Not(called(unsafe)))) so a TS evaluator produces the same verdict as the Python verifier. The strategy bundle and adapter dispatch are Python-only for now; documented caveat in the TS docstring. Sponsiofactory + every framework-specific guard class synthesize thetool_policydeny contract uniformly. The earlier code path only synthesized in theSponsio(framework=…)factory; direct framework-specific construction (LangGraphGuard(tool_policy=…), the idiomatic Python pattern) silently dropped the policy. Centralized intoBaseGuard.__init__.
LangGraphGuardrejects chained redirects (A → B → C) and self-redirects (A → A) loudly. Previously a chained redirect silently executed the intermediate tool, and a self-redirect would have infinite-looped. Both now raiseToolCallBlockedwith a clear message naming the chain.render/components.py:contracts_tablewraps the name column inText(name). Rich interprets[…]as markup; contract descs containing brackets (e.g.only [search, read_file] approved) were having the bracketed segment silently swallowed.discovery/trace_replay.pythreadscontent_atomsintoground(). The previous call site dropped the argument, so parameterised content predicates (contains(pii),arg_has(...)) were silently false-negative during historical-trace replay.
- Per-benchmark deep dives under
docs/reference/benchmarks/(agentdojo, odcv, redcode, swebench, tau2). Cross-reference fixed (the index claimed "Four third-party benchmarks" but had five). - HIGH-priority strategy / pattern enumeration fixes across
docs/concepts/contracts.md,docs/concepts/overview.md,docs/concepts/architecture.md,docs/reference/oss-scope.md,docs/reference/config-yaml.md,docs/reference/patterns.md,docs/reference/observability.md,docs/guides/observe-vs-enforce.md,docs/guides/faq.md. The strategy taxonomy is consistent across all of them now:DetBlock/EscalateToHuman/WarnOnly/RedirectToSafe.RetryWithConstraintis an extension point. sponsio/tracer/semconv.pystale comments updated to match.
pyyamlis now a core dependency. It was previously declared only under theconfig/alloptional-dependency groups, but the config loader, thesponsio host installpath,sponsiorc, and plugin scan/append all importyamlon the core code path. A basepip install sponsio(orpipx install sponsio/mise use pipx:sponsio) shipped without it, so the onboarding wizard crashed withModuleNotFoundError: No module named 'yaml'on the firstsponsio host install. (#61)
- The build smoke-test in CI now runs
python -c "import yaml"andsponsio packs(a YAML-reading command) in the clean-install venv, in addition to--version/--help. The old smoke test only exercised click-level commands, which is why the missing core dependency slipped through to a release.
Open-source launch build. Closes the missing-implementation gap in 0.1.0a3
(CLI imported sponsio.daemon / sponsio.plugin.append_ops but the wheel
shipped without them) and tunes the bundled capability rules.
sponsio.daemon: Unix-socket IPC server + client + handlers; powers the privileged-process side ofsponsio plugin appendso a system install can give kernel-level (separate-UID) self-modify protection.sponsio plugin append: structurally-additive merge from a staging YAML into a host bucket library; the only blessed write path through the self-modify pack.
- Capability/shell pack: drop session-wide
rate_limit(exec, 50)andloop_detection(exec, 20). The 24-hour cross-session trace store turned these into rolling caps that false-positived heavy interactive work; the targetedarg_blacklistand confirm-gate rules already cover the real attacks. - Capability/self-modify pack: extend protection to the upstream
sponsiopackage (contract bundles + engine.py) so an editable /--user/ venv install can't be used as an "edit the bundle to silence the rule" bypass. Maintainer workflow: override withcustomized: {match: {source: "library:tier1.self-modify"}, disabled: true}. - Onboard wizard: drop redundant trailing "mode flip" hint (axis 3
already asks); language-aware bare-loop guard API hint
(
guardBefore/guardAfterfor TS,guard_before/guard_afterfor Python).
sponsio --versionwas hardcoded to "0.2.0a0" in the Clickversion_option; now readssponsio.__version__so it trackspyproject.tomlautomatically.- 0.1.0a3 wheel was missing
sponsio/daemon/andsponsio/plugin/append_ops.py, causingsponsio plugin appendandsponsio daemon …to ImportError on a freshpip install. 0.1.0 ships them.
Pre-launch test build. Sponsio is a runtime contract enforcement layer for AI agents: deterministic LTL contracts evaluated as a compiled DFA on every tool call, with framework adapters for the common agent stacks and a CLI for scanning, mining, and reporting.
- Runtime engine: LTL → DFA compiler, finite-trace evaluator, observe / enforce modes, session log writer, OTel exporter.
- Pattern library: 44 deterministic patterns (
must_precede,rate_limit,idempotent,arg_blacklist,arg_allowlist,no_data_leak,segregation_of_duty,cooldown,must_confirm,bounded_retry,loop_detection,scope_limit,arg_length_limit,data_intact,destructive_action_gate, etc.) exposed both as Python factories and as natural-language triggers. - Contract bundles:
sponsio:core/runaway,sponsio:core/universal,sponsio:capability/shell,sponsio:capability/filesystem,sponsio:incident/openclaw. - Framework integrations: LangGraph / LangChain.js, Claude Agent
SDK, OpenAI SDK, OpenAI Agents SDK, Google ADK, Vercel AI SDK,
CrewAI, MCP, plus a no-framework
guard_before/guard_afterAPI. - CLI:
sponsio init(interactive 4-axis wizard), plus the underlyingsponsio onboard,scan,validate,check,report,refresh,eval,export,export-sessions,host,plugin,packs,patterns,prompt,mode,doctor,skill,replay,explain,demo. - TypeScript SDK (
@sponsio/sdk): deterministic engine + the same set of framework integrations. - Static scanner (
@sponsio/sdk): AST-based code scanner for proposing contracts from a TS / JS codebase. - Local observability: session log JSONL writer,
sponsio host trace --followlive stream,sponsio reportrich / markdown / HTML / JSON output, OTel HTTP exporter for shipping to your own collector. - Plugins: Claude Code plugin (production), OpenClaw plugin (beta: type definitions track the public OpenClaw plugin docs; end-to-end exercise inside a live OpenClaw runtime is in progress).
- Benchmarks: ODCV-Bench (95.6% high-risk protection across 12
LLMs, 24 of 36 scenarios at 100% across every model) and
RedCode-Exec (92% combined detection across 1,410 cases), with
0 FP increase across 6 ODCV library iterations and 0% utility
FP on the 60-file clean-code audit. See
docs/reference/benchmarks.md.
- Status: alpha. APIs may shift before 1.0; the trace event schema and CLI surface follow SemVer for breaking changes from 0.2 onward.
- Apache 2.0: see LICENSE and the OSS Promise.