Skip to content

Latest commit

 

History

History
1218 lines (1067 loc) · 63.4 KB

CHANGELOG.md

File metadata and controls

1218 lines (1067 loc) · 63.4 KB

Changelog

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.


[Unreleased]

[0.2.0a21]: 2026-10-25

Added

  • Tier 2: sudo sponsio install --system (macOS and Linux). A _sponsio service 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 _sponsio can open; Claude Code gets its hooks from managed settings (managed-settings.d), with allowManagedHooksOnly on unless --allow-user-hooks. Libraries under ~/.sponsio/plugins are imported once. sponsio uninstall --system removes 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 --sandbox starts an agent in a sandbox (bubblewrap on Linux, Seatbelt on macOS, or a container recipe under deploy/tier3) whose only route out is an egress proxy that decides every connection against egress.allow and the _egress library.

  • 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_hash and hash; sponsio admin audit verify checks 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: in daemon.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 the signing extra.

  • Project layer. A repository's .sponsio/host.yaml adds contracts for hook calls made in it, and can never weaken a protected one; attempts are recorded as tamper_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, and EscalateToHuman releases a covered call (exact match on agent, tool and contract) as observed, 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 a DetBlock rule. SPONSIO_STANDING=0 opts out. This matters since 0.2.0a17, when EscalateToHuman started 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 FILE writes the signature the cloud made over a published book to FILE.sig, and --public-key refuses to write anything that key did not sign; sponsio signing-key prints the signing: block to pin. With cloud.proposals: true a 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.

Changed

  • 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 --tier reports 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, no disableAllHooks, 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_error in daemon.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 with signed: 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 status reads 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.

Fixed

  • 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 -o creates the bucket's directory on a first pull.

[0.2.0a20]: 2026-10-24

Added

  • 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, records config_drift, and adds a note to the next deny. sponsio rules status | diff | reload reviews 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 as tamper_attempt. sponsio daemon run --watch applies every change, for working on Sponsio's own rules.
  • sponsio doctor reports the enforcement tier. One line in the report, and sponsio doctor --tier for 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 verify compares each host bucket linked under cloud: in daemon.yaml with the book the cloud publishes for its agent, and the daemon does so on its own every verify_interval_s. Missing contracts are recorded as cloud_drift. Needs Sponsio Cloud's /v1/rulebook/verify.
  • Host config is backed up before every change. host install and host uninstall copy 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-backup puts the most recent backup back.
  • Docs: threat model, daemon reference, running Sponsio on a host, and the host hook path in benchmarks.

Changed

  • 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_HOURS and SPONSIO_PLUGIN_ROOT no 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 and host guard --direct. Migration: sponsio host install <host> --mode observe sets observe mode; an env entry 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 guard and --direct. The OpenClaw plugin blocks a call it gets no verdict for unless its failOpen option is set. on_error: allow still lets an evaluation error through and each such call is recorded as fail_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.append from 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 install no longer creates the legacy _host, _host_subagent and _host_openclaw buckets and reports a bucket it just created as created. sponsio plugin init still creates the legacy buckets.
  • Installed hooks run a shim that talks to the daemon, and fail closed. sponsio host install claude-code / cursor now write a hook command that runs sponsio_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 an env block in the host settings no longer changes the mode or where the hook connects. Claude Code PreToolUse entries 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 guard and sponsio plugin guard go through the daemon too, with the same fail-closed behaviour; --direct keeps the old in-process evaluation for debugging. The marketplace plugin's hooks.json adds onFailure and timeout.
  • The OpenClaw plugin talks to the daemon over its socket. It no longer spawns sponsio plugin guard per tool call. sponsio host install openclaw writes sponsio-config.json beside the deployed plugin (socket, plugin root, state dir, how to start the daemon); the plugin reads no environment variable, so SPONSIO_GUARD_BIN, SPONSIO_PLUGIN_ROOT and SPONSIO_GUARD_MODE no longer affect it, and the guardBin / pluginRoot / guardMode manifest keys are ignored. When no daemon answers the plugin starts one, and blocks the call if it still cannot be reached. Reinstall with --force to replace an already deployed plugin.
  • sponsio doctor checks 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 ignores onFailure. sponsio host install refuses to write a hook pinned to a socket path too long to bind.
  • One daemon per state dir. The daemon holds <state-dir>/daemon.lock and 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.

Fixed

  • 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 cooldown and 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.json that 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, not conversation_id, so every Claude Code session on a machine shared one trace. The history key is now conversation_id, else session_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's session_id and 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 HOME still wrote the real ~/.claude/settings.json and ~/.cursor/hooks.json. They now resolve per call, and the suite stops and restores the file if any test writes under the real home.

[0.2.0a19]: 2026-10-14

Fixed

  • 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 the ArgValue term keyed on the exact spelling, so they missed issue_refund and mcp__finance__issue_refund. The join now lives in the predicate key itself, on both runtimes. loop_detection follows a tool across an MCP prefix (search, then mcp__web__search, used to reset the run), and a counter is no longer overwritten by a stale spelling.
  • sponsio host install names its own binary. A relative invocation (./venv/bin/sponsio host install) wrote whatever sponsio was first on PATH into 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, or python -m sponsio.cli for a module invocation.
  • --mode on host install does what it says. It wrote defaults.mode into the host library, which the hook never read, so a default install printed observe and blocked anyway. The hook now defaults to enforce openly, and --mode observe rides on the hook command (env SPONSIO_GUARD_MODE=observe ...), where the agent under guard cannot change it. A mode: line already in a host library is ignored, as it always was.

[0.2.0a18]: 2026-10-13

Fixed

  • 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.

    • --strict never reached the Claude Code hook. Claude Code's hook payload carries no host field, so the hook routed every call to the legacy _host bucket while sponsio host install claude-code --strict had written defaults.unconfigured: deny into _host_claude_code. The posture was on disk and never read, and mcp__acme__* still ran. The hook now names the host it was installed for (host guard claude-code routes to _host_claude_code; a host the payload carries wins). Installs that only ever had _host/sponsio.yaml keep 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.json right after >, so > .claude/settings.json was denied and > ~/.claude/settings.json, > $HOME/... and an absolute path were not. truncate -s0, ln -sf /dev/null, chmod 000 and rm -rf ~/.claude were not mutators at all, though each leaves the IDE loading no hooks. capability/host-config- integrity now 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-modify and 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)" and wget --post-data="$(env)" were denied only after an earlier printenv in the same conversation. The stateless rule now matches the three parts independently with -F, --form and --post-data as 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) and finance__issue_refund (OpenClaw's unprefixed form) now answer to issue_refund, in both runtimes; the hook also routes a re-cased prefix to the server's own library instead of the host bucket.
    • 5.000 and €5.000 read 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 bare 5.000 stays the float it is in every programming language.
    • @sponsio/sdk 0.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.


[0.2.0a17]: 2026-10-04

Changed

  • EscalateToHuman refuses 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", but BaseGuard let it run: a policy of "refunds over $10k need CFO approval" issued the refund and paged the CFO afterwards. The strategy now returns action="blocked" with escalation=True, so every adapter's stop gate refuses it and CheckResult.escalated still 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.

Added

  • sponsio host install <host> --strict. Writes defaults.unconfigured: deny into the host library, so a tool whose namespace has no contract library is refused instead of running with a warning. Unlike SPONSIO_UNCONFIGURED=deny alone it survives a new shell, and the agent cannot flip it because the host library is under capability/self-modify. The environment variable (deny / allow) still overrides it. Allowing remains the default.

Fixed

  • 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 normalised ArgValue comparisons only. arg_value_range, the cap YAML and the pattern factory produce, reads arg_numeric, which int() / float() (parseFloat in 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 as args:unevaluable instead 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 for WarnOnly now makes the next check refuse with guard: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-modify named the rule files, so rm -rf ~/.sponsio or mv ~/.sponsio/plugins /tmp took 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.create returned a forbidden tool call unchecked, and the chat variant and with_streaming_response raised AttributeError. The guard now parses the wrapper and raises ToolCallBlocked when 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 to observe.
    • Missing-argument refusal sees MCP spellings. mcp__shell__Bash with no arguments was let through by a rule written for Bash.
    • sponsio.__version__ was stuck at 0.2.0a11 through a16, so sponsio --version and sponsio doctor misreported every release since. It is correct again and a test now pins it to pyproject.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 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 in tests/test_fail_open_layer.py.

    • Tool names now meet on one spelling. Predicate keys are dict lookups, so a rule written against issue_refund was silently inert against a call that arrived as Issue_Refund, with a trailing space, or as mcp__finance__issue_refund — our own documented MCP wire format. With must_precede compiling to Or(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, so called, count, consecutive_count and the argument atoms all bind. Affects must_precede, always_followed_by, never_together, rate_limit, cooldown, destructive_action_gate and every other called-based pattern.
    • Numeric guards read the shapes a model writes. A cap that stopped 5000 waved 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 args empty no argument atom is written, and arg_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=1 restores the old behaviour. Tools with no argument rules are unaffected.
    • guard.mode reports the monitor's live value. It returned a cached copy, so it could say enforce while the monitor had been moved to observe and nothing was being enforced.
    • Every shipped host template protects its own hook wiring. capability/host-config-integrity existed but was included by no _host*.yaml, so capability/self-modify guarded the rule files while .claude/settings.json and .cursor/hooks.json stayed 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-file and -T/--upload-file joined the flag set. curl -d @./payload.json stays 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=deny refuses 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.md had been advertising while the link returned 403.
  • LangGraph wrap_graph() / monitor_graph() now gate each node before it runs. The whole-graph wrappers iterated the inner graph's stream, ran guard_before only 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 raises ToolCallBlocked so the node never runs. The gate covers invoke / ainvoke / stream / astream / astream_events / batch / abatch (the async and batch entry points previously bypassed the wrapper entirely). invoke also no longer requires a checkpointer. monitor_graph() gained a mode= argument. Reported by Trenyx (independent verification, 2026-09).

  • The shell capability bundle's rm rules 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-variable rm rules had the same gap. All three now accept any option order with at least one recursive/force flag in short or long form, and anchor rm on a word boundary so perform -rf / no longer trips them. Reported by Trenyx.

  • dangerous_bash_commands() default now catches every spelling of a recursive rm. The preset's default list carried the literal "rm -rf", so rm -fr, rm -Rf, rm -r, rm -f -r and rm --recursive --force were not banned. The entry is now the regex RM_RECURSIVE_PATTERN (exported from sponsio.patterns.library and @sponsio/sdk), which matches a recursive flag in short or long form in any option order. Non-recursive rm -f file stays allowed. The TypeScript default list now mirrors the Python one exactly (it was missing sudo and 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/agents and @langchain/langgraph packages.

    • CrewAI (Python): on_tool_start returned a dict to refuse a call, but CrewAI only honours False, so the tool ran. It now returns False and on_tool_end swaps CrewAI's generic "blocked by hook" result for the contract violation. on_tool_end takes the context alone (CrewAI passes nothing else), register_global_hooks() imports from crewai.hooks, a guard error refuses the call instead of being swallowed, and the docs no longer show the Crew(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_tool object is wrapped at on_invoke_tool (it used to crash at wrap time).
    • OpenAI (Python): guard.wrap(client) was a no-op inherited from BaseGuard; it now guards that client. patch_openai() and wrap() cover chat.completions.parse() and the Responses API, and refuse stream=True with 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_before does.
    • Claude Agent (Python and TS): the PostToolUse hook read tool_result; the SDK sends tool_response.
    • Claude Code / Cursor hook: PostToolUse events 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=allow restores the old behaviour.
    • Vercel AI (TS): with ai v5 and later the middleware read result.toolCalls, which no longer exists, so nothing was checked; both the content and toolCalls shapes are handled, and wrapStream gates streamed tool calls instead of passing them through.
    • OpenAI Agents (TS): tool() exposes invoke(runContext, input), so the guard was handed the run context as the arguments; it now reads the JSON input.
    • LangChain.js (TS): LangGraph's ToolNode passes the tool-call envelope to invoke, so arg_field_has looked for fields on the envelope; the envelope is unwrapped.
    • OpenAI (TS): stream: true returned 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.

[0.2.0a11]: 2026-09-02

Two more rules that looked armed and were not, and the answer to the one question an audit trail exists for.

Fixed

  • 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 hands Sponsio() 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_config keeps 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_book returned zulu@v9], which parses as no version — a book nobody published. It only bit the agent that sorted last.

  • **tool bashcommand 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, so tool run_sql query must not contain DROP `` did not parse at all.

Changed

  • 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_permission and segregation_of_duty parse 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 structured pattern: form for the whole library — the gap was only in NL strings.


[0.2.0a10]: 2026-09-02

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.

Fixed

  • scope_limit was a string prefix, not confinement. A rule reading "writes stay under /safe" allowed /safe/../../root/.ssh/id_rsa, because the check was path.startswith(prefix) — the oldest escape there is, against a rule whose entire purpose is confinement. The same line also counted /safeguard/evil.txt as 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 B compiled 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 users walked past dangerous_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, so DELETE no longer matches the word "deleted" in a comment or a column named drop_date.

  • An enforcing run could start with a rule missing. Strict compile read defaults.mode and nothing else, so a project that said enforce in any of the four other places the runtime honors dropped the broken contract, printed one UserWarning, and enforced the rest. Strictness follows the effective mode now, and a contract carrying its own mode: enforce raises either way.

  • sponsio doctor green-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. Its Runtime mode line also read two of the three places a yaml can say enforce, so a blocking project was reported as observe (shadow — safe default).

  • sponsio doctor failed on a minimal install. find_spec on 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 no google — which is what pip install sponsio gives you. Doctor opened with a red mark on a healthy install.

  • A pattern given the wrong number of arguments raised a bare TypeError from 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 — raised string indices must be integers from inside the bridge. A bare name is the shorthand now.

Changed

  • A contract row sent to a console carries the pattern and args it was built from, so a run whose agent has no published book is no longer offered the rules it was just checked against.
  • tool_allowlist reads the same in both runtimes; TypeScript's NL parser understands scope_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.

Removed

  • The SMT hooks. base.py accepted an smt= config and imported sponsio.integrations.smt_middleware; CloudClient.smt imported sponsio.cloud.smt. Neither module is in this build, so opting in raised ModuleNotFoundError at construction while the public docstring documented the feature. They return with the modules that make them work.

[0.2.0a9]: 2026-09-02

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.

Added

  • 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.yaml does, 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=1 turns it off; a failed upload prints the manual command and never stops the run.

Fixed

  • A broken ./sponsio.yaml says 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.

Changed

  • docs/reference/config-yaml.md names all five places the mode comes from and which one wins. A contract's own mode: beats the mode your code passes, so a rule armed in the console stops calls inside a run started with mode="observe". @sponsio/sdk reads runtime.mode only, so defaults.mode governs the Python runtime and not the TypeScript one.
  • Every install line in the READMEs, QUICKSTART and docs/ passes --pre. Without it pip resolves to 0.1.1, the last stable — a build that predates the cloud console, the evidence lane and the current CLI.

[0.2.0a8]: 2026-09-01

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.

Added

  • Evidence middleware. observe_llm_call extracts 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.

Fixed

  • 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 200 with 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 local sponsio.yaml fallback now applies whenever that file defines the agent, not only when the config was a sponsio:// ref. The multi-agent error also stopped telling a caller who passed an agent_id to pass an agent_id.
  • Multi-agent runs lost their claim verdicts. attach(auto=False) returned before wiring the output lane. auto says 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_limit and bounded_retry labels ("limited to 1 invocation"), Python and TypeScript in step.

Changed

  • 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.

Fixed (carried from 0.2.0a3)

  • 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 as compare("gt", "5000", 1000). Python raised TypeError and fell through to False (guard held → the 5000 payment was allowed) while TypeScript coerced and returned true (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 for Lt/Le/Gt/Ge when the other operand is numeric, so numeric guards compare numerically and fail closed identically. Non-numeric strings stay incomparable (False), and Eq is unchanged.
  • @sponsio/sdk is now edge-runtime safe. Marked the package sideEffects (narrowed to the CLI entry) so bundlers can tree-shake the Node-only YAML/config-loading path out of edge bundles (Cloudflare Workers), complementing the createRequire deferral in 0.2.0a3.
  • Trace mining fails open when its extension isn't bundled. CodeAnalyzer imported TraceMiner unguarded, crashing the trace-mining path with ModuleNotFoundError in builds without the optional trace_mining extension; it now degrades to "no contracts mined", matching the other call sites.

Changed

  • Added an explicit [tool.ruff] config to pyproject.toml so local lint matches CI, and synced docs/reference/cli.md with the real CLI surface (onboard/serve/daemon/cursor now documented).
  • The CLI now centers on code and policy scanning. sponsio scan reads source code and policy docs; sponsio check --trace and sponsio eval still replay traces. Trace-derived contract mining (the sponsio refresh command and sponsio scan --trace) is no longer part of this distribution.

[0.2.0a3]: 2026-06-08

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.

Fixed

  • redirect_to_safe now 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, a redirect_to_safe violation returned action="redirected" with blocked=False, and every adapter except LangGraph gated on if check.blocked — meaning the guard rolled the unsafe call out of the trace AND THEN the adapter executed the original unsafe tool anyway. A new CheckResult.stop_original property (blocked OR redirected) is wired through every non-substituting adapter, so a redirect now refuses the unsafe call. LangGraph still branches on redirected first and performs the substitution. The Cursor adapter takes a separate evaluate_event path and is tracked as follow-up. Regression test added at tests/test_redirect_to_safe.py.

  • TS Eq now matches Python == for composite values (ts/packages/sdk/src/core/evaluator.ts). The previous === comparison was reference equality for arrays and objects, so Eq(ArgValue("tool", "field"), CtxValue("expected")) on list- or object-valued args could pass in Python and fail in TS on the same trace. New valuesEqual does element-/key-wise deep comparison; parity test added at ts/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-level createRequire(import.meta.url) threw when import.meta.url was 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 calls createRequire.

  • Suite-wide pytest setup errors cleared up (tests/conftest.py). The autouse rich-style cache reset invoked isinstance(obj, Style) on every live object; lazy proxies from optional SDK imports (notably OpenAI's sounddevice-pulling submodules) raised from their __class__ getter and errored 1684 of 2312 test setups. Now swallows introspection failures.

Changed

  • filter_tools documents O(candidates × trace_length) re-grounding cost (sponsio/integrations/base.py).
  • workflow_step documents the end-of-trace weak-next vacuity caveat for batch verify / replay paths (sponsio/patterns/library.py).
  • Var.__eq__, _warned_missing_vars, and arg_value retention 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_complete to asyncio.run (tests/test_claude_agent_integration.py).

Documentation

  • 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).

Compatibility

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.

Credits

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.


[0.2.0a2]: 2026-06-07

Added

  • Term abstraction in the formula AST (sponsio/formulas/formula.py). The arithmetic comparison family (Eq, Le, Lt, Ge, Gt) now accepts any Term, not just Var or Const. Four new term subclasses unlock contracts that compare runtime values against each other:

    • ArgValue(tool, field): raw value of args[field] when the current event is a call to tool.
    • 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.

    Var and Const become Term subclasses, so their existing counter-style semantics (default 0 for missing, numeric-only coercion) are preserved. ArithExpr is now an alias of Term so existing type hints keep working.

  • workflow_step(trigger, next_action) pattern (sponsio/patterns/library.py). Prescriptive counterpart to the block-style patterns: when trigger holds at the current event, the next event must satisfy next_action. Both arguments are arbitrary atoms, so the same factory covers tool-ordering, ctx-driven remediation, and arg-conditional follow-ups. Compiles to G(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 via include: [sponsio:benchmark/<name>] like a capability pack but kept separate in intent (benchmark-reproduction artefacts, not auto-selected by onboard). Documented in docs/reference/benchmark-libraries.md.

  • NL DSL extensions for the new primitives (sponsio/generation/dsl_to_contract.py). The natural-language parser recognises workflow_step and the new Term comparison forms so YAML hand-authoring and sponsio validate reach the new surface.

Changed

  • Pattern count is now 46 (was 45). Catalog tables and README callouts are updated to match.

Known limitations

  • TypeScript SDK parity gap. The Term abstraction, the workflow_step factory, and the five benchmark YAML libraries are Python-only in this release. TS will catch up in a follow-up. See docs/reference/ts-sdk-parity.md for the tracked gap list.

[0.2.0a1]: 2026-06-06

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.

Fixed

  • 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; only README.md is 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-banner warning keeps working under both URL forms.

[0.2.0a0]: 2026-06-03

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.

Added

  • tool_policy block (YAML + inline kwarg): declarative default-deny posture. default: deny + approved: [search, …] synthesizes a tool_allowlist contract 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 in sponsio.yaml and on Sponsio(tool_policy={…}). Both paths share one synthesis point so the resulting contract is identical.
  • enforcement: proactive mode: 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 on BaseGuard that 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 a dry_run flag on RuntimeMonitor.check_action that suppresses every observable side effect under a depth counter.
  • redirect_to_safe(unsafe, safe) pattern + RedirectToSafe strategy: 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 surface result.redirected_to for 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 a RuntimeWarning naming the offending callable.
  • Cross-integration verification script. scripts/verify_v0_2.py runs 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 that pytest misses (conftest pins SPONSIO_MODE=enforce, production default is observe).
  • Three workflow case studies. examples/integrations/python/v0_2_*.py. Refund agent (LangGraph + redirect_to_safe + filter_tools), coding agent (CrewAI + tool_policy default-deny + proactive), AP automation (vanilla Sponsio + EscalateToHuman with Slack / email / PagerDuty notifiers). Each exits 0 on success and surfaces FAIL with detail on regression.

Changed

  • sponsio mode <observe|enforce> CLI is now parent-aware. Prefers updating runtime.mode (the only line the TS loader reads), falls back to defaults.mode, refuses to append a fresh enforce block out of thin air on a yaml without an existing mode line, allows appending observe only. CI scripts that relied on the old exit-1 behavior for malformed configs keep working. Walk-and-track replaces the naïve re.subn.
  • EscalateToHuman action semantics documented. The class docstring now spells out the two patterns: notify-only (agent continues, useful for high-stakes-action telemetry) and the DetBlock + register_callback pairing for notify-and-refuse. The runtime layer does NOT gate CheckResult.allowed on action="escalated" because the monitor uses EscalateToHuman() 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_safe was the lonely exception; LLM extraction (llm_extraction.py:535) always passes desc=nl to the pattern factory, so the previous signature silently failed any LLM-extracted redirect_to_safe rule. Now uniform.
  • TS SDK gets a redirectToSafe factory. 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.
  • Sponsio factory + every framework-specific guard class synthesize the tool_policy deny contract uniformly. The earlier code path only synthesized in the Sponsio(framework=…) factory; direct framework-specific construction (LangGraphGuard(tool_policy=…), the idiomatic Python pattern) silently dropped the policy. Centralized into BaseGuard.__init__.

Fixed

  • LangGraphGuard rejects 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 raise ToolCallBlocked with a clear message naming the chain.
  • render/components.py:contracts_table wraps the name column in Text(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.py threads content_atoms into ground(). The previous call site dropped the argument, so parameterised content predicates (contains(pii), arg_has(...)) were silently false-negative during historical-trace replay.

Documentation

  • 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. RetryWithConstraint is an extension point.
  • sponsio/tracer/semconv.py stale comments updated to match.

[0.1.1]: 2026-05-22

Fixed

  • pyyaml is now a core dependency. It was previously declared only under the config / all optional-dependency groups, but the config loader, the sponsio host install path, sponsiorc, and plugin scan/append all import yaml on the core code path. A base pip install sponsio (or pipx install sponsio / mise use pipx:sponsio) shipped without it, so the onboarding wizard crashed with ModuleNotFoundError: No module named 'yaml' on the first sponsio host install. (#61)

Changed

  • The build smoke-test in CI now runs python -c "import yaml" and sponsio 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.

[0.1.0]: 2026-05-06

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.

Added

  • sponsio.daemon: Unix-socket IPC server + client + handlers; powers the privileged-process side of sponsio plugin append so 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.

Changed

  • Capability/shell pack: drop session-wide rate_limit(exec, 50) and loop_detection(exec, 20). The 24-hour cross-session trace store turned these into rolling caps that false-positived heavy interactive work; the targeted arg_blacklist and confirm-gate rules already cover the real attacks.
  • Capability/self-modify pack: extend protection to the upstream sponsio package (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 with customized: {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/guardAfter for TS, guard_before/guard_after for Python).

Fixed

  • sponsio --version was hardcoded to "0.2.0a0" in the Click version_option; now reads sponsio.__version__ so it tracks pyproject.toml automatically.
  • 0.1.0a3 wheel was missing sponsio/daemon/ and sponsio/plugin/append_ops.py, causing sponsio plugin append and sponsio daemon … to ImportError on a fresh pip install. 0.1.0 ships them.

[0.1.0a3]: 2026-05-02

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.

Added

  • 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_after API.
  • CLI: sponsio init (interactive 4-axis wizard), plus the underlying sponsio 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 --follow live stream, sponsio report rich / 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.

Notes

  • 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.