Skip to content
This repository was archived by the owner on Oct 24, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9f23ea2
chore(deps): update greenlet requirement from >=3.5.4 to >=3.5.5
dependabot[bot] Aug 17, 2026
5255d26
feat(examples)+docs: run the interop experiment, write the demand ins…
claude Aug 17, 2026
3da966d
chore: retrigger CI after runner-startup failure on 5255d26
claude Aug 17, 2026
3e34331
chore: retrigger CI — probe whether Actions is assigning runners again
claude Aug 17, 2026
9fb5d97
chore: retrigger CI now that the PR is marked ready for review
claude Aug 17, 2026
0773da9
fix: address CodeRabbit review — issuer binding, alg validation, inte…
claude Aug 17, 2026
e2b9f52
fix: address CodeRabbit round 2 — signer-identity validation, gate co…
claude Aug 17, 2026
5ab6828
fix: reject mismatched inner-bundle alg; parametrize signer-identity …
claude Aug 17, 2026
c1bf1dd
chore: retrigger CI — periodic probe for Actions runner availability
claude Aug 17, 2026
23be3dd
chore: retrigger CI — closure probe for Actions runner availability
claude Aug 17, 2026
29107cb
Merge branch 'main' into dependabot/pip/greenlet-gte-3.5.5
PetrefiedThunder Aug 17, 2026
dc062ca
docs: complete function docstring coverage in the interop example and…
claude Aug 17, 2026
45da2df
chore(deps): update alembic requirement from >=1.19.0 to >=1.19.1
dependabot[bot] Aug 17, 2026
e194a03
chore(deps): update stripe requirement from >=15.4.0 to >=15.5.0
dependabot[bot] Aug 17, 2026
89634c3
fix(discovery): manifest generation must not crash on an oversized price
claude Aug 17, 2026
3532240
docs(ip): fold the 2026-08 research into the package; assemble a prov…
claude Aug 17, 2026
514c91a
fix(state): release the aiosqlite worker thread at interpreter shutdown
claude Aug 17, 2026
25ac731
test(crash): exercise every instrumented fault point
claude Aug 17, 2026
6bca94e
docs(ip): record current repo visibility as authoritative, history as…
claude Aug 17, 2026
0932b18
chore: ignore agent-tooling worktree scratch
claude Aug 17, 2026
938ed63
test(crash): make the crash scenarios order-independent
claude Aug 17, 2026
cddbd23
Merge remote-tracking branch 'origin/dependabot/pip/alembic-gte-1.19.…
claude Aug 17, 2026
6f0d396
Merge remote-tracking branch 'origin/dependabot/pip/stripe-gte-15.5.0…
claude Aug 17, 2026
3367464
Merge remote-tracking branch 'origin/claude/middleware-tool-interface…
claude Aug 17, 2026
bd2c192
Merge remote-tracking branch 'origin/claude/trust-plane-loop-demo-cs2…
claude Aug 17, 2026
e9c6bb9
Merge remote-tracking branch 'origin/claude/durable-state-atexit-back…
claude Aug 17, 2026
07cb391
Merge remote-tracking branch 'origin/claude/ip-prosecution-package-5a…
claude Aug 17, 2026
0c7a35e
Merge remote-tracking branch 'origin/claude/exercise-remaining-fault-…
claude Aug 17, 2026
250a9a0
Merge remote-tracking branch 'origin/claude/agent-middleware-market-r…
claude Aug 17, 2026
b4e57d5
Merge remote-tracking branch 'origin/main' into claude/merge-pull-req…
claude Aug 17, 2026
3a7e15b
Merge remote-tracking branch 'origin/main' into claude/merge-pull-req…
claude Aug 17, 2026
f916b2a
Merge remote-tracking branch 'origin/main' into claude/merge-pull-req…
claude Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@ Thumbs.db
# Docker
*.log
.vercel/

# Agent tooling scratch: throwaway git worktrees used to run checks in
# isolation. Full repo copies (including .env.production and coverage
# artifacts) — never commit these.
.claude/worktrees/
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ full release gate; do not backfill a final `v1.2.0` tag.

### 🔬 Competitive research, and the positioning correction it forced

- **The interop question now has running code.**
`examples/acta_receipt_interop.py` (plus `tests/test_acta_receipt_interop.py`,
8 tests) transcodes a signed portable receipt into the ScopeBlind ACTA
receipt envelope as a custom `agentmiddleware:governed_invoke` type — inner
production signature verified over the exact `signing_input` bytes, outer
Ed25519-over-JCS envelope, economic linkage as first-class fields, floats
and unverified or linkage-less bundles refused. Deliberately an experiment
in `examples/`, not a product surface; shipping it remains a product call.
- **The demand question now has an instrument.**
`docs/partner-interview-script.md`: a past-tense-only discovery script with
an incident-first structure, a commitment test priced in engineer-afternoons
rather than opinions, a per-call scoring table, and a decision rule
committed before the interviews so five calls produce a verdict.

- **Second research pass (2026-08-15) answered two of the open questions.**
The protect-mcp receipt draft
(`draft-farley-acta-signed-receipts`) was read directly: it is an individual
Expand Down
89 changes: 89 additions & 0 deletions app/core/durable_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@

import aiosqlite
import asyncio
import atexit
import json
import logging
import os
import threading
from dataclasses import asdict, is_dataclass
from datetime import date, datetime, time
from decimal import Decimal
Expand Down Expand Up @@ -55,6 +58,71 @@ def _json_default(value: Any) -> Any:
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")


# --- SQLite worker-thread shutdown backstop --------------------------------
# ``aiosqlite.Connection`` composes its worker ``Thread`` without
# ``daemon=True``, and CPython joins non-daemon threads during interpreter
# shutdown. A connection nobody closes therefore wedges the process at exit
# with no traceback and no output — the failure mode that cancelled CI at
# GitHub's six-hour limit before ``close_durable_state`` was wired into the
# test teardown. ``close()`` is still the real cleanup path; this only catches
# consumers that never reach it (a script, a REPL, a worker entrypoint).
#
# Registration goes through ``threading._register_atexit``, NOT ``atexit``.
# Plain ``atexit`` handlers run *after* the non-daemon join, so they can never
# break this particular hang — verified by observation, and the reason the
# obvious implementation does not work. ``threading._register_atexit`` runs
# inside ``threading._shutdown`` before the join; the stdlib's own
# ``concurrent.futures.thread`` relies on it for the same purpose.
_TRACKED_SQLITE_CONNECTIONS: dict[int, tuple[aiosqlite.Connection, int]] = {}
_SQLITE_SHUTDOWN_HOOK_REGISTERED = False


def _release_tracked_sqlite_connections() -> None:
"""Release worker threads for connections nobody closed."""
current_pid = os.getpid()
for conn, owner_pid in list(_TRACKED_SQLITE_CONNECTIONS.values()):
# Shutdown hooks are inherited across fork(). A child must never close
# a connection owned by its parent.
if owner_pid != current_pid:
continue
logger.warning(
"Durable-state SQLite connection was not closed; releasing its "
"worker thread at shutdown. Call close_durable_state() on exit."
)
try:
# ``stop`` rather than ``close``: it is synchronous and tolerates a
# missing event loop by design, enqueueing its close-and-stop work
# with a ``None`` future. No loop has to survive until shutdown.
conn.stop()
except Exception:
logger.debug("Failed to stop SQLite worker at shutdown", exc_info=True)
_TRACKED_SQLITE_CONNECTIONS.clear()


def _ensure_sqlite_shutdown_hook() -> None:
"""Register the shutdown hook once per process."""
global _SQLITE_SHUTDOWN_HOOK_REGISTERED
if _SQLITE_SHUTDOWN_HOOK_REGISTERED:
return
register_early = getattr(threading, "_register_atexit", None)
if register_early is not None:
register_early(_release_tracked_sqlite_connections)
else:
# No known CPython lacks it (3.9+), but degrade rather than crash on
# import if a future runtime drops the private hook.
atexit.register(_release_tracked_sqlite_connections)
_SQLITE_SHUTDOWN_HOOK_REGISTERED = True


def _track_sqlite_connection(conn: aiosqlite.Connection, owner_pid: int) -> None:
_ensure_sqlite_shutdown_hook()
_TRACKED_SQLITE_CONNECTIONS[id(conn)] = (conn, owner_pid)


def _untrack_sqlite_connection(conn: aiosqlite.Connection) -> None:
_TRACKED_SQLITE_CONNECTIONS.pop(id(conn), None)


class DurableStateStore:
"""Simple key/value JSON state store backed by PostgreSQL or Redis."""

Expand All @@ -79,6 +147,7 @@ def __init__(self):
self._redis: redis.Redis | None = None
self._pg_pool: asyncpg.Pool | None = None
self._sqlite_conn: aiosqlite.Connection | None = None
self._sqlite_owner_pid: int | None = None

@property
def backend(self) -> str:
Expand Down Expand Up @@ -131,6 +200,20 @@ def _missing_explicit_backend(self, intended: str, missing_var: str) -> str:
mark_durable_state_fell_back(intended)
return "memory"

def _register_sqlite_shutdown_backstop(self) -> None:
"""Track this connection so interpreter shutdown cannot hang on it."""
conn = self._sqlite_conn
if conn is None:
return
self._sqlite_owner_pid = os.getpid()
_track_sqlite_connection(conn, self._sqlite_owner_pid)

def _unregister_sqlite_shutdown_backstop(self) -> None:
"""Stop tracking once the connection is closed through the normal path."""
if self._sqlite_conn is not None:
_untrack_sqlite_connection(self._sqlite_conn)
self._sqlite_owner_pid = None

async def _ensure_ready(self) -> None:
if self._initialized:
return
Expand Down Expand Up @@ -171,6 +254,7 @@ async def _ensure_ready(self) -> None:
await self._redis.ping()
elif self._backend == "sqlite":
self._sqlite_conn = await aiosqlite.connect(self._sqlite_url)
self._register_sqlite_shutdown_backstop()
self._sqlite_conn.row_factory = aiosqlite.Row
await self._sqlite_conn.execute(
"""
Expand Down Expand Up @@ -428,6 +512,11 @@ async def close(self) -> None:
self._pg_pool = None

if self._sqlite_conn is not None:
# Untrack first: the backstop is keyed on the connection object, and
# clearing the attribute below would leave nothing to look it up by.
# Doing it before the close also means a close that raises cannot
# strand a tracked connection.
self._unregister_sqlite_shutdown_backstop()
try:
await self._sqlite_conn.close()
except Exception:
Expand Down
4 changes: 3 additions & 1 deletion app/services/mcp_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ def _service_to_mcp_tool(self, service: dict) -> dict[str, Any]:
raw_cost = service.get("credits_per_unit", 1.0)
try:
per_call_cost = float(raw_cost)
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
# OverflowError: an int too large for a float (a registry write
# could carry one even though prices are normally float/Decimal).
per_call_cost = 1.0
if not math.isfinite(per_call_cost):
# A malformed price (unparseable, NaN, infinite) must not
Expand Down
19 changes: 15 additions & 4 deletions docs/market-research-2026-08.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ built from §3 and §4.
conversation settles that.
5. Would a buyer with an EU AI Act mandate reject a vendor with no compliance
mapping outright, or accept receipts as one input among several?
6. Does the demand exist? The instrument for answering is now written:
[`partner-interview-script.md`](partner-interview-script.md), with a
pre-committed decision rule so five interviews produce a verdict rather
than an argument.

---

Expand Down Expand Up @@ -308,10 +312,17 @@ thread's author fixed it with idempotency keys and moved on.
documents.
- **The practitioner quote is the receipt argument verbatim**: "nothing in the
transcript says so." Candidate for `/compare/` or the pitch, cited.
- **Interop vs. compete is now a live, cheap experiment**: one serializer
emitting `agentmiddleware:*` receipts in the draft's envelope, offline
verification included. Whether to run it is a product call; the cost side is
no longer unknown.
- **Interop vs. compete is now a live, cheap experiment — and the experiment
has been run.** `examples/acta_receipt_interop.py` transcodes the published
proof receipt into an `agentmiddleware:governed_invoke` receipt in the
draft's envelope (JCS canonicalization, Ed25519, `issuer_id`=`kid`), with
the original signed bundle embedded and independently verifiable — the
signature covers the exact `signing_input` bytes, and the wrapper's
`issuer` label is checked against the issuer bound to the key material in
the trust-keys snapshot rather than trusted on its own — and the
ledger/idempotency linkage carried as first-class fields. Cost measured:
~200 lines plus tests, no trust-plane changes. Whether to *ship* it stays a
product call; the cost side is now a fact, not an estimate.
- **§6 risk 1 escalates**: the receipt-format neighbour added spend evidence
within two draft revisions. If a registry lands, economic receipt types
become table stakes the way signatures did.
Expand Down
Loading