CHANGELOG
Unreleased
v3.14.4: September 2, 2026
SECURITY
A peer could write into a namespace its trust type excludes by putting a newline in the property name. Permission globs were compiled to a regex anchored with
$and withoutre.DOTALL, so*could not cross a newline and$tolerated a trailing one: for afriendorpartnerpeer,excluded_patterns: ["private/*", "security/*", "_internal/*"]did not matchprivate/\nx. The reachable vector is the JSON body ofPOST /{actor}/properties(keys are property names); at the previous release afriendpeer’s{"private/\nx": "v"}returned 201 and the row appeared in the owner’s listing. A newline in the URL path never reaches a handler on either integration (both routers answer 404), and tab, NUL and CR were already matched by*. Impact is integrity — a value planted inside a protected namespace — not disclosure. Affected: any deployment that grantsfriendorpartner, or a custom trust type withexcluded_patternsor a bare-literaldeniedentry, to a peer. What changed, all of it about identifiers only (property, list, tool, prompt, method and resource names, peer ids); values — property content, list items, tool input — are untouched:Patterns now match the whole identifier (
\Z), and*/?match a newline like any other character (re.DOTALL).The evaluator denies any identifier containing a C0 or C1 control character before consulting rules. This is also what keeps a bare-literal
denied: ["secret"]from flipping to an allow forsecret\nnow that the anchor is exact. Denials are logged atWARNINGwith the identifier’srepr().New property and list names containing a control character are refused at the store (
ValueError) and answered400by the REST layer, for every path segment of aPUTand every key of aPOST(checked before any key is applied). A name that already exists stays readable and deletable by its owner.Six other identifier validators moved from
$to\Z/fullmatch: the two remote peer-id patterns (a<32 hex>\npeer id is how aremote:<id>\nbucket came to exist), the two encrypted-state classifiers, the v1 list index and orphan-row patterns, and MCP resource URI template matching. Legacyuri_patternresource dispatch usesre.fullmatchinstead of the prefix matchre.match.The six MCP single-item permission checks (
tools/call,prompts/get,resources/readon both transports) now fail closed: an evaluator that raises denies with-32003and anERRORlog line carrying the traceback, instead of serving the request atDEBUG. The*/listfilters are unchanged.
Observable changes for legitimate use: a wildcard rule that used to miss an identifier with an embedded newline now matches it; an identifier carrying any control character is now denied to peers and cannot be created; and a fault inside the permission subsystem (a failing trust lookup, say) now blocks MCP
tools/call,prompts/getandresources/readwith-32003until it is fixed, where it used to let them through — an availability cost taken deliberately on a security path, and theERRORlog line names it. The equivalentmethods/actionsdeny bypass was never reachable — dispatch there is an exact dict lookup.
FIXED
PostgreSQL ``get_bucket()`` returned ``None`` for an empty bucket. It now returns
{}and reservesNonefor missing arguments or a caught backend fault, matching DynamoDB. Consequence: an empty bucket is now authoritative on PostgreSQL as it has been on DynamoDB since 3.14.3 —Attributes.get_attr(name)afterget_bucket() == {}answersNonewithout a backend read for the life of that instance. DynamoDB still reports most faults by raising rather than returningNone, because the guarded region there is Query construction and PynamoDB fires the request lazily. The five library call sites that end inget_bucket(...) or {}(attribute_list_store,callback_processor,remote_storagetwice,fanout) still fold a fault into “empty” and are knowingly deferred.The two backends stored different attribution for a colliding attribute row.
bucket_nameisbucket + ":" + nameand both halves may contain:, so bucketremote:abc/namexand bucketremote/nameabc:xshare a primary key. DynamoDB’s PutItem reattributed the row to the last writer; PostgreSQL’sON CONFLICT DO UPDATErefreshed onlydata/timestampand kept the first writer’sbucket. The upsert now setsbucketandnametoo, so on both backends the row belongs to the last writer — which means a later write through the other pair moves it between buckets.Attribute point reads and deletes compare the bucket exactly.
get_attr,get_attr_strict,delete_attr,delete_attr_conditionalandconditional_update_attrkeyed onbucket_namealone and would answer, or delete, the colliding sibling’s row; they now apply the same exactbucketcompareget_bucket()anddelete_bucket()gained in 3.14.3.
CHANGED
``GET /mcp/info`` is derived from the application, not the demo. Both integrations carried a byte-identical literal document (
tools_count: 4,prompts_count: 3,actor_lookup: "email_based", “ActingWeb MCP Demo …”) for every deployment. It is theresource_documentationtarget of the OAuth discovery chain and unauthenticated, so it is now built by one function from config and the in-memory hook registry only. Removed:tools_count,prompts_count,actor_lookup. Added:server_name(fromwith_mcp(server_name=)). Changed:mcp_enabledfollows the configured value (it was a literalTrue),descriptionis the app’s own (ActingWeb app: {aw_type}unless the app setsdesc), andsupported_featureslists only what the registry actually exposes. Read the tool list fromtools/list. No library version is disclosed. This changes a response shape in a patch release: a client that readstools_count,prompts_countoractor_lookupfrom/mcp/infowill find them gone. The old values were the demo’s literals for every deployment, so nothing that read them was reading anything true; still, a client that hard-coded them needs updating.``GET /mcp`` reports the configured server name and real capabilities. It answered
"actingweb-mcp"and three literalTruecapabilities regardless of configuration while theinitializehandshake answered the configured name; the three surfaces now share one derivation. A deployment that never setserver_namesees"actingweb"here now, which a client keyed on the old"actingweb-mcp"string will notice.``fqdn`` and ``proto`` are stripped, then validated, when ``Config`` is built. Surrounding whitespace (a trailing newline from a
.envfile) is stripped; a double quote, backslash, interior whitespace or control character raisesValueErrornaming the character,APP_HOST_FQDN/APP_HOST_PROTOCOL, and the accepted formhost[:port][/base]with no scheme.ActingWebAppstrips at its boundary too. A scheme prefix onfqdnis not detected — it produces the doubled scheme it always has.
REMOVED
``BaseActingWebIntegration.get_oauth_discovery_metadata()``, and the two tests that were its only callers. Both integrations serve
/.well-known/oauth-authorization-serverfromOAuth2EndpointsHandler, the method was never rendered in the API docs, and while it lived it advertisedscopes_supported: ["openid", "profile", "email", "mcp"]— neither the served list nor a subset of it.
v3.14.3: August 29, 2026
FIXED
A transient read fault could empty a v1 list.
verify(),compact()andmigrate_to_v2()each read the actor’s whole property partition throughfetch_all_including_lists()and ended the line inor {}. PostgreSQL’sfetch_all_including_listsreturnsNoneon a caught exception, so a throttle or a dropped connection presented itself as an empty partition:compact()computedordered_values = [], deleted rows0..stored_length-1and wrotelength: 0— and a followingverify()reported the now-empty listhealthy: true. All three now read only their own list’s rows, throughget_range(_v1_bounds()), which raisesDbErroron a backend fault instead of returning nothing.Those same three reads are now strongly consistent. Two of them rewrite destructively from what they read —
compact()writes survivors to0..n-1then deletes the tail,migrate_to_v2()deletes v1 rows0..highest_seen— so a row missed by an eventually consistent replica read was overwritten by its successor and its slot deleted, silently. The v2 counterparts already stated this rule; v1 now agrees.``actor.property_lists.list_all_with_rows()`` raised ``TypeError`` on a permission-scoped actor view.
AuthenticatedPropertyListStoredefined no bulk readers, so the call fell through__getattr__, which permission-checked the method name as a list name — which passes, since an unmatched target evaluates toNOT_FOUNDand onlyDENIEDraises — and returned a_PermissionEnforcingListViewwrapping a bound method. All three bulk readers (list_all(),list_all_with_rows(),list_prefix_with_rows()) are now defined explicitly and filter denied lists out of bothnamesandrows, in one bulk permission evaluation. Rows are narrowed with the library’s own attribution logic, never a barestartswith()prune, which for a denied listfoowould also strip permitted siblingfoo-old’s item rows while leaving its-metarow — after which the permitted list reads as[]with nothing raised. A permission-system error returns an empty result rather than a partial one, and no denied list name appears in any message or log emitted by this path. Behavior change:__getattr__on that view now raisesAttributeErrorfor a name colliding with a store method (exists,list_all,list_all_with_rows,list_prefix_with_rows), so a user list actually named one of those is unreachable through the authenticated view. That is the safe reading: the alternative repair — resolving such a name to the underlying store’s method — would hand a permission-scoped accessor an unfiltered whole-partition read.DynamoDB attribute buckets matched by bare prefix, and one of them deleted what it matched.
Attribute’s range key isbucket + ":" + name, butget_bucket()anddelete_bucket()queried it withbegins_with(bucket)— no delimiter — so a bucket saw, and indelete_bucket()’s case destroyed, the rows of every bucket having its name as a prefix.RemotePeerStore.delete_all()tears down bucketremote:{peer_id}when a trust relationship ends, and most call sites build that id withvalidate_peer_id=False, so the ids are remote-party-chosen: ending trust with peerabcdeleted peerabcd’s entire dataset. Both methods now query with the delimiter and compare the storedbucketexactly, the guarddelete_by_chain()andsubscription_suspension’s cascade check already carried. The delimiter alone is not enough — bucket names contain:and attribute names contain:, so bucketremote:abc/namexand bucketremote/nameabc:xproduce an identical range key and are in fact the same row. PostgreSQL comparedbucketexactly on both paths and was never affected; the two backends now agree.
ADDED
``property_lists.list_prefix_with_rows(prefix)`` — read one namespace of an actor’s list properties, and their rows, in a single query, instead of dumping the whole partition. Available on
PropertyListStoreand on theActorInterfacewrapper. Both halves of the return are scoped:namesholds only the matching lists, so code migrating fromlist_all_with_rows()that keeps iteratingnamessilently stops seeing every list outside the prefix — this is a contract, not a caveat.prefixis a prefix, not a namespace: it also matches a list named exactlyprefixand siblings like{prefix}-old, so pass the delimiter ("memory_", not"memory") if you mean a namespace. It is not universally cheaper: on a measured account the whole-partition dump was 1,361.0 RCU over 11 queries and the five scoped reads covering the same lists were 1,363.5 over 15, so replacing one dump with several scoped calls is marginally worse. It pays when you want one namespace, or when you issue several concurrently — the library stays synchronous and spends one query per call, so the latency win is the caller’s to take. Reads are eventually consistent, matching what the dump already did. An empty prefix raisesValueError, and a backend fault raisesDbErrorrather than swallowing to([], {})aslist_all_with_rows()does — for a scoped read an empty result is the ordinary answer, so a swallowed throttle would read as content. There is deliberately no names-onlylist_prefix()sibling: a keys-only projection saves no DynamoDB read capacity.``actingweb.property.rows_for(names, rows)`` — the subset of a
rowsdict attributable to a given set of list names, using the library’s own row encoding. For narrowing a(names, rows)pair after pruningnames. A barestartswith(f"list:{name}-")is wrong here: for listfooit also claims siblingfoo-old’s rows, and used to prune it strips a permitted sibling’s item rows while keeping its-metarow, after whichto_list_from_rows()returns[]with nothing raised.``DbPropertyProtocol.get_prefix(actor_id, prefix, keys_only, consistent_read)`` — read every property row of an actor whose name begins with a prefix, in one query. The sibling
get_rangecannot express: a prefix has no exact inclusive upper bound, and any synthesised sentinel (prefix + "~") is a guess about which byte sorts last that is wrong for names continuing past it. DynamoDB uses nativebegins_with; PostgreSQL usesstarts_with()with a bound parameter — notLIKE, so_and%are literal, and not aCOLLATE "C"bound pair, because byte ordering itself disagrees between collations whilestarts_withdoes not. Neither backend normalizes Unicode, so an NFD prefix does not match an NFC name — the same on both. A falsy prefix returns{}without touching the backend, since PostgreSQL would match every row and DynamoDB would raise.
CHANGED
Behavior change: a fully-loaded attribute bucket is now authoritative. After
Attributes.get_bucket()returns,get_attr()answersNonefor a name absent from the loaded dict without a point read — where before every absent name cost one read per instance. Nothing raises or warns, so this note is the discovery mechanism. What changes observably: a long-livedAttributesinstance loses the accidental first-miss re-read, so an attribute written by another process after this instance loaded the bucket is not seen byget_attr()on it. Library call sites are unaffected — everyAttributesin the permission and token paths is constructed per call — but note thathandlers/mcp.pycaches anActorInterfaceon a sliding five-minute TTL, so instances there can outlive a single request. Calldelete_bucket(), or construct a freshAttributes, to force a re-read. Three supporting corrections ride with it: the “loaded” flag is now set only when the backend actually returned a dict, so a faulted read can never present itself as “the bucket has no such attribute”;get_attr()no longer caches its misses into the loaded bucket, which used to makeget_bucket()report names that have no stored row; and adelete_attr()(or falsyset_attr()) that the backend reports as failed clears the flag, so the row the backend still holds is re-read rather than reported absent for the life of the instance.Behavior change: ``Attributes.set_attr()`` now mirrors the backends’ falsy delete. Both backends treat a falsy
dataas a delete and returnTrue—delete_attr()is literallyset_attr(data=None)— while the in-memory dict cached{"data": <falsy>, "timestamp": ...}. Soset_attr(name, data={})(or[],"",0,False) removed the row but left the name present in the cache. The name is now dropped from the cache too. “Absent” stays distinguishable from “present with a null value”: a stored row holdingnullstill reads back as the truthy dict{"data": None, "timestamp": ...}.The v1 list maintenance methods no longer dump the actor’s whole property partition.
verify(),compact()andmigrate_to_v2()read one list throughget_range()over that list’s own bounds — the read shape_v1_item_names_in_range()and every v2 counterpart already used. On an actor with many lists this is roughly one dump’s worth of read capacity saved per call rather than spent per list. The user-facing beneficiary is_maybe_lazy_migrate()(off by default, behindACTINGWEB_LAZY_MIGRATION_MAX_LENGTH), which runs all three inside a user’sappend()/insert(): three whole-partition dumps in one request become three one-list reads. Reports are unchanged, includingforeign_format_rows— v2 residue rows sort below the v1 bounds, so they now cost one extra keys-only range read, mirroring what_v2_verify()already spends counting v1 residue. On a single-list actor the saving is ~3×, not the headline ratio.
v3.14.2: August 28, 2026
FIXED
``GET /mcp`` answered an unauthenticated request with a ``200`` discovery document instead of the
401challenge — the other half of the same discovery bug as the entry below. The MCP authorization spec has clients find the authorization server by making an unauthenticated request to the MCP endpoint and readingWWW-Authenticate. A conformant client instead parsed this handler’s bespoke JSON body as the protected-resource metadata and failed on the missingresourcefield, never reaching the real metadata — which was correct all along. Codex reported exactly that: “Metadata error: Protected resource metadata missing required resource field”. An unauthenticatedGET— including one carryingAccept: text/event-stream, the spec’s stream opener — is now the same401challenge as every other MCP method. Behavior change: a caller that read the discovery document fromGET /mcpwithout a token now gets401. The document is unchanged and has not moved — an authenticatedGET /mcpstill returns it — but there is no unauthenticated copy of it. A client that needs server metadata without a token should read/.well-known/oauth-protected-resource/mcp, which is where the challenge points and which the MCP spec directs it to anyway;/mcp/infois a separate endpoint with a different shape, not a substitute. The.with_mcp(enable=False)404still takes precedence, so a disabled endpoint never advertises an authorization server.The MCP ``401`` challenge did not point at the protected-resource metadata.
WWW-Authenticatecarried only a non-standardauthorization_uri, omitting theresource_metadataparameter that RFC 9728 section 5.1 (and, through it, the MCP authorization spec from2025-06-18) requires. A conformant client had no discovery pointer: it either guessed the well-known location or reported the resource metadata as missing/malformed and never opened the sign-in window — the user sees a configured server that never asks them to authenticate. The challenge now carriesresource_metadata="<base>/.well-known/oauth-protected-resource/mcp"alongside the existingerror="invalid_token"andauthorization_urihints, at all three sites that emit it — including the async handler the FastAPI integration actually serves.
CHANGED
``offline_access`` is now advertised in ``scopes_supported`` on the authorization-server metadata and both protected-resource variants. The server already issues a refresh token with every authorization-code token response and already advertises the
refresh_tokengrant; clients that gate long-lived sessions on the scope being listed could re-prompt for authorization. The authorization endpoint does not validate the requested scope, so this is advertising catching up with behaviour — no token semantics changed, and the token response still reports the granted scope asmcp.
v3.14.1: August 25, 2026
FIXED
``@app.subscription_hook`` never fired. It registered into
HookRegistry._subscription_hooks, but nothing in the callbacks handler ever calledexecute_subscription_hooks()— the legacy fallback path (used when.with_subscription_processing()is not enabled) only invoked@app.callback_hook("subscription"). A hook registered with@app.subscription_hookwas silently never invoked. The legacy fallback branch inactingweb/handlers/callbacks.pynow also callsexecute_subscription_hooks(), so both mechanisms work; either can mark the callback as processed. Behavior change: on the legacy fallback path, a subscription callback that a registered@app.subscription_hookhandles (returns truthy for) now gets204instead of400— this is the fix taking effect, since such a hook’s return value was previously discarded. The legacy path does not track sequence numbers or clear diffs (that only happens via.with_subscription_processing()or an explicit PUT acknowledgment), so this does not affect data integrity.``.with_mcp(enable=False)`` didn’t actually disable the ``/mcp`` endpoint. The Flask and FastAPI integrations register
/mcpunconditionally, and neitherMCPHandler.get()/.post()norAsyncMCPHandler.post_async()checkedconfig.mcpbefore serving a full response — an app that disabled MCP still had a live, responding MCP server. All three now return404whenconfig.mcpisFalse. Behavior change: found while consolidatingexamples/demo/(which disables MCP) — a consumer relying on the endpoint silently responding regardless ofwith_mcp()will now see404unless it explicitly enables MCP.MCPHandler()’s default (test-only)Confignow defaultsmcp=True, since every existing caller of the no-arg constructor was specifically testing MCP behavior.Documentation taught APIs that do not exist or had the wrong contract:
@app.trust_hook(does not exist; the real mechanism is@app.lifecycle_hook("trust_fully_approved_local"/"trust_fully_approved_remote"/"trust_deleted")),@app.mcp_tool_hook(does not exist; use@app.action_hook(...)+@mcp_tool(...)),app.config(not public; useapp.get_config()),execute_action_hooks’s argument order (action_namecomes first, notactor), and the@resource_hookexample inactingweb/mcp/decorators.py’s docstring (the real mechanism is@app.method_hook(...)+@mcp_resource(...)). Also fixed: docs passing a literalpeer_id="peer123"after bindingcreate_relationship()’s real return value, indocs/guides/trust-relationships.rstanddocs/quickstart/getting-started.rst.with_sync_callbacks()’s docstring said its default wasTrue; the underlying setting defaults toFalseuntil the method is called. Clarified that calling the method at all is the opt-in.from actingweb.interface import lifecycle_hookraisedImportError; every other standalone hook decorator was exported except this one.``DbTrust.create()`` defaulted ``approved`` to ``””`` on both database backends, which PostgreSQL rejects outright on its boolean
approvedcolumn (invalid input syntax for type boolean) when a caller omits the argument. DynamoDB silently tolerated the empty string. Both backends now default toFalse, matching theTrustProtocolsignature. Every in-library caller already passed a bool, so behavior only changes for direct callers relying on the default — which previously crashed on PostgreSQL.The new p2p quickstart’s subscribe step 403’d as written:
create_relationship()only approves a relationship on the side that initiates it, so the peer’s side stayed unapproved andsubscribe_to_peer()was rejected — the guide’s Python narrative never had the peer approve its side (its curl “Verify” walkthrough happened to, independently, which is why manual testing during development didn’t catch this).examples/p2p_quickstart.pynow registers anon_trust_request_receivedlifecycle hook that auto-approves incoming requests (demo-only — flagged as such in both the code and the guide);docs/guides/p2p-quickstart.rst’s curl walkthrough now also captures each actor’s passphrase from its creation response and sends the Basic auth every non-creation request actually requires, rather than omitting it and discarding the passphrase. Verified end-to-end against a running server. Also fixed:examples/mcp_quickstart.pyhad the same missingproto="http://"override as the p2p example did before this release — the OAuth2 redirect URI generated for Stage 2 pointed athttps://localhost:..., which nothing on the plain-HTTP uvicorn server it starts answers on.
CHANGED
CI now enforces
ruff format --checkalongsideruff check, and the 19 files that had drifted from the pinned formatter (0.15.20) were reformatted in a mechanical commit.README documentation pointers are now absolute readthedocs.io URLs instead of bare
docs/...rstpaths, which render as broken text on PyPI (a:doc:role only resolves inside the Sphinx build). Repository-only files (CONTRIBUTING.rst,CLAUDE.md,CHANGELOG.rst) are unaffected — those are correctly filesystem paths, not published docs.MCP quickstart is now explicitly a two-stage recipe. Previously the pasted example had OAuth2 configuration commented out, so following it produced a server where every MCP method beyond
initialize401s with no explanation. The example now configures OAuth2 (reading credentials from the environment) and the guide states plainly that Stage 2 (real OAuth2 credentials, a bearer token) is required beforetools/listandtools/callwork, linking to the token-acquisition steps rather than leaving a 401 unexplained. The database prerequisite (previously unstated) is now inline, matching the peer-to-peer quickstart.PyPI discovery metadata: the one-line description now names what the library does (per-actor MCP servers, OAuth2, peer-to-peer data sharing) instead of “The official ActingWeb library”;
keywordsgainedmcp,ai,llm,agent,model-context-protocol, andactor; added theTopic :: Scientific/Engineering :: Artificial IntelligenceandProgramming Language :: Python :: 3.13trove classifiers;homepage/documentationURLs are nowhttps://; and a[tool.poetry.urls]block adds Changelog/Issues links to the PyPI sidebar.``actingweb/__init__.py`` now has a module docstring: the two headline capabilities, the modern entry point (
from actingweb.interface import ActingWebApp), the MCP decorators, links to the MCP and peer-to-peer quickstarts, and a note that every hook boundary is erased toCallable[..., Any]despitepy.typedshipping — a type checker will not catch a wrong hook signature.__all__(the legacy lazy-load surface) is unchanged; a comment now says so explicitly.``AGENTS.md`` rewritten, replacing 105 lines of contributor guidance that had drifted from
CLAUDE.md(a nonexistentthoughts/shared/path, a structure diagram omitting the PostgreSQL backend, a three-file version-bump instruction contradicting the tag-driven release process, and zero mentions of MCP, trust, or subscriptions) with a ~40-line pointer toCLAUDE.mdplus MCP/peer-to-peer quickstart links for anyone building an application with the library. Also removedAGENTS.mdfrom.github/workflows/claude-code-review.yml’spaths-ignore— that exemption is what let it go eight months without automated review whileCLAUDE.mdstayed current.Superseded-API warnings added to every migration guide (
docs/migration/v3.1.rstthroughv3.14.rst) and inline markers on illustrative (non-real) signatures indocs/contributing/style-guide.rstandarchitecture.rst. Greppingdocs/for trust-creation calls previously returned several hits with no indication of whether the code shown was current, historical, or never real to begin with.
ADDED
``examples/demo/``: the full ActingWeb demo application (OAuth2 login, the complete hook system, a customized web UI — a pure ActingWeb protocol example, not MCP; see
examples/mcp_quickstart.pyfor that) moved into this repository from the separateactingwebdemorepository, so the reference application is version-locked to the library and exercised by this repository’s own test suite instead of drifting against a floating>=dependency pin. Deployment (AWS credentials, the OAuth client secret, thedemo.actingweb.iocustom domain) stays inactingwebdemo, which remains the deployment pipeline for this code — seeexamples/demo/README.md. Repo-only: not part of the published wheel.Agent Skill for building applications on ActingWeb (
skills/actingweb-app/): task recipes — add a property hook, expose an MCP tool, establish trust and subscribe to a peer, configure a custom trust type’sacl_rules, look up an actor by property value — for AI coding agents working in a repository that merelypip install``s this library, which is the one surface that reaches them (everything else in this release improves what such an agent finds *if it comes looking*). ``git clonethis repo and point your agent at the directory, ornpx skills add actingweb/actingweb.``llms.txt`` / ``llms-full.txt`` are now generated on every docs build via
sphinx-llms-txt, and will be served athttps://actingweb.readthedocs.io/en/latest/llms.txtonce this lands. Adopted as a substrate for tools that read it (a human pointing an agent at the docs) and for the Google Lighthouse audit that now checks for it — not on a claim that AI agents fetch it live at request time; the evidence for that is thin.Two docs guides published to Read the Docs for the first time:
docs/guides/caching.rstanddocs/guides/oauth-login-flow.rstwere previously.mdfiles — invisible to the Sphinx build (source_suffixis.rstonly) despite being real, substantial content. A reader with the repo checked out could find them; a reader on readthedocs.io, which every other change in this release now points at, could not. The other two.mdfiles underdocs/in the same situation (docs/guides/postgresql-migration.md,docs/contributing/TESTING.md) duplicated a larger, current.rsttwin and were deleted rather than published.
v3.14.0: August 21, 2026
Note
This release makes property lists faster and cheaper to use,
especially for code that looks up list items by position in a loop
(for i in range(len(lst)): lst[i]) — that pattern can be
surprisingly slow on larger lists. The fix is a new way to find and
change items by value instead of position: find(),
items_with_handles(), remove_where(), update_where() and
friends. See docs/migration/v3.14.rst for the full guide, including
three small breaking changes.
SECURITY
A peer with only read access to a property list could still write to it.
AuthenticatedPropertyListStore— the permission-checked way apps expose property lists to peers — only checked read permission before handing back a list object, and that object was fully editable. So a peer granted read-only access could add, change, or remove items anyway. Plain properties (not lists) were never affected; this was specific to lists, and has been present since the feature was introduced. It’s now fixed: list writes require write (or delete) permission, matching how plain properties already worked. If any peer’s trust type is meant to be read-only on a property list, it’s worth checking that peer’s recent activity for writes it shouldn’t have been able to make.
CHANGED
Breaking:
get_metadata()["length"], and thecountfield the REST API returns for a single list, are now a close estimate rather than a guaranteed-exact count. In practice the estimate can only be off by a handful, and it self-corrects.len(actor.property_lists.<name>)and actually iterating a list remain exact, always, on every list format — this only affects that one metadata field and that one REST field. Seedocs/guides/property-lists.rstfor a recipe if you rely on an exact count near a hard limit.Breaking:
AuthenticatedPropertyListStore.create()is removed. It never worked in any released version — every call raised an error — so there is no working code for this to break. Lists are created automatically on first write; there was never a separate creation step.Breaking (narrow): in a bulk request that both updates and deletes the same list item in one call, the delete used to win and silently discard the update. Now the delete is recognized as stale and reported back as a conflict instead. If you relied on the old “delete wins” behavior, send the update and delete as two separate requests.
Bulk list updates now report a conflict for an individual item instead of silently overwriting it, when something else changed that item at the same time. See
docs/guides/property-lists.rst“Bulk update items”.Updates made with the new value-based methods now notify subscribed peers using the item’s value, so a peer can still find the right item even if its position changed. Peers on an older ActingWeb version keep working exactly as before for every operation that existed before this release; the one exception is these new value-based update diffs, which carry no position for an old peer to fall back on, so a pre-3.14 peer does not apply them (it skips them without error). If peers you do not control replicate a list you mutate with
update_where()orupdate_by_handle(), they need 3.14 to see those updates.A previously-documented race window, where a list format upgrade could be silently undone by a change happening at the same moment, is now closed rather than merely bounded. See
ListMetadataContentionErrorbelow.
ADDED
Look up and change list items by value, not position – the headline feature of this release:
find(key, value)/find_all(key, value)– find the item(s) whose field matches a value, in one read.items_with_handles()– fetch every item in a list along with a short-lived reference (“handle”) you can use to update or delete that exact item afterward. Handles are meant to be used right away, not saved for later – see the migration guide for why.delete_by_handle(handle)/update_by_handle(handle, item)– change or remove one item by its handle. If something else already changed that item, the call safely does nothing and reports that instead of overwriting it.remove_where(key, value)/update_where(key, value, item)– remove or update every item matching a value, in one call.
All of the above are available directly on
actor.property_lists.<name>and go through the same permission checks as the library’s existing list methods. Seedocs/guides/property-lists.rstfor full examples.consistent=False, an opt-in parameter on list-reading methods (to_list(),slice(),find(), and similar) for reads that can tolerate being a moment out of date, in exchange for lower cost and better performance. Off by default everywhere, including inside the library itself.ListMetadataContentionError(importable fromactingweb): a new, specific, catchable error for the rare case where a list update can’t complete because of a conflicting change happening at the same moment. The library’s own request handlers turn this into an HTTP 503 “please retry” response automatically.PropertyListStore.list_all_with_rows()– for apps that read several of an actor’s lists at once, this fetches everything in a single read instead of one read per list.actingweb-verify-orphans, a new command-line tool that scans your database for leftover data belonging to actors that no longer exist (for example, from an interrupted deletion). It only reports what it finds and never deletes anything on its own. Seedocs/reference/actor-deletion.rst“Finding orphaned rows”.Clearing or deleting a whole list now happens in a small number of batched operations instead of one step per item – much faster on large lists, with no code changes needed.
FIXED
actor.property_lists.<name>.get_metadata()was silently unreachable through the normal way apps access lists, despite being a documented, public method. It now works as documented.A list item whose value is
Nonenow replicates to subscribed peers like any other value. Previously a diff for such an item omitted itsitem/old_itemfield entirely, and the receiving side dropped the whole notification as unrecognized – silently, the same failure mode as theremove()bug below.When a peer receives a value-based update it cannot apply (the value matches no row, or more than one), it now logs a WARNING naming the list and suggesting a resync. The update was already (correctly) not applied in that case; what was missing was any operator-visible signal that the replica had just diverged.
remove_where()/update_where()on the coreListPropertylayer now capture the values they return during the same scan that matches them (v1 format) – previously a second positional read could return a different value than the one actually removed or replaced if a concurrent writer landed in between.actingweb-verify-orphansre-run against a checkpoint in which every table is already complete now says so (REPLAYED FROM CHECKPOINT) and exits with a distinct status (3) instead of reprinting the earlier scan’s findings as if they were current.Removing an item from a list wasn’t reaching subscribed peers. A bug meant
remove()never actually notified peers, for as long as the feature has existed – the notification was silently dropped every time. This is now fixed. If you have peers subscribed to a list that usesremove(), they may be out of sync with removals made before this upgrade; a manual resync is the safest way to catch them up if you suspect this affected you.On PostgreSQL, reading properties for an actor with no data at all returned an empty result differently than on DynamoDB. Both backends now behave the same way; unlikely to have been noticeable in practice.
v3.13.0: August 15, 2026
Note
This is the consolidated release. It supersedes ``v3.13.0rc1`` through
``v3.13.0rc6``, whose separate sections have been merged here. Changes
that existed only between release candidates — defects introduced by one
pre-release and fixed by the next — are deliberately not listed: nothing
released before v3.13.0 ever exhibited them, so they are noise for
anyone upgrading from v3.12.0. If you pinned an rc and want the
intermediate history, it is in the git log for CHANGELOG.rst.
Warning
Upgrading from ``v3.12.0`` requires reading three things, in this order, because two of them change behaviour rather than only fixing it:
The SECURITY section below. An MCP authorization bypass affected every release from
v3.3(2025-10-04) onwards. The library cannot tell you whether it was exploited in your deployment; the migration guide says what to check.The three entries marked Breaking under CHANGED — the
/properties/<name>/itemsresponse shape, list reads failing fast on corruption instead of silently compacting, and out-of-boundsPUTnow returning 404.docs/migration/v3.13.rst, in full. In particular: a green test suite is not evidence this release landed correctly. The read-path and capacity fixes are invisible to functional tests, and the guide gives an operation-profile recipe for confirming them against your own hot paths.
Two operator tools ship with this release —
actingweb-verify-property-lists and
actingweb-migrate-property-lists. Automatic conversion of existing
lists to the v2 storage format is off by default; read the migration
guide before turning it on, and note that compact() is documented as
not crash-safe (see DOCUMENTATION).
SECURITY
Fixed an MCP authorization bypass where one client’s permissions could be served to a different client on the same actor. The trust-relationship cache used by the MCP authentication path was keyed by actor id alone, not
(actor_id, client_id). On an actor with more than one registered MCP client, once the cache was warm, one client’s resolved trust relationship — and therefore its entire permission rule set — could be silently served to a different client’s requests on the same actor. This has been demonstrated in practice: a read-only client performed a write immediately after a read-write client authenticated against the same actor. The trust cache is now keyed by(actor_id, client_id), closing the bypass. Affected versions: every release sincev3.3(2025-10-04) throughv3.13.0rc2, roughly ten months (i.e. fixed in this release). The library cannot detect whether this was exploited in your deployment; see the migration guide for what to check.``resources/read`` had no authorization check at all on Flask (sync) deployments. The permission gate read an actor attribute (
_mcp_trust_context) that no production code ever wrote, so the check silently fell through and every resource read was served regardless of the requesting client’s trust type or permissions. FastAPI (async) deployments were not affected — the async handler already read the correct runtime context. The sync path now uses the same mechanism as async and is authorized identically. If your Flask deployment serves resource URIs beyond the defaultmcp_clienttrust type’s narrow pattern set (public/*,shared/*,notes://*,usage://*,actingweb://properties/all), see the audit step indocs/migration/v3.13.rst— those requests have been working only because the check was dead.Missing trust is now fail-closed instead of fail-open. A valid MCP access token that cannot be resolved to a trust relationship (a broken or orphaned trust row, or an eventual-consistency gap right after registration) previously granted full access. It now returns an empty
tools/list/resources/list/prompts/listand a distinct-32003error naming the cause ontools/call/prompts/get/resources/read, rather than silently permitting the request. A short negative-TTL cache bounds transient eventual-consistency misses to seconds rather than the full token cache window. Deployments relying on the permission subsystem being unavailable (a genuine outage, as opposed to no trust being found) are unaffected — that failure mode is still fail-open, deliberately, so a permission-subsystem outage does not lock out every client.The MCP client-id-to-trust resolver now matches exactly instead of by substring. The previous resolver checked whether the OAuth2 client id appeared anywhere inside a trust row’s peer id (
client_id in peer_id_str), which a peer id crafted through the ordinary/trustpeer protocol could satisfy without ever going through MCP client registration. The resolver now requires an exactoauth_client_idmatch, or — for legacy rows created before that field existed — an exact, fully-reconstructed peer-id match gated on the row’sestablished_viabeing an OAuth2-family value. Seedocs/migration/v3.13.rstfor how to find trust rows that predateoauth_client_idand may need re-authorization after upgrading.Trust-row client metadata written during the affected window is unreliable.
client_name,client_version,client_platform,last_accessed, andlast_connected_viamay have been overwritten by a different client than the one that actually holds the credential, for any trust row touched while the cache-crossing bug was live. Current values self-heal on that client’s next request after upgrading (current state, not history — the metadata is a live cache with no audit trail).
CHANGED
Lookup-table mode is now the default reverse-lookup mechanism (
use_lookup_tabledefaults toTrue; the deprecated legacy GSI/index mode can be pinned withwith_legacy_property_index(True)orUSE_PROPERTY_LOOKUP_TABLE=false). Upgrading deployments keep resolving reverse lookups through deprecated fallbacks (v1 lookup table, then the legacy GSI where present) with per-hit warnings and a loud startup ERROR until the newscripts/backfill_property_lookup.pyis run — seedocs/migration/v3.13.rstfor the per-deployment decision tree. Note the semantic change: lookup-table matching is per (property name, value); the legacy GSI matched on value alone.Freshly created properties tables match the configured reverse-lookup mode. In lookup-table mode, auto-created
<prefix>_propertiestables no longer carry the legacy value-keyedproperty-indexGSI — previously every fresh deployment inherited it even when nothing read it, paying double write/storage cost and rejecting any property value over DynamoDB’s 2048-byte GSI-key limit (values over 2 KB now verified to store correctly). Legacy mode still creates the GSI so the legacy reverse-lookup path works. Existing tables are never altered.Property reverse-lookup table redesigned (v2, digest keys). On DynamoDB, lookup rows now live in a new
<prefix>_property_lookup_v2table keyed by a SHA-256 digest of (property name, value); the plaintext value is no longer stored. This removes the old design’s hot-partition key (all emails shared one partition), its undocumented 1024-byte value cap, and plaintext PII in key material. Writes are conditional: a value already mapped to a different actor is now a loudly-logged collision instead of a silent last-writer-wins overwrite (PostgreSQL gained the same idempotent-create/collision semantics). Existing v1 lookup tables are read as a deprecated fallback; rebuild the v2 table from the properties table with the backfill script, verify, then drop the v1 table. Reverse lookups for property names not configured as indexed now returnNonewith a warning instead of silently using the legacy path, and the legacy GSI path raises an actionable error when the index is missing from the live table instead of an opaque backend exception. Lookup write failures are logged at ERROR (previously swallowed — a failed lookup write silently broke login-by-email for that user).Hot-path read amplification removed across the property and actor paths.
GET /<actor>/propertiesnow serves the entire response (simple properties, list discovery, list metadata and list items) from a single partition read — it previously re-read every property individually after the bulk read, read the partition a second time for lists, and re-read each list’s metadata row. Single-property reads no longer pay an extra list-existence check on every hit, repeat property writes skip the list-collision read, the actor’s internal attribute bucket is loaded lazily (once, instead of eagerly twice per actor construction), attribute buckets in general no longer load fully on construction, bulk property deletion reads its partition once instead of twice, and permission evaluation caches confirmed-absent trust overrides instead of re-reading per request.Auto-created DynamoDB tables now use on-demand billing. Tables the library creates were provisioned with tiny fixed capacities inherited from old Meta defaults (the property lookup table — the login path — got 2 read units / 1 write unit per second, a hard throughput wall). Newly created tables are now
PAY_PER_REQUEST. Existing tables are not changed — convert them once, in place (never recreate; the lookup table holds live login data):aws dynamodb update-table --table-name <prefix>_property_lookup \ --billing-mode PAY_PER_REQUEST
(AWS allows one billing-mode switch per table per 24 hours. Check
<prefix>_peertrusteestoo.)Per-actor DynamoDB reads no longer scan the whole table. Nine call sites (property fetch/delete, trust list fetch/delete, peer-trustee fetch/lookup/delete) issued a full-table
Scanwith a partition-key filter — read cost grew with total table size, not the actor’s data (measured ~2,000 RCU per property fetch on a 16 MB table). All are now partitionQuerycalls; the trust reads keep their strong consistency. Trust and peer-trustee list results are now range-key sorted (deterministic order) instead of arbitrary scan order.DynamoDB table-existence checks are now memoised per process. Every accessor construction used to issue a live
DescribeTablecall (measured at >1,000/minute in a near-idle deployment); the check now runs at most once per table per process, and all tables are pre-warmed concurrently at Flask/FastAPI integration time instead of serially on the first request.``RuntimeContext`` is now genuinely request-scoped, and ``set_custom_context()`` no longer persists across requests. Runtime context (MCP/OAuth2/web) was stored as a mutable attribute on the actor object, while the MCP handler deliberately hands the same cached
ActorInterfaceto every request for a hot actor — so context could leak between requests and, under concurrency, between callers. It is now stored in acontextvars.ContextVarkeyed by actor id (task-local under asyncio, thread-local under a WSGI worker), and the Flask and FastAPI integrations clear it at the end of every request including when a handler raises. The public API (RuntimeContext(actor)and all its getters/setters) is unchanged, and read-only use inside request-scoped hooks needs no changes. The one behavior change for application code: data written withset_custom_context()is gone by the next request, even against the same actor object. If your hooks used it as a cross-request cache rather than as request-scoped data, move that to actor properties or the caching guide’s patterns (docs/guides/caching.md). Seedocs/guides/hooks.rst.``tools/call`` permission checks now pass ``operation=”use”`` on FastAPI, matching Flask. The async handler passed
"invoke"while the sync handler passed"use", a transport-dependent divergence. It is unread by every trust type shipped with ActingWeb (they expresstoolsas allowed/denied lists, which do not consultoperation), so this changes nothing unless your application defines a patterns/operations-based tools rule — in which case a rule that matchedinvokeon FastAPI must now matchuse.MCP ``structuredContent`` is now opt-in.
tools/callresults emitstructuredContentonly when a tool hook sets that key explicitly, and only when its value is a JSON object. Extra top-level keys are no longer promoted intostructuredContent.Why this changed: at least one major MCP client discards every text content block when
structuredContentis present on a result. Under the previous behaviour, adding a single scalar key to a hook’s return value silently deleted that tool’s entire prose payload — the model received only the promoted keys, and nothing in the protocol reported the loss to either side. Neither reference server implementation promotes individual keys: the SDK’s low-level server emitsstructuredContentonly whencontentis its exact serialization, and FastMCP only for a declared return model.This is also a hardening improvement. A hook doing
return {"content": [...], **rel.to_dict()}was shipping the trust row’ssecretto the model;properties.to_dict()can likewise carryoauth_token/oauth_refresh_token. Only an explicitly namedstructuredContentnow leaves the process. Note the win is scoped to thecontentbranch — the legacy text-wrap path still stringifies the whole dict.Migration: nest the data you want structured under an explicit
structuredContentkey, and keep the same object serialized in a textcontentblock (the spec’s backwards-compatibility guidance — some clients ignorestructuredContententirely). For tools whose payload is prose, drop the extras instead. The explicit passthrough already exists inv3.11.0andv3.12.0, so migrated hooks produce identical output on both, and no coordinated deploy is needed.New list properties now use a v2 storage format (fractional rank keys) instead of dense integer indices. Delete and insert are now a single conditional write each instead of a shift loop over every following item – the interrupted-shift corruption class Phases 1-3 hardened against structurally cannot occur in a v2 list, because there is no separate stored
lengtha row could disagree with; position is always derived by sorting present rows.to_list()/slice()/ iteration remain a single query regardless of list size. Existing lists are untouched and keep working exactly as before (the v1 format, including everything Phases 1-3 hardened, is fully supported indefinitely) until migrated. New list names may no longer contain#(reserved for internal storage keys) –ListPropertyraisesValueErrorimmediately on first use of such a name; existing v1 lists already named this way are unaffected.list:-prefixed property names are now also structurally excluded from the property-lookup table sync, regardless of configuration.scripts/verify_property_lists.pynow understands both formats – v2’s “unhealthy” signal is rank keys approaching the length cap (compact()rebalances them), not holes/orphans, which are structurally impossible under v2.Existing v1 lists migrate to v2 automatically and gradually. Small lists (<= 50 items) migrate lazily the next time they’re mutated (
append/insert/item__setitem__/__delitem__); a failed lazy migration is logged and the original mutation still succeeds as v1 – migration is a background upgrade, never a reason an ordinary write can fail. Larger lists stay fully functional as v1 until swept by the newscripts/migrate_property_lists.py(dry-run by default;--migrateto perform it; reports refused names and duplicate residue;--downgrade ACTOR_ID/list_nameis an emergency-only v2->v1 converter for rollback scenarios).ListProperty.migrate_to_v2()is idempotent and safe to interrupt and re-run at any point, including across concurrent v1 mutations between attempts.Breaking: list-property reads now fail fast on corruption instead of silently compacting past it.
ListProperty.to_list(),.slice()and.to_list_from_rows()raiseListCorruptionError(anIndexErrorsubclass) when an item within the list’s recorded length is missing from storage, matchingAttributeList’s existing contract. Every HTTP path that serves list content (GET /properties/<name>,?format=full,?metadata=true,GET/POST /properties/<name>/items, the bulk list-item POST, list DELETE) now returns 409 Conflict with{"error": "list_corrupted", "list": ..., "detail": ..., "remedy": "compact"}instead of a 500 or a quietly-wrong compacted response. Repair withListProperty.compact()(see ADDED below) before retrying.Breaking: ``GET /properties/<name>/items`` response shape changed. Was a bare JSON array (identical to
GET /properties/<name>); is now{"items": [{"index": i, "item": ...}, ...], "count": n}.indexis the storage index, matching whataction=update/action=deletealready expect initem_index— GET and POST are now consistent with each other. Flask gained this route for the first time (parity with FastAPI, which already had it).Breaking: ``PUT /properties/<name>?index=N`` beyond the list length now returns 404 instead of silently padding the list with
Noneup toN(an unbounded-write DoS vector as well as a spec violation — the spec requires 404 forindex > length;index == lengthstill MAY append).docs/protocol/actingweb-spec.rst“List Property PUT”.The bulk list-item POST (
POST /propertieswith a{"items": [{"index": N, ...}]}-shaped list value) now applies every update before any delete, regardless of the order items appear in the request, then applies deletes in descending index order. Previously, processing in request order meant a delete appearing before an update in the same batch could shift the update onto the wrong item. All indices in a batch are now interpreted against the list as it stood before the batch, consistently.The bulk list-item POST now rejects an update index beyond the end of the list with 400, the same rule
PUT ?index=Nfollows. A batch may still populate a list with consecutive indices (0, 1, 2, …) — the bound advances with each append the batch performs — but it can no longer name an arbitrary index and have the gap padded withNoneone row at a time, which turned a single request into as many database writes as the index was large. The whole batch is validated before anything is written.``verify()`` accepts an ``identity_key``, adding a
duplicate_identitiesreport that catches duplicates the existing byte-adjacency heuristic structurally cannot. That heuristic is blind two ways, and both were hit in a real deployment: it stops finding a duplicate the moment either copy is edited (so it misses exactly the lists that have been used since the damage), and it only looks at neighbouring rows (so it missed the sameidat positions 31 and 36 and called the list healthy).duplicate_identitiescompares the identifying field across the whole list and survives later edits, and the report carriesidentity_checked_countso an empty result is distinguishable from a mistyped key that compared nothing. Both sweep tools gained--identity-key; the migrate tool’s dry-run duplicate warning previously used only the unreliable comparison.The operator tools now ship with the library as the console commands
actingweb-verify-property-listsandactingweb-migrate-property-lists(implementation moved toactingweb.maintenance;scripts/keeps thin wrappers). They were previously absent from the wheel, which mattered once converting existing lists became an explicit operator step rather than something that happened on its own.Both sweep scripts now print the backend, region/host and table prefix they are about to operate on, and warn when
AWS_DB_PREFIXis an unset default. The library defaults it todemo_actingweb; running a sweep without the application’s environment could silently target a different deployment and report it clean, which looks exactly like good news.scripts/verify_property_lists.pyno longer deletes the checkpoint file on a clean read-only run — it is now gated on--repair, matching the migrate script. A dry run could previously remove the resume state of an interrupted--repair.Lazy migration now refuses a list that ``verify()`` reports as unhealthy, logging what it found and how to fix it, instead of migrating it. Migration closes holes in flight and reports what it closed, which is right for an operator running
scripts/migrate_property_lists.pyand reading the output — but doing it silently under an ordinaryappend()destroyed the evidence: the hole disappeared, the lost item stayed lost, duplicate residue was promoted to real data, andverify()began reporting the list healthy. Repair (compact()orverify_property_lists.py --repair) is now always an explicit operator action; damaged lists keep serving v1 and keep raisingListCorruptionErroruntil then.Automatic conversion of existing lists to the v2 format is OFF by default, controlled by the new
ACTINGWEB_LAZY_MIGRATION_MAX_LENGTHenvironment variable (default0; set a positive number to allow v1 lists of at most that size to convert on their next write, or usescripts/migrate_property_lists.py). This does not make v2 opt-in — every list created after upgrading is v2 with no operator action; only the conversion of pre-existing data waits for a deliberate decision. The default is off because it is a rollback-safety control before it is a latency one. A pre-3.13.0 process does not error on a migrated list — it reads it as empty, because a v2 list stores nolengthfield and an older reader takes the absence as zero; a write from that process then forks the list across both formats with nothing reporting an error, and--downgradecannot reconcile a forked list. Deployment gives a brief mixed-version window; rollback gives none — every list that migrated before the rollback reads as empty afterwards, recoverable only one list at a time. With the limit at 0 the release changes no data, so rolling back is a pure code rollback. Secondarily: migration is synchronous, so oneappend()to a 40-item list performs the whole migration — dozens of sequential writes plus two full-partition reads — inside that request. The library logs one INFO per process the first time it encounters a v1 list while conversion is disabled, naming the script, so “off” does not quietly become “never”.ListProperty.storage_format()(new, public) returns 1 or 2 from a single point read.scripts/migrate_property_lists.pynow uses it instead ofverify()for format detection, which was fetching the actor’s entire property partition once per list.Migration now refuses a damaged list.
actingweb-migrate-property-lists --migrateandListProperty.migrate_to_v2()refuse a list whoseverify()reports missing or orphaned indices, and the dry run reports those lists as needing repair rather than counting them as “would migrate” — it exits1when any exist, so0means the migration has nothing to trip over. Migrating damaged data is not merely lossy but unreportably lossy: migration renumbers the survivors, so afterwards the hole is gone, the list verifies healthy, and nothing is left to say an item was destroyed. An earlier form of the dry run warned about duplicate residue and said nothing whatsoever about holes, so a sweep over a fleet containing one could report “0 refused, 0 errors” and an operator would proceed past the last moment the damage was visible. Lazy migration already refused damaged lists on exactly these grounds; this applies the same rule to the deliberate path. Repair first (actingweb-verify-property-lists --repair), or pass--migrate-damaged/migrate_to_v2(allow_damaged=True)to migrate anyway — which logs what it is giving up.Duplicate residue is unaffected and still migrates: it stays visible after conversion, since a v2 list’s
verify()reports duplicates the same way a v1 list’s does, so migrating it destroys no evidence. Only holes and orphans gate.A successful migration that closed holes (only reachable via
--migrate-damaged) now logs at WARNING rather than INFO.``–repair`` refuses a reverted migration instead of blessing it as healthy. A v1 list that is damaged and carries rows of the v2 storage format is not an ordinary hole: it is a list whose migration was reverted, with its items alive in the v2 rows and the v1 range empty.
compact()used to rewrite that empty range, set the length to what it found, report the list healthy, and leave the only surviving copy as unreferenced residue for the nextclear()/delete()/migrate re-run to sweep — the same unreportable loss the entry above refuses damaged lists to avoid, in the other operator tool.compact()now refuses that combination and says why;allow_reverted=True(actingweb-verify-property-lists --repair-reverted) is the deliberate override, mirroring--migrate-damaged. The verifier’s log line for foreign-format rows splits accordingly: on a healthy list it still reports inert residue and how to clear it; on a damaged v1 list it now warns that those rows are probably the data and that sweeping or repairing destroys them. The previous text said “harmless to reads; clear it by re-running –migrate” unconditionally, which in this shape was an instruction to delete the only surviving copy.A write from a ``ListProperty`` held across a migration is no longer silent. Such an instance dispatches on its cached storage format, so the item lands in a row of the old shape that readers of the new format never return — and
verify()reports the list healthy, because the row surfaces only asforeign_format_rows, which is documented as inert residue. It now logs a WARNING naming the list, both formats, and what to do. Bounded to one write per retained instance: the metadata write refreshes the cache, so the instance self-corrects from there. The write itself is still lost — closing that needs dispatch on a fresh metadata read, which is deferred.This needs an instance retained across a migration.
PropertyListStore.__getattr__builds a freshListPropertyon every attribute access, so ordinaryactor.property_lists.<name>use has a one-request window rather than a process-lifetime one.Each list mutation now costs one additional point read. Merging into a fresh read of the metadata row is what makes the fix above work, and it is not free: where a mutation previously wrote metadata from cache, it now reads the row first. One extra round trip per
append()/insert()/pop()/remove()/__setitem__()/__delitem__(), on both backends. The alternative is a compare-and-set primitive neither backend’sDbPropertyexposes; correctness was preferred over the round trip, and the trade is recorded rather than buried.The migration and property-list guides now state what a whole-list rewrite does and does not exclude. Nothing locks out a concurrent application write, so “run repair when the actor is not taking writes” remains a real requirement; what is now guaranteed is that such a write cannot change the list’s storage format. Two concurrent migrations resolve to last-writer-wins at whole-list granularity — accepted and documented rather than prevented.
The MCP unsupported-protocol-version rejection now names the supported versions and logs at WARNING. A client sending an
MCP-Protocol-Versionthis server does not speak still gets HTTP 400 with JSON-RPC-32600and nodatapayload — that response shape is deliberately unchanged, because it is the signal dual-era MCP clients use to recognise a legacy-era server and fall back toinitialize. What changed: the errormessagenow lists the versions that would have worked, which for a client that cannot fall back is the only diagnostic it can show a user; and the rejection is logged at all, which it previously was not.The logging is graded per origin rather than uniform, because the rejection is answered on an unauthenticated path. A lone rejection is the normal legacy-server handshake and is recorded at INFO; only a sustained run from the same origin — a client retrying instead of falling back, i.e. a modern-only client — escalates to WARNING, once per origin per five-minute window. That keeps the actionable signal visible without handing anonymous callers a log-volume lever, and it makes the “one-off versus sustained” judgement in code instead of leaving it to whoever reads the logs. Origin is taken from
Mcp-Session-Idor theUser-Agent; the number of tracked origins is capped.The response code and the absence of
data.supportedare now covered by regression tests that state why, since “fixing” this to a spec-shaped-32022withdata.supportedwould silently convert every dual-era client’s working fallback into a retry loop.Revoking a token, deleting a trust relationship or downgrading a permission now evicts the MCP caches.
MCPHandler.clear_token_from_cache()had exactly one caller — the logout handler — so every other path that invalidates a client’s identity or authorization left the in-process MCP caches serving the old answer for up to their five-minute TTL. A revoked token kept authenticating, a deleted trust kept authorizing, and a narrowed permission kept being honoured at the old level, from any warm process.Eviction is now wired into
TokenManager.revoke_token(), the SPA session pathsrevoke_access_token()/revoke_refresh_token()(which is what/oauth/revokereaches),revoke_token_chain()(the refresh-token-reuse theft response),revoke_all_tokens(), trust deletion, and both the store and delete paths ofTrustPermissionStore. It is actor-wide by necessity rather than scoped to one client: the cachedActorInterfacecarries the trust list itself, so evicting a single(actor, client)entry would leave the shared wrapper answering from stale state. One consequence worth knowing: any permission change on one client invalidates the cache for every MCP client connected to that actor.Eviction alone would not have been enough. A request that read a token from storage just before the revocation could write what it read back into the cache just after, reviving the credential for a full TTL while the eviction reported success. A generation counter, bumped by every eviction and snapshotted by each request before the reads that feed the caches, makes such a request decline to populate them.
This closes the window within one process only. Every MCP cache is a module global, so a multi-worker or multi-container deployment still serves the stale entry from every other process until its TTL expires. Closing that needs a shared invalidation channel and is not attempted here.
``tools/list`` now warns when a hook’s ``output_schema`` will not be advertised.
outputSchemacomes solely from@mcp_tool; a schema passed to@app.action_hook(..., output_schema=...)or derived from aTypedDictreturn annotation is stored separately and never bridged, so an author who declares one either of those ways gets silence rather than an advertised schema — and the missing-structuredContentwarning stays quiet for them too, because it is gated on the same metadata. The warning fires once per tool per process, at listing time.Deliberately a warning and not a merge: merging would newly advertise
outputSchemafor everyTypedDict-annotated tool, and each one that does not also returnstructuredContentwould begin failing on spec-conforming clients. That decision belongs with thestructuredContentwork and is not foreclosed here.
ADDED
DynamoDB table auto-creation can now be disabled for deployments that manage tables via infrastructure-as-code: set
AWS_DB_AUTO_CREATE_TABLES=falseor callwith_dynamodb(auto_create_tables=False). With auto-creation off, the library never callsDescribeTable/CreateTable, so both permissions can be dropped from the runtime IAM role.Actor deletion tombstones and a tri-state deletion check.
ActorInterface.get_deletion_status(actor_id, config)returnsDeletionStatus.DELETED/NOT_DELETED/UNKNOWNfrom a tombstone written before theactor_deletedhook runs — so an external call made from that hook, and any provider callback racing it, already seesDELETED— and retained for 30 days, past every provider’s webhook retry window.This exists because a guard of the form “skip this work if the actor no longer exists” could not be written correctly.
Actor.delete()removes the actor row last, soget_by_id()keeps returning a live actor for the entire wipe: the check fails open in exactly the window it matters, which the documented cleanup pattern enters deliberately (actor_deletedcancels an external subscription; the provider’s callback races the wipe). Checking harder is not available either, becauseget_by_id()returnsNonefor a deleted actor and for a failed read — so the same guard fails closed on a throttle, which for a paid-subscription webhook means the customer paid and silently never got access. A consumer hit both: 4 attribute rows in production belonged to fully deleted accounts.A tombstone is positive evidence, so it has a safe failure direction:
UNKNOWNmeans proceed, costing at most one orphan row an operator sweep can find, rather than dropping work for a paying customer. The read is a single strongly-consistent point read — measured at exactly oneGetItem, not asserted. Tombstones live under a reserved id that is never itself an actor, so no deletion can destroy them, and expired ones are filtered at read time so TTL means the same thing on both backends. Re-creating an actor clears its tombstone (create(actor_id=...)accepts a caller-supplied id).The actor row deliberately still goes last. Deleting it first would make existence checks fail closed rather than open, but “missing” is indistinguishable from “read failed” anyway, and it would cost the two properties that ordering buys: deletion stays retriable if a wipe step fails, and rows are never briefly classifiable as orphaned. The tombstone answers the question without that trade.
``actor_deleted_complete`` lifecycle hook, fired after the wipe completes with
actor=Noneandactor_id=<id>. There was previously nowhere to put “do this once the actor is definitely gone”, which forced applications into the race above by design:actor_deletedis the only place the actor’s data is readable, so it was also the only place to act on it. Now the work splits — read inactor_deleted, act inactor_deleted_complete— which removes the race at its source independently of the tombstone. The absentActorInterfaceis the point, not an omission.``get_attr_strict()`` on both attribute backends — a point read that raises on infrastructure errors instead of collapsing them into
None, and treats an expired row as absent. Used by the tombstone read; available to applications with the same need. Building the tombstone read onget_attr()would have reintroduced the very bug it fixes.Custom backend implementers: this method is now part of
DbAttributeProtocol, which is@runtime_checkable— an out-of-tree attribute backend must addget_attr_strict()to keep satisfyingisinstance()checks against the protocol. Both bundled backends implement it.Operator-facing table verifier:
python -m actingweb.db.verify_tablesreports which required DynamoDB tables exist and which are missing. With auto-creation disabled, “all tables exist” becomes a precondition the library deliberately no longer checks (an existence check would cost anAccessDeniedper accessor construction on a slimmed role), and the required set previously had to be read out of the source. Run it out-of-band with operator credentials — it only reads, never creates, and adds nothing to the request path.--listprints the names without calling AWS. Exit codes:0all present,1one or more missing,2the check could not run. The same list now drives table pre-warm, so what the library creates and what operators are told to pre-create cannot drift apart.``auto_create_enabled()`` is public API (
from actingweb.db.dynamodb import auto_create_enabled). DroppingDescribeTable/CreateTablefrom the runtime role affects the whole role, not just the library, so application code that probes its own tables (boto3table.load()/describe_table, pynamodbexists()) needs the same switch — previously it had to re-implement the environment parsing.set_auto_create()andreset_ensure_cache()are exported alongside it.MCP: a warning when a tool declares ``output_schema`` but returns no ``structuredContent``. Spec-conforming clients reject such a result outright (
Tool X has an output schema but did not return structured content), which was already broken before this release for anyone declaring a schema — the library advertisesoutputSchemaintools/listbut never consulted it at call time. The warning fires once per tool per process, at call time, and only when the negotiated protocol version supports structured content, so a correctly-written hook talking to an older client never trips it.output_schemaandstructuredContentremain independent: declaring a schema has never caused structured output to be emitted, and the library still does not validatestructuredContentagainst a declared schema.MCP: a warning when ``structuredContent`` is set to a non-object. MCP requires a JSON object there; a list, string or number was previously dropped silently.
Noneis exempt and stays silent — it carries no payload, both reference clients read a nullstructuredContentas absent, and{"structuredContent": value or None}is a legitimate way to express “nothing structured this time”. In that case the key is omitted from the response rather than emitted asnull.Property-list repair primitives.
ListProperty.verify()reports a list’s health (holes, orphan rows, a duplicate-value heuristic) without modifying anything;ListProperty.compact()closes holes and removes orphans in one pass while preservingdescription/explanation/created_at(unlike the previousclear()+extend()workaround, which reset them). Duplicate residue is reported but never rewritten — a duplicate value always means a destroyed item, and silently collapsing one copy would bless the data loss as intentional. Both are also available throughactor.property_lists.<name>.verify()/.compact(). New operator script:scripts/verify_property_lists.py(dry-run by default;--repairinvokescompact()on unhealthy lists).Opt-in diagnostics for the PostgreSQL attribute ``DELETE`` path. Set
ACTINGWEB_PG_DELETE_DIAGNOSTICS=1to log, per attribute delete, the statement’srowcount, the deleting connection’s resolved schema andsearch_path, and a post-commit re-read on a freshly checked-out connection. That combination distinguishes a delete that matched no rows (wrong schema or wrong key) from one that matched and did not persist — the question the quarantine above left unanswered for two months. Off by default. When on it costs a savepoint-wrapped schema read plus a post-commit re-read per delete, and cannot affect the delete it observes: the schema read runs before theDELETEand inside a savepoint, because a server-side failure there would otherwise abort the transaction and turn the later commit into a silent rollback — the exact defect the diagnostics exist to find.
Note
compact() crash atomicity is not addressed here and is deferred.
The warning boxes describing that window stand: the damage it
leaves is visible in the data and verify() catches it.
FIXED
Changing an indexed property left its old reverse-lookup row behind on DynamoDB. When an indexed value (e.g.
email) was updated through the normal properties API, the old lookup row was never deleted, so reverse lookup by the previous value kept resolving to the actor and blocked any other actor from claiming that value. The DynamoDB backend now reads the current value before writing, matching the PostgreSQL backend. (Only affected lookup-table mode, which is unreleased.)Reverse-lookup migration fallbacks matched on value alone, ignoring the property name. During a not-yet-backfilled migration, the legacy-GSI (DynamoDB) and sequential-scan (PostgreSQL) fallbacks could return an actor whose different property happened to share the requested value; both now filter by property name. (Migration-only path, unreleased.)
Two DynamoDB accessors never auto-created their tables. First use of subscription suspension (
DbSubscriptionSuspension) or the peer-trustee list on a fresh deployment crashed with a table-not-found error; both now go through the same table-existence guard as every other accessor.Property lookup env overrides were silently ignored by the fluent builder.
ActingWebAppstamped its own hardcoded lookup-table defaults onto the config on everywith_*()call (a guard intended to detect “explicitly set” was always true), so the documentedUSE_PROPERTY_LOOKUP_TABLEandINDEXED_PROPERTIESenvironment variables were clobbered for any app that called a builder method after construction — including the documented rollback path for lookup-table migration. The builder now tracks whether these settings were explicitly configured; precedence is consistently explicit builder call > environment variable > library default.Every property write issued a subscription-suspension read, even with no subscribers.
register_diffs()runs on each write and checked suspension before fetching subscriptions, so an actor with no subscriptions on the target paid a DynamoDBGetItemand the subscription query, only to do nothing either way. The subscription fetch now comes first and returns early when empty; suspension is consulted only when there is something to suspend — one fewer read per property write on the common path, no caching assumptions required. Behaviour is unchanged (suspended and no-subscriptions were already the same outcome). Measured on DynamoDB Local, a property write on an actor with no subscribers is now{GetItem: 1, PutItem: 1, Query: 1}— no suspension read, noDescribeTable, noScan.A failed subscription-suspension check was indistinguishable from “not suspended”.
Actor.is_subscription_suspended()logged any failure at DEBUG and returnedFalse, so a missing suspensions table (pynamodb raisesTableDoesNotExist, which is notDoesNotExist) or a denied read made every target read as un-suspended: a bulk import’ssuspend()silently no-opped and per-item callbacks fired for every imported item — exactly what suspension exists to prevent — with no error anywhere. Operational failures are now logged at ERROR, naming the actor, the target and the consequence. The call still degrades to “not suspended”; it is no longer silent about it. (The PostgreSQL accessor already logged these at ERROR.) The ERROR is rate-limited to once per five minutes per process —register_diffs()calls this on every property write, so an unconditional ERROR would flood at write rate, while a once-per-process guard would report the fault a single time on a warm serverless container and then stay silent through every later failure, including a different one. Throttled failures are logged at DEBUG.Suspending a target suppressed nothing.
suspend_subscriptions("properties")— the documented bulk-import usage — had no effect on property writes.PropertyStoreregisters every diff with the property name as the subtarget, so the check looked up"properties:<name>"whilesuspend()had stored"properties", and the two never met. Only an exactly matching(target, subtarget)pair ever suspended anything. Target-level suspension now cascades to every subtarget beneath it, on both backends.This is the functional half of the same symptom the missing-table fix above addresses: a bulk import’s
suspend()no-opping and per-item callbacks firing for every item. Restoring the table alone would not have fixed it.Verified on DynamoDB Local: a property write under a suspended target now issues
{GetItem: 1, PutItem: 1, Query: 2}against{GetItem: 3, PutItem: 3, Query: 3}un-suspended — the diff row and sequence bump are genuinely skipped. Previously the two were identical, because suspension did nothing.Behaviour change. Code that suspended a target while expecting individual subtargets to keep notifying will now see them suppressed. The reverse case is unaffected:
suspend(target, subtarget)still suspends only that pair, and a subtarget suspension made while its target is already suspended is still recorded separately, so resuming the target does not silently lift it.Subscriptions created through the interface layer received no diffs for the lifetime of the actor instance.
Actor.create_subscription()did not invalidate the actor’s cached subscription list (subs_list); only the delete paths did, and only the protocol handler invalidated by hand after a create. So a subscription created viaActorInterface.subscriptionsor the authenticated-peer view was invisible toregister_diffs()afterwards — a silently dropped notification with no error anywhere.create_subscription()now invalidates it itself, so the invariant no longer depends on every caller remembering. Note this refreshes only the instance it is called on: a subscription created in another process, or against a different cachedActor, still leaves that instance’s list stale until it is rebuilt — tracked inthoughts/todo/subs-list-cache-asymmetry.md.Config-bound singletons no longer serve one application’s state to another.
get_actingweb_oauth2_server(),get_mcp_client_registry(),get_actingweb_token_manager(),get_oauth2_state_manager(),get_permission_evaluator(),get_registry()(trust types),get_trust_permission_store(),get_peer_permission_store(),get_peer_profile_store()andget_cached_capabilities_store()each take aconfigargument but, once built, ignored it — they bound to the first config the process ever passed and returned that instance to every later caller. (One of them documented this explicitly: “config parameter kept for interface consistency but not used”.) Any process hosting more than one ActingWeb application therefore had the second application silently using the first’s configuration, including its database backend. The observed consequence: MCP dynamic client registration wrote a trust row to one backend while trust resolution read another, so the client’s trust was never found. Under the old fail-open behavior that granted the client full access; under the fail-closed behavior above it returns-32003. Each getter now rebuilds when handed a differentConfiginstance, and still returns the cached instance for the same one. Single-application deployments — the overwhelming majority — are unaffected either way.Note that
ActingWebOAuth2Servercomposes three of these, so rebinding the server alone was not sufficient:client_registry,token_managerandstate_managerhad to rebind too.get_oauth2_state_manager()re-reads its encryption key from the new config’s system actor on rebind; the key is stored rather than generated, so it is stable.MCP: an explicit ``isError`` is now honoured on the legacy text-wrap path. A hook returning
{"isError": True, "error": "boom"}with nocontentkey previously reached the wire with noisErrorfield at all, so the failure was reported to the client as a success.isErroris honoured only when the hook sets it — it is never inferred from an"error"key or any other shape heuristic, so applications that return{"error": ...}on their normal error path see no change. Thestr(result)text serialization is unchanged, soisErrorappears both inside that text and as a wire field.``list.index()`` semantics for negative ``start``/``stop``.
index(value, -1)could previously return-1as an index (the loop ranrange(-1, n)and then indexed negatively). Both storage formats now normalize bounds exactly aslist.indexdoes, through one shared helper so they cannot diverge across a migration.``pop()`` and ``remove()`` now delete conditionally on the value they read. Resolving the item’s storage key once was not enough: a concurrent assignment to that same key between the read and the delete would discard the other writer’s value while reporting the one this caller saw — an outcome corresponding to no serial ordering of the two operations. Both now use the new
DbPropertyProtocol.delete_if_value_equals()and re-resolve on a failed condition, sopop()always returns exactly what it removed andremove()always removes exactly what it matched.__delitem__/__setitem__remain unconditional by design.pop()on a v2 list no longer raisespop from empty listagainst a list another writer has appended to (the empty check consulted a cached length before the v2 path could refresh it).``ListProperty.insert()`` destroyed data on every call into a non-empty list on DynamoDB.
insert()reused one cachedDbPropertyhandle across its whole shift loop instead of taking a fresh handle per get()/set() like every other list method; on DynamoDB this caused each shifted row to be overwritten with the last value read instead of its own. Fixed both at the call site (fresh handles, matching the rest of the class) and in the DynamoDB and PostgreSQL backends’DbProperty.get()/set()(a cached handle for a different(actor_id, name)is now always discarded rather than reused).Backend read/write failures on property (and property-list item) storage were silently swallowed as absence or as a no-op.
DbProperty.get()now raisesactingweb.db.exceptions.DbErroron a genuine backend fault instead of returningNone(Nonemeans the row does not exist, and only that, on both backends). EveryListPropertymutation (append,__setitem__,__delitem__,insert,clear,delete, metadata writes) now checksset()’s return value and raisesRuntimeErrorinstead of continuing past a failed write. This is a breaking change for any code that relied on a backend fault degrading toNone/no-op on a property read/write.Backend exception text no longer reaches HTTP error responses from the list-property handlers (
PUT/POST/DELETE on/properties/<name>and/properties/<name>/items) — the client sees a generic message; the original exception is still logged server-side.Unparsable list metadata no longer self-heals into a fresh empty list (which orphaned every existing item row with no way back to them) —
ListPropertynow raisesValueErrorinstead, and the row is left untouched for repair.
DOCUMENTATION
The migration guide is organised by where you are upgrading from, not by which release candidate added what.
docs/migration/v3.13.rsthad grown five sections titled “Behaviour change in rcN”, each opening with a note telling you to read every section between your version and the target. That works while the rcs are current and stops working the moment they are one release. It now opens with a Start here section giving two paths — from3.12.x(do all four pieces of work, in the stated order, because two have pre-upgrade steps) and from a release candidate (a table saying exactly what is left to do for each ofrc1throughrc6) — followed by what changed afterrc6for anyone tracking the pre-releases. The four work sections are named for what they cover rather than for the rc that introduced them, and the DynamoDB material is now nested under its own heading instead of sitting as a flat run of peers titled “Overview”.New page: Actor Deletion Semantics (
docs/reference/actor-deletion.rst). States the contract that previously had to be read out of the source: the exact deletion order and why the actor row goes last, thatget_by_id()keeps resolving throughout the wipe, that itsNonemeans “not found or read failed”, which of the two deletion hooks to use for what, and how to write the guard. Also documents that attribute and property writes do not validate that the actor exists — an unknownactor_idcreates rows nothing will clean up. That is by design (actor_idis a key prefix, not a foreign key, on both backends), but the API shape invites the opposite assumption: one consumer’s test fixture wrote 228 rows under 114 ids that were never actors, unnoticed for five months. Closes with the four non-obvious edge cases in classifying orphaned rows, each of which is a way to delete live data.The Apple Sign-In revocation example now says why
actor_deletedis the right hook there (/auth/revokeis one-way) and what to do instead for a provider that calls back.The required-table list is published (
docs/reference/database-backends.rst,Required DynamoDB tables) with hash/range keys and which tables are conditional.AWS_DB_AUTO_CREATE_TABLES=falsemakes this a precondition, and it previously existed only in source.<prefix>_subscription_suspensionsis called out specifically: its accessor had no auto-create guard before 3.13, so long-lived deployments frequently never created it.The pre-warm/IaC race is documented. Declaring the new tables in CloudFormation/Terraform in the same deploy as the library bump lets a cold start create
<prefix>_property_lookup_v2first, failing the stack withResourceInUseException. Sequence the deploys, or let auto-creation own the tables — not both.The rollback instruction is qualified. Legacy mode needs the
property-indexGSI on the properties table; a table created before that index existed has no rollback path for reverse lookup. Checkdescribe-tablebefore upgrading.Migration rehearsal against DynamoDB Local is documented end-to-end — table creation, fallback tiers, backfill, tripwire — so the backfill is not performed for the first time in production.
A recipe for proving the fixes landed, since both are invisible to functional tests: count DynamoDB operations via
botocore.client.BaseClient._make_api_call. Includes the trap that abefore-call.dynamodbsession handler counts nothing under pynamodb (get_session()returns a fresh session per call), which makesassert scans == 0pass vacuously.Validating this release requires a DynamoDB backend — a green suite on PostgreSQL exercises none of the changed code.
Auditing your own ``DescribeTable``/``CreateTable`` use is called out when recommending the flag: the IAM change is account-wide, not library-scoped.
Reverse lookup is not always the login path — apps resolving via
get_from_creator()are unaffected, which changes how urgent the backfill is. The doc now says to check rather than assume.Direct ``DbPropertyLookup().get()`` use bypasses the fallback tiers (v2-only), so operational scripts return
Noneduring the migration window while the app is healthy.get_actor_id_from_property()is named as the supported entry point.``describe-table``’s ``ItemCount`` refreshes only every ~6 hours, so a freshly backfilled lookup table still reports
ItemCount: 0. Verify withscan --select COUNT.Documented that ``compact()`` is not crash-safe in either storage format, and that an interrupted repair leaves damage repair will not itself fix. Only the v2 rank-rebalance window was documented in this release; the v1 path – the one every upgrading operator runs via
actingweb-verify-property-lists --repair– has the same shape. Survivors are written to their new positions before the tail rows are deleted, so an interruption leaves a copy at both, with the stored length unchanged: the list reads back with no error.verify()catches it through the adjacent-duplicate heuristic, but re-running--repairwill not remove the copy, because duplicates are preserved by design – including the one the interrupted repair created. Now covered in the migration guide’s repair step (where operators are told to run it), the property-lists guide, andcompact()’s docstring, with a regression test pinning the measured interruption states. No behaviour change.Corrected two stale statements about lazy migration, which is off by default in this release: the downgrade-ordering warning described a 50-item list as “by definition a lazy-migration candidate” (true only where an operator has raised the limit), and the bulk script’s own docstring opened by describing lazy migration as the normal path.
v3.12.0: July 9, 2026
ADDED
Built-in default web-UI templates. The library now ships a minimal set of templates (factory/sign-in page, actor dashboard, properties, trust, OAuth2 consent, email/verification pages), so
with_web_ui(True)renders a working web UI out of the box with no app-supplied templates. Application templates of the same name still take precedence (Flask: a template-only blueprint; FastAPI: the apptemplates_diris searched first). Both integrations also register a/loginroute that renders the sign-in page.
FIXED
Programmatically-enabled property lookup tables leaked stale reverse-lookup rows on bulk delete.
DbPropertyList.delete()andDbProperty.get_actor_id_from_property()constructed a freshConfig()internally to decide whether to maintain the property lookup table. A freshConfig()only reflects environment variables and defaults, so a lookup table enabled through the builder (with_indexed_properties()/with_legacy_property_index(enable=False), which set attributes on the app’sConfig— not env vars) read back as disabled inside these methods. As a result, deleting all of an actor’s properties (e.g. on actor deletion) skipped lookup-table cleanup and left stale entries that could resolve reverse lookups to a deleted actor; reverse lookup could likewise take the wrong code path.get_property_list()now injectsuse_lookup_table/indexed_propertiesintoDbPropertyList(matchingget_property()), and both methods use the injected settings instead of a throwawayConfig. Applies to both the DynamoDB and PostgreSQL backends. Env-var-based configuration is unchanged (constructors still fall back to the environment).DynamoDB bulk property delete could remove another actor’s reverse-lookup row. The DynamoDB property lookup table is keyed on
(property_name, value), so when two actors share the same indexed value the row points at whichever actor wrote last.DbPropertyList.delete()deleted the lookup row for each of the deleted actor’s indexed values unconditionally, which could wipe out a different actor’s still-valid reverse-lookup entry. The bulk cleanup now verifies the row belongs to the actor being deleted before removing it, matching the single-property delete path. PostgreSQL was unaffected (its cleanup already scopes the delete byactor_id).``with_mcp(server_name=…, instructions=…, enable=…)`` were silently ignored.
ActingWebApp.__init__builds theConfigeagerly (permission warmup), and the runtime config-sync did not re-apply the MCP fields, so builder-set MCP options never reachedConfig.config.mcpis now a first-class attribute, kept in sync (including the advertised/metacapability tag).``@mcp_tool`` metadata no longer appears blank in ``GET /<id>/actions``. Stacking the required
@app.action_hook("name")decorator over@mcp_toolattached empty explicit metadata that shadowed the MCP metadata. Hook metadata resolution now merges per field: explicit non-empty values win, otherwise the@mcp_toolmetadata (then auto-generated schemas) is used.Built-in factory templates now honor a URL path prefix. The sign-in form action and the post-creation “Go to dashboard”/”Back to start” links in the bundled factory templates hardcoded root-relative
/URLs, so a deployment mounted under a prefix (config.rootlikehttps://host/base/) posted to and linked at the domain root instead of the mounted app. The factory handler now passes abase_path(derived fromconfig.root) into the template context and the templates prepend it.
DOCUMENTATION
Rewrote
README.rstto reflect the current framework (AI/MCP + web/SPA + native-mobile clients over one backend).Corrected a range of API-reference inaccuracies verified against the code:
TrustManager/SubscriptionManager/PropertyStoremethod names and signatures,AwProxyusage,ActorInterfaceattributes, authenticated views, and theActorInterface.create(hooks=app.hooks)requirement for lifecycle hooks to fire. Stopped recommendingactor.is_owner()(a placeholder that always returnsTrue) as an access guard.Fixed quickstart friction found by a cold-build usability pass: DynamoDB Local prerequisites, the
migrate_db.pydownload URL (masterbranch), MCP endpoint auth (no dev bypass), scalar-property stringification,templates_dirbeing optional, and Basic-auth vs OAuth for/www.
v3.11.0: July 4, 2026
3.11.0 rolls up everything developed since v3.10.1 (previously published as the
v3.10.2bN and v3.11.0bN pre-release lines). The theme is authentication
breadth and SPA/mobile session hardening: Sign in with Apple, GitHub and native
Google sign-in, native-mobile OAuth code/ticket exchange, a substantial hardening
of the SPA refresh-token rotation flow, and a round of MCP protocol improvements.
It also removes the vestigial optional MCP Python SDK dependency.
Almost all of 3.11.0 is additive and backward compatible. The items that need
action when upgrading from 3.10 are collected in
docs/migration/v3.11.rst — in short: PostgreSQL users run one new Alembic
migration; DynamoDB users confirm native TTL is enabled; split-domain SPA
deployments set spa_redirect_origins; SPA/mobile clients review the
refresh-token contract. The notes below describe the net change from v3.10.1;
fixes to functionality introduced within this same pre-release window are folded
into the feature that introduced them rather than listed separately.
ADDED
Native mobile and multi-provider sign-in:
Sign in with Apple support across web SPA, native iOS, Android Capacitor, and the LLM-triggered (MCP) OAuth web form. New
app.with_apple_sign_in(...)builder configures Apple as a first-class OAuth provider (Services ID + Team ID + Key ID +.p8private key). Apple’s ES256client_secretJWT is minted on demand and cached per 5-minute bucket; theid_tokenis validated against Apple’s JWKS (no userinfo endpoint). The.p8key is supplied viaprivate_key_path/APPLE_PRIVATE_KEY_PATH(file wins) orprivate_key_pem/APPLE_PRIVATE_KEY_PEMand is validated eagerly at config-build time. Seedocs/guides/apple-sign-in.rst.New ``app.with_github(…)`` builder mirroring
with_apple_sign_in/with_google_native: fills in GitHub’s endpoints and, withmobile_redirect_uri, registers agithub-mobileprovider that uses the server-side ticket flow (GitHub issues no OIDCid_token, so it cannot use the JWT-bearer grant).New ``app.with_google_native(…)`` builder for native Google sign-in via the JWT-bearer grant (accepts explicit
audiencesor derives them from the per-platform client IDs).Native mobile OAuth code exchange: the
authorization_codegrant onPOST /oauth/spa/tokenlets native mobile apps exchange an OAuth code received via deep link for ActingWeb SPA tokens (RFC 8252).exchange_code_for_token()accepts an optionalredirect_urioverride so provider classes honor custom URL schemes, and the SPA authorize endpoint validates the requested provider via_is_known_provider(). Provider-name variants such asgoogle-mobile/github-mobileare resolved by prefix matching increate_oauth2_authenticator.New JWT-bearer grant on ``POST /oauth/spa/token`` (
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, RFC 7523): native apps exchange a providerid_token(assertion) plusnoncefor an ActingWeb session. The validator is dispatched by the declaredproviderand the tokenissmust match it; single-use replay protection is enforced.New ``mobile_ticket`` grant (generalized from the original Apple-only
apple_mobile_ticket, which remains as an alias): any native-mobile provider whose authorization response lands on the HTTPS callback is handed an opaque single-use deep-link ticket and the code is exchanged server-side — no IdP code and no ActingWeb token ever rides the deep link. Used by Apple-on-Android and GitHub mobile. Redemption is atomic and race-free single-use (a conditional delete reports success to exactly one caller, closing the concurrent-replay window), and the 300 s TTL is enforced at redemption.Apple nonce hashing handled by the library. Apple’s native flow puts
SHA256(nonce)(hex) in the id_token’snonceclaim while Google echoes it verbatim. The Apple id_token validator accepts either the raw nonce or its SHA-256, so apps pass the same raw nonce to the JWT-bearer grant for every provider instead of pre-hashing per provider. Google validation stays a strict verbatim match.New POST ``/oauth/callback/apple`` endpoint for Apple’s
response_mode=form_postcallback, protected by a server-side single-use state nonce (CSRF-safe without relying on SameSite cookies).Normalized ``user_info`` shape on the ``oauth_success`` hook (
display_name/given_name/family_name/email/subplus passthrough) across all providers. Apple’s first-sign-inuserpayload (name) is merged before the hook fires. GitHub carries the profile name inname(falling back tologinwhen unset), normalized consistently across the web-login, SPA-via-callback and token-exchange paths.actor.store.oauth_provideris now written on every sign-in (create and existing-actor paths), so account-deletion / revocation logic can rely on it./oauth/configprovider entries gain additiveresponse_mode(form_postfor Apple,queryotherwise) andplatformfields.
SPA session hardening:
``ActingWebApp.with_spa_redirect_origins(*origins)`` builder — a fluent way to allow additional SPA redirect origins for split-domain deployments (previously only settable via
Config.spa_redirect_origins). Feeds the/oauth/spa/authorizeredirect_uri allowlist.``ActingWebApp.with_spa_cors_origins(*origins)`` builder — restrict the CORS
Access-Control-Allow-Originfor the SPA OAuth endpoints (default"*").spa_cors_originsis now a first-classConfigattribute, and the CORS handler treats an empty list as"*".Bounded retention for used SPA refresh tokens + an efficient expiry purge. Two changes keep the (single, shared) SPA token bucket from growing without bound — previously a used refresh token was retained for the full two-week TTL and nothing ever deleted it:
On rotation, a consumed refresh token’s storage TTL is shortened from two weeks to a bounded reuse-detection window (
SPA_REFRESH_TOKEN_REUSE_WINDOW, default 2 days). After the window a used token is purged and a later replay reads as an expired token (rejected) rather than triggering chain revocation — an acceptable, bounded detection horizon that shrinks steady-state token volume ~7×.Self-contained purge — no cron/Lambda required. The
/oauth/spa/tokenendpoint opportunistically calls the newOAuth2SessionManager.maybe_purge_expired_tokens(), which runs at most once perSPA_TOKEN_PURGE_INTERVAL(1 hour) per process via a process-local throttle. The underlyingpurge_expired_tokens()/ backendDbAttribute.delete_expired(now_epoch=None, buckets=None)issues a single set-basedDELETEon PostgreSQL backed by the existingidx_attributes_ttlpartial index (O(expired rows), not O(all tokens)). On DynamoDB the purge is a no-op: cleanup relies on the table’s native TTL onttl_timestamp, which must be enabled once per environment (seedocs/reference/database-backends.rst→ “Expired Token Cleanup (TTL)”).
Indexed refresh-token family revocation (PostgreSQL).
revoke_token_chaindelegates to a new backendDbAttribute.delete_by_chain(actor_id, buckets, chain_id). On PostgreSQL this is a singleDELETEbacked by a new partial expression indexidx_attributes_chain_idon(data ->> 'chain_id')— O(chain) regardless of how large the shared token partition grows (requires the new Alembic migration ``d4e5f6a7b8c9``; runalembic upgrade head). On DynamoDB it remains a bounded scan of the two token buckets (no GSI on the JSON-embeddedchain_id); a GSI on a promoted top-levelchain_idattribute is the documented optimization path for very large DynamoDB deployments.
MCP:
MCP protocol version negotiation: the
/mcphandler negotiates the protocol version duringinitializeinstead of hardcoding2024-11-05. It echoes the client’s requestedprotocolVersionwhen supported, otherwise returns the server’s latest supported version (maintained inactingweb/mcp/protocol.py, currently through2025-11-25). TheMCP-Protocol-Versionrequest header is honored on post-initialize requests (defaulting to2025-03-26when absent,400when present-but-unsupported), and GET discovery reports the full supported-version set. Backward compatible:2024-11-05-only clients still negotiate2024-11-05. The newactingweb/mcp/protocol.pymodule exposes the version constants andnegotiate_protocol_version/is_supported_protocol_version/supports_structured_contenthelpers as a single source of truth (also used by the OAuth2 discovery endpoint).Structured tool output (``structuredContent``):
tools/callresults populate the specstructuredContentfield when the negotiated protocol version supports it (>=2025-06-18). A hook returning a dict withcontentplus extra top-level keys has those extras promoted intostructuredContent; an explicitstructuredContentfrom the hook is passed through, and a hook-supplied_metais preserved. For older negotiated versionsstructuredContentis omitted (the payload is still carried bycontent).Warning
The promotion described above was removed in v3.13.0rc4.
structuredContentis now emitted only when a hook sets that key explicitly; extra top-level keys are no longer promoted. See thev3.13.0rc4entry anddocs/migration/v3.13.rst.MCP tool per-actor visibility:
@mcp_toolaccepts avisibility_predicate(actor) -> boolto omit tools fromtools/listfor actors that should not see them (fail-closed on predicate errors). Note: visibility filtering applies totools/listonly — omitted tools can still be invoked by name, so call-time enforcement must be implemented separately for any tool that gates privileged behavior.MCP tool per-actor descriptions:
@mcp_toolaccepts adescription_predicate(actor) -> str | Noneto override the tool description per actor, taking precedence overclient_descriptionsand the staticdescription.Configurable MCP server name:
ActingWebApp.with_mcp(server_name="myapp")sets the name announced in the MCPinitializehandshake and surfaced onserverInfo.name(some clients use it as the default tool prefix,myapp:searchvsactingweb:search).Configurable MCP server instructions:
ActingWebApp.with_mcp(instructions="...")sets the server-level orientation string emitted on theInitializeResult.instructionsfield — useful for pointing new LLMs at an entry-point tool (e.g.how_to_use()).serverInfo.versionreports the ActingWeb version.Per-MCP-session identity on ``MCPContext``: new optional
transport_session_idandclient_infofields expose per-session identity distinct frompeer_id/trust_relationship(which are per-OAuth2- credential and shared across concurrent sessions on the same credential).transport_session_idis taken from the spec’sMcp-Session-Idheader and isNonewhen the transport supplies none;client_infocarries the liveclientInfocaptured at the session’sinitializecall.get_client_info_from_context()prefers this live per-sessionclient_infoso two MCP sessions sharing one OAuth2 credential no longer see each other’s identity. Backward compatible: both fields default toNone.
CHANGED
PyJWT[crypto]is now a core dependency (required for Apple’s ES256client_secretand RS256id_tokenvalidation).FastAPI minimum raised to ``>=0.112`` (the
fastapiextra). The FastAPI integration uses the Starlette 1.0 request-firstTemplateResponsesignature (adopted in 3.10.1), which requires Starlette>=1.0/ FastAPI>=0.112; the dependency floor now matches, soactingweb[fastapi]can no longer resolve an incompatible older FastAPI/Starlette that raisedunhashable type: 'dict'.OAuth2Authenticatorwas refactored to a strategy pattern: provider-specific behavior now lives onOAuth2Providersubclasses (GoogleOAuth2Provider/GitHubOAuth2Provider/AppleOAuth2Provider). Public method signatures and theactingweb.oauth2.requestspatch point are preserved — fully backward compatible.Logout no longer revokes the upstream identity-provider grant. On
/oauth/logout(and the/oauth/spa/*session-token revoke), the handler now clears the stored provider token locally (nullingoauth_token,oauth_token_expiryandoauth_token_timestamp, leaving only theoauth_provideridentity metadata) instead of calling the provider’s token-revocation endpoint. The old behavior (added in 3.10.0) was acceptable for Google but wrong for Apple: hitting Apple’s revocation endpoint emails the user and severs the Sign in with Apple grant entirely, re-prompting for consent on the next login. Logout is a session action, not an account disconnect; provider-side revocation is now reserved for an explicit account-disconnect / delete flow. This supersedes the 3.10.0 “OAuth provider token revoked on logout” behavior.The MCP OAuth2 server now resolves authenticators on demand (
_get_authenticator(provider)) instead of pre-instantiating Google/GitHub, so Apple is offered in the LLM-triggered authorization web form.Token-exchange, token-refresh, token-revocation, and userinfo error logs now redact
client_secret/assertion/id_token/client_assertionand truncate the response body.MCP ``tools/call`` results include ``isError`` (defaulting to
false) per the MCP spec when the hook returns a dict containing ``content``, where previously such a hook returning{"content": [...]}withoutisErrorwas passed through verbatim. Clients doing strict equality on the result object may observe the added field. This does not apply to the legacy text-wrap path (a hook returning a dict with nocontentkey, or a bare value), which emits noisErrorat all; see thev3.13.0rc4entry, which makes that path honour an explicitly setisError.MCP server name no longer includes ``actor_id``: the announced server name defaults to
"actingweb"instead of"actingweb-{actor_id}". Each MCP connection is already per-actor, so disambiguation in the name was unnecessary and made client-side tool prefixes noisy.
FIXED
SPA/mobile refresh-token reuse detection now revokes only the offending token family, not every token for the actor. Refresh tokens carry a
chain_ididentifying their rotation family: rotation propagates the parent’schain_idwhile each fresh login starts a new chain, and reuse-after-grace now callsOAuth2SessionManager.revoke_token_chain(actor_id, chain_id)to revoke only that lineage (RFC 6819 token-family revocation), including the chain’s access tokens. Previously reuse calledrevoke_all_tokens(actor_id), so a single stale token from one device logged the user out everywhere (and clients that did not degrade the resulting401to a login screen went blank). The actor’s other devices/sessions keep working. Legacy refresh tokens minted beforechain_idexisted fall back to revoking only the presented token.The refresh-token grace window now issues a full rotation instead of an access-token-only response, fixing a delayed-lockout trap. Reuse of an already-used refresh token within the extended grace window now issues a full rotation (new access + new refresh token in the same
chain_id), so a client that dropped a prior rotation (e.g. a mobile WebView suspended before persisting the rotated token) recovers instead of being locked out one access-token lifetime later. Any later reuse across the divergent branches is caught by the chain-scoped reuse detection above.``OAuth2SessionManager.revoke_all_tokens()`` no longer crashes with
RuntimeError: dictionary changed size during iterationwhen an actor has a matching token to revoke: both loops now snapshotlist(<bucket>.items())before deleting.Cancelled SPA consent no longer renders a backend error page. When a user cancels consent during an SPA login, the provider redirects back with
error=access_deniedand no authorization code. The OAuth callback now detectsspa_modefrom the OAuthstateand bounces the error back to the SPA’s callback URL aserror/error_descriptionquery params (validated withis_safe_spa_redirect, falling back to the configured root), so the app can show a friendly message instead ofaw-root-failed.html/ a 500. Apple’sresponse_mode=form_postcallback gets the same treatment.Actor/trust deletion no longer recurses infinitely and exhausts the DB connection pool.
Trust.delete()now deletes the local trust record first and performs OAuth2 client cleanup afterwards, so the delete-client cascade (delete_client -> delete_relationship -> delete_reciprocal_trust -> Trust.delete) finds nothing to re-discover and terminates. Previously the cascade recursed untilmaximum recursion depth exceeded, and on PostgreSQL each recursion level leaked a pooled connection, self-deadlocking the pool.OAuth2 client deletion (and trust deletion) no longer aborts when the best-effort global client index lookup misses: when the caller supplies the authoritative
actor_id(as trust deletion does), the client is deleted from that actor’s bucket regardless, preventing orphaned-but-listable clients.MCP ``tools/list`` now surfaces ``title`` and ``outputSchema``:
@mcp_toolhas long acceptedtitleandoutput_schema, but the/mcptools/listbuilder dropped both when constructing the tool definition. Hosts that key on the MCP 2025-11-25Tool.title(e.g. Claude Code’s tool-permission dialog) therefore saw only the internalname, and clients supporting structured output had no schema to validate against.tool_defnow includestitleandoutputSchemawhen set on the decorator.MCP ``tools/call`` formatting parity between Flask and FastAPI: the sync (
MCPHandler) and async (AsyncMCPHandler) handlers now share a singleformat_call_tool_resultimplementation, so both frameworks format tool-call responses identically.A leftover debug log in the actor-by-creator lookup (
db.postgresql.actoranddb.dynamodb.actor) that was emitted atWARNINGon every match — a hot path during OAuth sign-in — is nowDEBUGwith a clearer message.
REMOVED
Dropped the optional ``mcp`` (MCP Python SDK) dependency. ActingWeb’s MCP support is fully hand-rolled and did not functionally depend on the SDK (the SDK-backed
ActingWebMCPServerwas never wired to serve requests). Removing it also drops ~a dozen transitive packages (the SSE/streaming + jsonschema + pydantic-settings stack). Themcpinstall extra is removed; MCP support no longer requires any extra.Removed ``actingweb.mcp.get_server_manager``, ``MCPServerManager`` and ``ActingWebMCPServer`` (the vestigial SDK-backed server). The supported way to configure the MCP server name and instructions is
ActingWebApp.with_mcp(server_name=..., instructions=...). The internal_match_uri_templatehelper moved toactingweb/mcp/uri.pyasmatch_uri_template.
SECURITY
SPA OAuth open-redirect / session-token leak fixed. The
redirect_uripassed toPOST /oauth/spa/authorize(where the browser is later redirected with a one-time?session=id) is now validated against an allowlist: the backend’s own FQDN, the origins of configured OAuth redirect URIs / Apple mobile deep links, and any origins listed in the newConfig.spa_redirect_origins(for split-domain SPA deployments). An off-originredirect_uriis rejected with400at authorize time, and the callback falls back to the backend root rather than honoring an unsafe target. Previously an attacker who induced a victim to start an authorize flow with an attacker-controlledredirect_uricould receive the victim’s one-time session id and exchange it for tokens. This affected all SPA providers (Google / GitHub / Apple).Raised the minimum versions of security-sensitive dependencies past known vulnerable releases.
PyJWT >= 2.13(was^2.9) clears CVE-2026-48523 — an algorithm-allowlist bypass in thePyJWKClient/ JWKS decode path, which is exactly the flow the new nativeid_tokenvalidation (Apple / Google) relies on.cryptography >= 48.0.1(was>= 43.0),requests >= 2.32.4(was>= 2.20; clears the proxy-Authorizationand.netrccredential leaks and theverify=Falsepersistence bug), andoauthlib >= 3.2.2(was>= 3.2; clears theredirect_uriDoS CVE-2022-36087). These are lower-bound raises only — they do not cap any dependency’s upper version.
v3.10.1: Apr 2, 2026
FIXED
Starlette 1.0 compatibility: Update all
TemplateResponsecalls infastapi_integration.pyto use the new Starlette 1.0 signatureTemplateResponse(request, name, context=...)instead of the deprecatedTemplateResponse(name, {"request": request, ...})convention. This fixes anunhashable type: 'dict'error when rendering OAuth authorization forms with Starlette >= 1.0.0. Requires Starlette >= 1.0.0 (included with FastAPI >= 0.112.0).
v3.10.0: Mar 21, 2026
BREAKING CHANGES
Library Bucket Naming Convention: All library-internal attribute buckets now use
_prefix to avoid namespace collisions with user-defined buckets. Renamed:trust_types→_trust_types,trust_permissions→_trust_permissions,peer_profiles→_peer_profiles,peer_capabilities→_peer_capabilities, and all OAuth index buckets. Existing transient data (tokens, sessions) will naturally be recreated; trust types and permissions may need explicit migration.``google_token_data`` parameter renamed:
TokenManager.create_authorization_code()parameter renamed toprovider_token_datato reflect multi-provider OAuth support.
ADDED
Multi-Provider OAuth Support: Multiple OAuth providers (e.g., Google and GitHub) can now be configured simultaneously using
.with_oauth(provider="google", ...).with_oauth(provider="github", ...). The login page renders buttons for all configured providers, and/oauth/configreturns all providers. Fully backward compatible — existing single-provider configurations work without modification.SPA Email Collection Flow: When an OAuth provider cannot supply a verified email, the SPA OAuth callback now redirects back to the SPA with
?email_required=true&session=<id>so SPAs can display their own email input UI.Email Verification via ``/oauth/email?verify=<token>``: New GET handler provides actor-ID-free email verification URLs. The
email_verification_requiredhook now fires consistently for both SPA and HTML template flows. The legacy/{actor_id}/www/verify_email?token=<token>URL remains functional for backward compatibility.Provider Display Name Helper: New
get_provider_display_name()public function in theoauth2module for consistent provider name formatting (e.g., “GitHub”).Peer Profile Caching: Cache profile attributes (displayname, email, etc.) from trusted peer actors to eliminate repeated API calls. Configure with
ActingWebApp.with_peer_profile(attributes=[...]). NewTrustManagermethods:get_peer_profile(),refresh_peer_profile(),refresh_peer_profile_async().Peer Capabilities Caching: Cache methods and actions that peer actors expose. Configure with
ActingWebApp.with_peer_capabilities(enable=True). NewTrustManagermethods:get_peer_capabilities(),get_peer_methods(),get_peer_actions(),refresh_peer_capabilities(),refresh_peer_capabilities_async().Peer Permissions Caching: Cache permissions that peer actors have granted us (distinct from
TrustPermissions, which stores what we grant to peers). Configure withActingWebApp.with_peer_permissions(enable=True). Includes access-checking methods:has_property_access(),has_method_access(),has_tool_access().Permission Query Endpoint: New
GET /{actor_id}/permissions/{peer_id}endpoint allows peers to query the permissions they have been granted, including effective trust type defaults.Automatic Peer Notification on Permission Change: When permissions are updated, the peer is automatically notified via a POST to their
/callbacks/permissions/{actor_id}endpoint. Configure withActingWebApp.with_peer_permissions(notify_peer_on_change=True)(default:True).Auto-Delete Cached Peer Data on Permission Revocation: Optionally delete cached peer data when permissions are revoked. Enable with
ActingWebApp.with_peer_permissions(auto_delete_on_revocation=True).Automatic Subscription Processing: New
CallbackProcessor,RemotePeerStore, andFanOutManagermodules handle incoming subscription callbacks with automatic sequence validation, gap detection, resync triggering, and back-pressure support.Pull-Based Subscription Sync: New
SubscriptionManager.sync_subscription()andsync_peer()methods (and async variants) for explicitly fetching and processing pending diffs from peers. Complements push-based callbacks for “Sync All” workflows.Subscription Suspension: Subscriptions can be suspended on repeated delivery failures and resumed later, with automatic resync on resume. New
SubscriptionSuspensiondatabase table in both DynamoDB and PostgreSQL backends.Revoke Peer Subscription: New
SubscriptionManager.revoke_peer_subscription(peer_id, subscription_id)for semantically clear deletion of inbound subscriptions (peer’s subscription to our data). Notifies the peer and removes the local record.Subscription Deleted Lifecycle Hook: New
subscription_deletedlifecycle event triggered when an inbound subscription is deleted, receivingactor,peer_id,subscription_id,subscription_data, andinitiated_by_peerflag.Inbound Subscription Query: New
SubscriptionManager.get_subscriptions_from_peer(peer_id)for querying inbound subscriptions (peers subscribed to our data). Complements existingget_subscriptions_to_peer().AsyncMCPHandler for FastAPI: MCP tools and prompts with async hooks now execute natively in the FastAPI event loop, without thread pool overhead. FastAPI automatically uses
AsyncMCPHandler; Flask continues using the syncMCPHandler.Database Accessor Pattern: New factory functions in
actingweb.db(get_property(),get_actor(),get_trust(), etc.) create database instances with configuration automatically injected. New protocol definitions inactingweb.db.protocolsprovide full type safety and IDE support.Attribute List Storage: New
ListAttributeandAttributeListStoreclasses for storing distributed lists in attribute buckets (not exposed via REST API). Same semantics asListProperty/PropertyListStorebut stored in attributes, bypassing the 400 KB property size limit.List Metadata Access: New
get_metadata()method on bothListPropertyandListAttributeexposes internal metadata (created_at,updated_at,version,item_type,chunk_size,length).Property/List Name Collision Detection: Creating a property when a list already exists with the same name (or vice versa) now raises
ValueError, preventing ambiguity and data loss.Configurable AwProxy Timeout: New
timeoutparameter on theAwProxyconstructor. Accepts a single value or a(connect_timeout, read_timeout)tuple. Default changed to(5, 20)seconds.Request Correlation in Logging: Every log line now includes
[request_id:actor_id:peer_id]context for distributed tracing. New public API:enable_request_context_filter(),set_request_context(),get_request_id(),get_actor_id(),get_peer_id(). Request IDs are extracted from theX-Request-IDheader or auto-generated, and included in response headers.Inter-Actor Request Correlation: Correlation headers (
X-Request-ID,X-Parent-Request-ID) are automatically added to all peer-to-peer HTTP calls for complete request chain tracing.Lambda Environment Detection: Automatic detection of AWS Lambda deployments with a warning when async subscription callbacks are enabled, recommending
with_sync_callbacks()to prevent callback loss on function freeze.Configurable FastAPI Thread Pool: New
ActingWebApp.with_thread_pool_workers(workers)method for tuning the thread pool size (1–100 workers, default 10).Passphrase-to-SPA-Token Exchange:
POST /oauth/spa/tokennow acceptsgrant_type="passphrase"to exchange a valid creator passphrase for SPA tokens. Devtest mode only (returns 403 in production). Useful for Playwright and automated testing tools.Revoked Trust Detection: During subscription sync, if a peer has revoked a trust relationship (all subscriptions returning 404), the system automatically detects this and either cleans up dead subscriptions (if trust still exists) or removes the local trust entirely, triggering the
trust_deletedlifecycle hook.Baseline Sync on Subscription Creation:
subscribe_to_peer()now performs an immediate baseline data fetch after creating the subscription, ensuring consistent initial state regardless of whether the peer has existing data or pending diffs. New async variant:subscribe_to_peer_async().Peer Metadata Refresh on Subscribe: Subscription creation automatically refreshes cached peer profile, capabilities, and permissions metadata (when those features are configured), eliminating the need for a separate sync cycle.
RemotePeerStore Enumeration: New
list_all_scalars()andget_all_properties()methods onRemotePeerStorefor enumerating stored peer data.get_all_properties()returns a combined view of all lists and scalars with type metadata (type,value,item_count).List Property Format Parameter:
GETrequests on list properties now accept?format=shortto retrieve only metadata (count, description, explanation) without fetching all items.Permission Callbacks: Incoming permission change notifications from peers are handled automatically and stored in
PeerPermissionStore. Use@app.callback_hook("permissions")to receive them in application code. Callbacks are delivered to/callbacks/permissions/{granting_actor_id}.Permission Protocol Option Tags: When
with_peer_permissions(enable=True)is configured,/meta/actingweb/supportedautomatically advertises thepermissioncallbackandpermissionquerycapability tags per ActingWeb Protocol Specification v1.4.
CHANGED
Loosen dependency version constraints: Runtime dependencies now use more permissive version ranges (e.g.,
boto3 >=1.26,requests >=2.20,cryptography >=43.0) to reduce version conflicts for downstream consumers. Optional framework dependencies (Flask, FastAPI, uvicorn) also loosened.OAuth provider token revoked on logout: Logout now revokes the stored OAuth provider token from
actor.storein addition to the ActingWeb session token. Providers without a revocation endpoint (e.g., GitHub) are silently skipped.List properties in ``GET /properties``: List properties are now included even without
?metadata=true, returned as{"_list": true, "count": N}. The full format (with description and explanation) is returned with?metadata=true.``GET /subscriptions?peerid=X`` returns all subscriptions: Now returns both inbound and outbound subscriptions for the given peer, rather than only outbound.
Email verification URL format: Verification links now use
/oauth/email?verify=<token>as the preferred format instead of/{actor_id}/www/verify_email?token=<token>. The legacy URL remains functional for backward compatibility, but new integrations should use the new actor-ID-free form.``get_github_verified_emails()`` made public: Renamed from
_get_github_verified_emails()onOAuth2Authenticator(private prefix removed), as it is called across class boundaries.Permission format normalization: Both shorthand list format (
["pattern1", "pattern2"]) and spec-compliant dict format ({"patterns": [...], "operations": [...]}) are now accepted and normalized consistently across all permission APIs (TrustPermissionsstorage,PeerPermissionscallbacks,AccessControlConfig.add_trust_type()). Shorthand format defaults to read-only operations.
SECURITY
GitHub Email Verification:
_get_github_primary_email()now requires bothprimaryandverifiedflags when selecting the email for actor linking. Previously, an unverified primary email was accepted, which could allow account-linking attacks via the GitHub/user/emailsAPI.Logging Security Hardening: Comprehensive audit of all log statements to prevent sensitive data leakage: OAuth tokens masked (first 8 characters only), OAuth request bodies and HTTP Authorization headers removed from logs, property values and HTTP response bodies no longer logged.
Remote Peer Data Sanitization: All data received from remote peers is sanitized to prevent JSON encoding failures from malformed Unicode (invalid UTF-16 surrogate pairs, invalid UTF-8 sequences).
FIXED
PostgreSQL Properties Value Index: Dropped the
idx_properties_valueB-tree index on theproperties.valuecolumn, which blocked storage of values larger than ~2700 bytes (e.g., embeddings, JSON blobs). Theproperty_lookuptable handles reverse-index lookups for properties that require value-based search. Includes Alembic migrationc3d4e5f6a7b8to drop the index on existing databases.MCP OAuth Flow Verified Email Requirement: The MCP OAuth flow now returns a clear
invalid_granterror when no verified email is available from the provider.List property subscription diff callbacks: Fixed internal
list:prefix leakage in subscription diff callbacks. Subscribers now receivesubtarget="myList"instead ofsubtarget="list:myList"; applications that were stripping this prefix can remove that workaround.Fix FastAPI double logout invocation: The FastAPI
/oauth/logouthandler was calling the underlying logout handler twice when a Bearer token was present alongside anoauth_tokencookie, causing redundant token revocation attempts against the OAuth provider.
IMPROVED
Parallel Test Isolation: Significantly improved pytest-xdist parallel test execution reliability (flakiness reduced from ~5% to <1%) via worker-namespaced OAuth2 registration and improved database state cleanup. All 42 xdist groups are now documented in
tests/integration/XDIST_GROUPS.md.Structured Proxy Error Responses: All
AwProxyresource methods (get_resource(),create_resource(),change_resource(),delete_resource(), and async variants) now return structured error dicts withcodeandmessagekeys for all error conditions, including when the peer returns a string-typederrorfield. Previously, the HTTP status code was lost and replaced with a hardcoded 500.
v3.9.2: Jan 16, 2026
ADDED
Synchronous Subscription Callbacks: Added
with_sync_callbacks()builder method andsync_subscription_callbacksconfig option for Lambda/serverless environments. When enabled, subscription callbacks use blocking HTTP requests instead of async fire-and-forget, ensuring callbacks complete before the request handler returns. This prevents callbacks from being lost when Lambda functions freeze after returning a response.New builder method:
ActingWebApp.with_sync_callbacks(enable=True)New config attribute:
Config.sync_subscription_callbacks(default:False)Refactored
Actor.callback_subscription()to use shared sync helper functionImproved logging with sequence numbers and peer IDs for callback debugging
Subscription Sequence in GET Response: Added
sequencefield to GET subscription response (/subscriptions/<peerid>/<subid>). The subscription’s current sequence number is now included at the top level of the response, allowing peers to detect gaps in received diffs without examining individual diff sequence numbers. Updated ActingWeb Specification to version 1.4.
FIXED
Network Exception Handling in Trust Creation: Fixed
get_peer_info()to catch network-related exceptions (ConnectionError,Timeout, etc.) that were previously uncaught. This prevents HTTP 500 errors during trust relationship creation when the peer is temporarily unavailable or slow to respond. The function now returns a proper 500 status code instead of crashing.
IMPROVED
Retry Logic for Peer Communication: Added automatic retry with exponential backoff to
get_peer_info(). Network requests now retry up to 3 times with delays of 0.5s, 1s, and 2s on transient network failures. This significantly improves reliability when peers are briefly unavailable or slow to respond.Test Fixture Reliability: Improved test server startup detection with faster polling (0.5s vs 1s) and added warmup requests after servers are detected as ready. This helps prevent race conditions in parallel test execution where the first real request might hit before internal initialization is complete.
v3.9.1: Jan 15, 2026
FIXED
Async Hooks in Sync Execution Methods: Fixed
execute_lifecycle_hooks(),execute_callback_hooks(),execute_property_hooks(),execute_subscription_hooks(), andexecute_app_callback_hooks()to properly execute async hooks when called from synchronous contexts. Previously, async hooks registered for these hook types would return unawaited coroutines instead of executing. Now all sync execution methods use_execute_hook_in_sync_context()to correctly handle both sync and async hooks viaasyncio.run()fallback.
v3.9.0: Jan 15, 2026
ADDED
Property Lookup Tables: Added dedicated lookup tables for property reverse lookups (
get_actor_id_from_property()), removing DynamoDB GSI 2048-byte size limit. Supports unlimited property value sizes for indexed properties in both DynamoDB and PostgreSQL backends.Configurable indexed properties via
with_indexed_properties()(default:oauthId,email,externalUserId)Dual-mode operation: new lookup table or legacy GSI/index
Backward compatible: defaults to legacy mode (
use_lookup_table=false)Environment variables:
USE_PROPERTY_LOOKUP_TABLE,INDEXED_PROPERTIESAutomatic cleanup: lookup entries deleted with properties/actors
PostgreSQL foreign key CASCADE for automatic orphan cleanup
actingweb.db.dynamodb.property_lookupmodule withPropertyLookupmodel andDbPropertyLookupclassactingweb.db.postgresql.property_lookupmodule withDbPropertyLookupclassactingweb.interface.ActingWebApp.with_indexed_properties()builder method for configurationactingweb.interface.ActingWebApp.with_legacy_property_index()builder method to control modePostgreSQL migration
70d60420526_add_property_lookup_table.pyfor lookup table schemaComprehensive test suite (
tests/test_property_lookup.py) with 26 tests for both backendsDocumentation in
docs/quickstart/configuration.rstwith migration guide and best practicesNative Async/Await Hook Support: ActingWeb hooks now support both synchronous and asynchronous (async/await) function definitions with automatic detection
New async execution methods:
execute_method_hooks_async(),execute_action_hooks_async(),execute_property_hooks_async(),execute_callback_hooks_async(),execute_app_callback_hooks_async(),execute_subscription_hooks_async(), andexecute_lifecycle_hooks_async()Async handler variants:
AsyncMethodsHandlerandAsyncActionsHandlerwith*_async()method variants (get_async(),post_async(),put_async(),delete_async())FastAPI integration automatically detects and uses async handlers for optimal performance without thread pool overhead
Backward compatible: Existing synchronous hooks continue to work without changes
Mixed support: Applications can use both sync and async hooks in the same application
Sync context support: Async hooks are executed via
asyncio.run()when called from synchronous contexts (Flask)Use
async deffor hooks that need to call async services (AWS Bedrock, async HTTP clients, async database operations, AwProxy async methods)
CHANGED
DbProperty.get_actor_id_from_property()now uses lookup table when configured, falling back to legacy GSI/indexDbProperty.set()now syncs lookup entries for indexed propertiesDbProperty.delete()now removes lookup entries for indexed propertiesDbPropertyList.delete()now cleans up all lookup entries when deleting actor propertiesFastAPI integration now preferentially uses async handler variants (
AsyncMethodsHandler,AsyncActionsHandler) for methods and actions endpointsSynchronous hook execution methods (
execute_*_hooks()) now support async hooks viaasyncio.run()fallbackHandler factory (
get_handler_class()) now supports creating async handler variants based on framework preference
v3.8.3: Jan 12, 2026
FIXED
Fixed Flask integration
TypeErrorin cookie handling by extracting cookie name as positional argument instead of kwargFixed missing subscription callbacks when deleting properties via WWW handler with
?_method=DELETEFixed trust relationship timestamps to always include timezone info in ISO format strings (both DynamoDB and PostgreSQL)
Fixed property list metadata to avoid auto-saving default metadata on first access
ADDED
Rich Metadata for Methods/Actions:
GET /<actor_id>/methodsandGET /<actor_id>/actionsnow return metadata (description, input/output schemas, annotations) with auto-generation from TypedDict type hints
CHANGED
Enhanced OAuth2 refresh token reuse handling with three-tier grace period (0-10s: full rotation, 10-60s: access token only, >60s: revoke all)
v3.8.2: Jan 3, 2026
FIXED
Trust Deletion Error Handling: Enhanced DELETE handler to return 404 when the remote actor doesn’t exist (not just when relationship doesn’t exist), enabling complete cleanup of orphaned trust relationships during
delete_reciprocal_trust()flow when the remote actor has been deletedOAuth Refresh Token Race Condition: Fixed race condition in refresh token rotation that could cause false token theft detection and forced re-login when concurrent requests use the same refresh token. The check-and-mark-as-used operation is now atomic using database-level compare-and-swap, preventing multiple requests from successfully using the same token
ADDED
Atomic Attribute Updates: Added
conditional_update_attr()method to both DynamoDB and PostgreSQL backends for atomic compare-and-swap operations, enabling race-free token rotation and other concurrent update scenariosAtomic Token Marking: Added
try_mark_refresh_token_used()method inOAuth2SessionManagerthat atomically checks and marks refresh tokens as used in a single database operation
v3.8.1: Jan 2, 2026
FIXED
Subscription Cache Invalidation: Fixed subscription handler to invalidate
subs_listcache after creating new subscription, ensuringregister_diffs()immediately sees newly created subscriptions for callback deliveryTrust Deletion Error Handling: Fixed DELETE handler for trust relationships to return 404 (instead of 403) when relationship doesn’t exist, enabling proper cleanup of orphaned trust relationships during
delete_reciprocal_trust()flowPostgreSQL Backend: Fixed SQL queries to quote
desccolumn as reserved keyword (PostgreSQL compatibility)PostgreSQL Backend: Fixed
Attributesclass to handle None values from PostgreSQL for non-existent attribute bucketsDatabase Backend Abstraction: Removed hardcoded DynamoDB imports in
TrustManagerandPermissionEvaluatorto use configured database backend dynamicallyTest Fixtures: Fixed
test_trust_manager_oauthmock to properly structureDbTrustmodule for compatibility with backend abstraction
ADDED
Migration Helper: Added
scripts/migrate_db.pyhelper script for simplified PostgreSQL migrations with automatic .env loading and environment validation
CHANGED
Documentation: Enhanced PostgreSQL setup documentation in quickstart guides with migration helper script usage, troubleshooting guide, and step-by-step setup instructions
TODO: Added task for implementing
TrustManager.create_relationship_async()method to avoid blocking event loop in async contexts
v3.8.0: Dec 31, 2025
CHANGED
Database Package Structure: Refactored
actingweb.db_dynamodbto hierarchical package structureactingweb.db.dynamodbfor better organizationInstallation Extras: Added optional dependency groups -
pip install 'actingweb[postgresql]'or'actingweb[dynamodb]'for backend-specific installationsDatabase Backend Selection: Environment variable
DATABASE_BACKEND(ordatabaseparameter inActingWebApp()) now supports"dynamodb"(default) or"postgresql"Documentation Overhaul: Comprehensive updates across all user-facing documentation: - Updated quickstart guides to include PostgreSQL setup instructions - Enhanced configuration reference with backend comparison tables - Expanded database maintenance guide to cover both DynamoDB TTL and PostgreSQL pg_cron cleanup - Added backend selection guidance throughout documentation
Logging Architecture: Implemented hierarchical logging with named loggers throughout codebase using
__name__patternLogging Configuration: Added centralized logging configuration with
configure_actingweb_logging()helper functionsLog Levels: Rebalanced log levels - significant operations (actor creation, trust deletion, etc.) now use INFO instead of DEBUG
ADDED
PostgreSQL Database Backend: Full PostgreSQL support as an alternative to DynamoDB
actingweb.db.postgresqlpackage with all 7 database tables (Actor, Property, Trust, PeerTrustee, Subscription, SubscriptionDiff, Attribute)PostgreSQL connection pooling via psycopg3 with configurable pool sizes
Alembic migrations for PostgreSQL schema management (
actingweb/db/postgresql/migrations/)Database backend protocols (
actingweb.db.protocols) for interface consistency across backendsProtocol compliance tests to ensure both backends implement the same interface
scripts/migrate_dynamodb_to_postgresql.py- Data migration tool with export, import, and validate operationsPerformance benchmarks (
tests/performance/) for comparing backend performanceComprehensive PostgreSQL documentation: -
docs/guides/postgresql-migration.md- Complete migration guide from DynamoDB to PostgreSQL -docs/reference/database-backends.rst- Detailed backend comparison, cost analysis, and recommendationsGitHub Actions matrix testing for both DynamoDB and PostgreSQL backends
Backend-specific pytest markers (
@pytest.mark.dynamodb,@pytest.mark.postgresql)actingweb.logging_configmodule with production/development/testing configuration helpersPerformance-critical logger identification for production optimization
Lazy log evaluation in hot paths for improved performance
ActorInterface.config property: Direct access to ActingWeb configuration object from
ActorInterfaceinstancesTrust Lifecycle Hooks: Added
trust_initiatedhook - fires when actor initiates trust request to peer (outgoing)Trust Lifecycle Hooks: Added
trust_request_receivedhook - fires when actor receives trust request from peer (incoming)Trust Lifecycle Hooks: Added
trust_fully_approved_localhook - fires when THIS actor approves, completing mutual trustTrust Lifecycle Hooks: Added
trust_fully_approved_remotehook - fires when PEER actor approves, completing mutual trust
FIXED
Database module import paths corrected from relative to absolute imports
v3.7.6: Dec 30, 2025
FIXED
Subscription Filtering: Fixed
get_subscriptions()callback parameter to properly filter by callback flag -callback=Nonenow returns all subscriptions,callback=Falsereturns inbound subscriptions,callback=Truereturns outbound subscriptionsTrust Deletion Hook: Enhanced
trust_deletedlifecycle hook to includerelationshipandtrust_dataparameters for consistency withtrust_approvedhook
CHANGED
Added documentation to
get_subscriptions()method explaining parameter filtering behavior
v3.7.5: Dec 27, 2025
ADDED
Actor Root Content Negotiation:
GET /<actor_id>now supports content negotiation - API clients receive JSON, browsers are redirected based on authentication status andwith_web_ui()configuration.Browser Redirect to /login: Unauthenticated browser requests to
/<actor_id>now redirect to/loginfor a consistent login experience instead of triggering OAuth directly.SPA Redirect Support: When
with_web_ui(False), authenticated browsers and OAuth callbacks redirect to/<actor_id>/appinstead of/<actor_id>/www.Integration tests for actor root endpoint content negotiation and redirect behavior.
CHANGED
OAuth2 callback handler now respects
config.uisetting - redirects to/<actor_id>/appwhen web UI is disabled (SPA mode).FastAPI and Flask integrations updated to redirect unauthenticated browser requests to
/login.Documentation extensively updated: routing-overview, web-ui guide, spa-authentication guide, and configuration reference now document browser redirect behavior.
v3.7.4: Dec 26, 2025
ADDED
DynamoDB TTL support for automatic cleanup of expired tokens, sessions, and auth codes.
v3.7.3: Dec 19, 2025
FIXED
WWW Callback Hook Template Rendering: The
wwwcallback hook can now render custom templates by returning{"template": "template-name.html", "data": {...}}. This allows applications to add custom web UI pages without modifying the core library.Callbacks Handler Response Data: GET, POST, and DELETE callback handlers now return the hook’s response data as JSON (200 OK) instead of just boolean success/failure, enabling richer callback interactions.
Methods/Actions ACL Rules: Added default ACL rules for
/methodsand/actionsendpoints for creator, friend, partner, and admin trust types.Added
template_nameattribute toAWResponsefor custom template rendering support.Fixed methods and actions handlers to use dual-context authentication (
_authenticate_dual_context) supporting both web UI (OAuth cookie) and API (basic auth) access.
ADDED
Added integration tests for custom www template rendering via callback hooks.
Added integration tests for OAuth2 logout SPA CORS behavior (origin echoing and credentials).
CHANGED
OAuth2 Logout Consolidation:
/oauth/spa/logoutnow delegates to the main/oauth/logouthandler for consistent behavior. The logout endpoint uses SPA CORS (echoed origin + credentials) to ensure cross-origin SPAs can clear session cookies. Both Flask and FastAPI integrations now properly propagate cookies from handler responses.Callbacks handler now returns actual hook results as JSON response body instead of just HTTP status codes.
Documentation updated with template rendering examples for www callback hooks.
v3.7.2: Dec 19, 2025
FIXED
Fixed Flask integration cookie handling: added
pathandsamesiteparameters for proper session cookie behavior across browser security policiesSimplified Flask OAuth session validation to use session manager directly instead of OAuth2 authenticator, fixing token validation issues
Improved Flask template rendering error logging for easier debugging
CHANGED
Added thoughts/shared/plans/2025-12-18-passphrase-login-feature.md as a way to support www login without a 3rd party auth provider
v3.7.1: Dec 18, 2025
FIXED
SECURITY: Permission override merging now uses union semantics for both
patternsANDexcluded_patternsarrays by default - base security exclusions (private/, security/, oauth_*) can no longer be accidentally cleared by individual trust relationship overridesCleaned up integration/unit tests to have all dynamodb-dependent tests in integration dir
v3.7.0: Dec 16, 2025
BREAKING CHANGES
Developer API Extended: SubscriptionManager and TrustManager have new methods with cleaner APIs and automatic lifecycle hooks
See
docs/migration/v3.7.rstfor comprehensive migration guideHTTP API remains 100% backward compatible - no changes required for applications using REST endpoints only
FIXED
SECURITY: Permission evaluator now returns DENIED (not NOT_FOUND) when explicit patterns are defined but target doesn’t match - fixes permission bypass via legacy ACL fallback
SECURITY: Properties
listallendpoint now filters properties and list properties based on peer permissions - prevents unauthorized data exposureProperties
listallnow includes list properties even when all regular properties are filtered by permissionsSubscription callbacks now fire asynchronously in async contexts to avoid blocking the caller
Fixed permission filtering for property lists to strip ‘list:’ prefix before permission checks
Property list diff notifications now include item data (item, index, items) for subscribers
Add trigger of oauth_success hook in SPA oauth2 login
Fixed unused variable and import warnings identified by ruff linting
Fixed
hasattr(x, '__call__')pattern replaced withcallable(x)for better type safety
ADDED
Permission Merge Control: Added
merge_baseparameter tomerge_permissions()function - defaults toTruefor fail-safe union merging of patterns/excluded_patterns; set toFalsefor explicit full override capabilityDeveloper API Extensions: Added methods to SubscriptionManager:
create_local_subscription(),get_subscription_with_diffs(),get_callback_subscription(),delete_callback_subscription()Developer API Extensions: Added methods to TrustManager:
create_verified_trust(),modify_and_notify(),delete_peer_trust(),trustee_rootpropertyWrapper Classes: Added
SubscriptionWithDiffswrapper providing clean access to subscription data and diffsAsync Authentication: Added async versions of authentication methods (
check_token_auth_async(),check_and_verify_auth_async()) to avoid blocking event loop during OAuth2 validationOAuth2 Token Heuristic: Added
Auth._looks_like_oauth2_token()method to avoid unnecessary network calls for non-OAuth tokensList Property Subscriptions: List property operations now trigger subscription notifications with structured diff payloads
Parallel Test Execution: Added pytest-xdist support with worker isolation (unique DB prefixes, ports, emails) for 3-4x faster test runs
Added Makefile targets:
make test-parallel,make test-parallel-fast,make test-all-parallelAdded
pytest-xdistdependency for parallel test executionGitHub Actions CI now runs tests in parallel with 4 workers
oauth_success hook now receives full OAuth user info
CHANGED
Permission Merge Documentation: Added “Permission Override Merging” section to
docs/guides/access-control.rstand updateddocs/reference/security.rstcheatsheetArchitecture: Handlers refactored to four-tier architecture (Handler → Developer API → Core Actor → Database) for clean separation of concerns
Handler Simplification: Handlers are now thin HTTP adapters delegating business logic to developer API
OAuth2 token validation now includes quick heuristic check before network requests for better performance
Integration tests now support parallel execution with automatic worker isolation
GitHub Actions workflow uses parallel testing (4 workers for public repos, 2 for private)
Added timeout-minutes to GitHub Actions jobs (20 min tests, 10 min type-check)
Documentation consolidated into CONTRIBUTING.rst and CLAUDE.md
v3.6.0: Dec 11, 2025
FIXED
Trust
trust_approvedlifecycle hook now triggers when receiving POST approval notification from peer (moved from PUT handler to POST handler)Fixed race condition in trust approval flow where trust relationship must be saved to database before notifying the peer.
Fixed missing deletion of permissions for trust relationship when deleting it.
Fixed missing
trust_deletedlifecycle hook trigger in trust DELETE handler.
ADDED
ACL Rules for Custom Trust Types:
add_trust_type()now accepts anacl_rulesparameter to specify HTTP endpoint access permissions. This enables custom trust types (likesubscriber) to access ActingWeb REST endpoints like/subscriptions/<id>for creating subscriptions. Each rule is a tuple of(path, methods, access).SECURITY: Subscription callbacks now respect property permissions - only properties the peer has
readpermission on are included in callbacks (fail-closed design)NEW ENDPOINT: Added
/trust/{relationship}/{peerid}/shared_propertiesendpoint for discovering properties available for subscriptionBREAKING: Subscription permission filtering is fail-closed - if permission evaluation fails, no data is sent to subscribers
Note on Subscription Access Control: Subscription creation is controlled by ACL rules (e.g., ("subscriptions/<id>", "POST", "a")), NOT by property permission patterns. Any peer with the subscription ACL can create subscriptions to any target. Property permissions only affect what data is included in subscription callbacks.
- Async HTTP Methods: Added async versions of AwProxy methods using httpx for non-blocking operations in async frameworks like FastAPI:
AwProxy.get_resource_async()- Async peer resource retrieval
AwProxy.create_resource_async()- Async peer resource creation
AwProxy.change_resource_async()- Async peer resource update
AwProxy.delete_resource_async()- Async peer resource deletion
Dependencies: Added
httpxas a new dependency for async HTTP client operations
CHANGED
Subscription handlers now use unified permission evaluator (
evaluate_property_access) instead of legacycheck_authorisationPermission changes made after subscription creation now affect subsequent callbacks (dynamic permission enforcement)
Actor
callback_subscriptionmethod now filters property subscription data based on peer permissions before sending
v3.5.6: Dec 4, 2025
FIXED
SECURITY: Fixed token revocation searching wrong actor bucket during trust deletion - tokens were stored in user’s actor but revocation looked in system actor (
_actingweb_oauth2), leaving tokens valid after trust deletion
v3.5.5: Dec 3, 2025
FIXED
Fixed Flask and FastAPI integrations not propagating handler headers (e.g., WWW-Authenticate) to OAuth2 endpoint responses
v3.5.4: Dec 3, 2025
FIXED
SECURITY: Trust relationship deletion now properly deletes the associated OAuth2 client and revokes all tokens - prevents deleted MCP clients from being reused after trust relationship is removed
v3.5.3: Dec 3, 2025
FIXED
SECURITY: MCP client deletion now immediately revokes all access and refresh tokens - prevents deleted clients from continuing to access resources using cached tokens
v3.5.2: Dec 3, 2025
FIXED
SECURITY: Fixed missing WWW-Authenticate header for 401 responses in OAuth2 token endpoint - RFC 6749 Section 5.2 requires WWW-Authenticate header for invalid_client errors
v3.5.1: Dec 1, 2025
FIXED
CRITICAL: Fixed OAuth2 actor creation not triggering lifecycle hooks -
config._hookswas never set, causingactor_createdand other lifecycle hooks to be silently ignored during OAuth-based actor creationAdded comprehensive regression tests for OAuth2 lifecycle hook integration
CHANGED
ActingWebApp now automatically attaches HookRegistry to Config object’s
_hooksattribute inget_config()
v3.5: Nov 30, 2025
ActingWeb Specification version 1.2
This release implements ActingWeb Specification version 1.2 with SPA-friendly API behavior.
FIXED
SECURITY: Fixed SPA PKCE code challenge not being sent to OAuth providers - PKCE parameters are now properly forwarded to authorization URLs
SECURITY: Fixed trust-based permissions bypass - handlers were using
getattr()on dict instead of.get()for peer ID lookup, causing permission evaluator to be bypassedFixed FastAPI cookie setting using
keyparameter instead ofname(FastAPI/Starlette API difference)Fixed SPA refresh token cookie not being stored by browser (changed
path="/"andsamesite="Lax")Fixed refresh token reuse false positives on rapid page refresh with 2-second grace period
Fixed
oauth_state.decode_state()to handle JSON null values for optional trust_type fieldRemoved sensitive token values from debug log messages (security improvement)
Fixed pytest marker registration for integration tests (added
integrationmarker)
CHANGED
SPA OAuth2 authorize endpoint only includes
trust_typein state when provided (distinguishes user login from MCP client auth)
BREAKING CHANGES
Empty Collection Response Behavior (Spec v1.2)
The following endpoints now return 200 OK with empty arrays/objects instead of 404 Not Found
when collections are empty. This is a breaking change for clients that rely on 404 to detect
empty collections:
GET /trust- Returns200 OKwith[]when no trust relationships exist (was404)GET /trust?relationship=<type>- Returns200 OKwith[]when no matches (was404)GET /properties- Returns200 OKwith{}when no properties exist (was404)GET /subscriptions- Returns200 OKwith{"id": ..., "data": []}when no subscriptions (was404)
Migration Guide:
Before (v1.1):
response = requests.get(f"{actor_url}/trust", auth=auth)
if response.status_code == 404:
trusts = [] # No trusts
else:
trusts = response.json()
After (v1.2):
response = requests.get(f"{actor_url}/trust", auth=auth)
trusts = response.json() # Always returns array (may be empty)
Note: Individual resource lookups still return 404 Not Found when the specific resource
does not exist (e.g., GET /properties/nonexistent, GET /trust/friend/nonexistent-peer).
DOCS
Spec v1.2: Added “Response Conventions” section documenting SPA-friendly empty collection behavior
Spec v1.2: Updated
/properties,/trust, and list properties sections with new 200 OK behaviorAdded
listpropertiesoption tag to ActingWeb specification for list property supportAdded comprehensive List Properties section to
docs/actingweb-spec.rstdocumenting ordered collectionsDocumented list property CRUD operations (GET/POST/PUT/DELETE) for items and full lists
Documented list property metadata endpoint (GET/PUT
/properties/{name}/metadata)Updated GET
/properties?metadata=truedocumentation to reference listproperties option tag
ADDED
SPA mode support in OAuth2 callback handler (
spa_mode=truein state parameter returns JSON instead of redirect)JSON API responses in email verification handler (based on
Accept: application/jsonheader)GET /{actor_id}/meta/trusttypesendpoint for trust type enumerationGET/PUT /{actor_id}/properties/{name}/metadataendpoint for list property metadataFactory JSON API:
GET /?format=jsonorAccept: application/jsonreturns OAuth configuration for SPAsNew test suites for SPA API endpoints (
tests/test_spa_api_endpoints.py,tests/integration/test_spa_api.py)
CHANGED
Trust Type Configuration Clarification
Trust types are now correctly scoped to MCP client authorization flows only:
Trust types are no longer exposed in user login endpoints (
GET /?format=json,/oauth/spa/*)Library no longer hardcodes default trust types (e.g.,
mcp_client)Applications must configure trust types for MCP OAuth2 flows via
AccessControlConfig.configure_oauth2_trust_types()/oauth/authorize(MCP authorization) shows trust types from registry;/oauth/spa/authorize(user login) does not
This change clarifies the distinction between:
User Login (ActingWeb as OAuth client to Google/GitHub): No trust relationship created, no trust_type needed
MCP Authorization (ActingWeb as OAuth server for MCP clients): Trust relationship created with specified trust_type
Security Enhancement
Added explicit validation in OAuth2 callback to prevent actor spoofing (validates OAuth email matches actor creator)
MCP OAuth2 flows derive actor_id from authenticated email, not user input
FIXED
Fixed unit test configuration to use correct DynamoDB port (8001) matching docker-compose.test.yml
Fixed Trust class attribute initialization to prevent AttributeError on early returns
v3.4.3: Nov 23, 2025
FIXED
Fixed MCP authentication to include error=”invalid_token” in WWW-Authenticate header per RFC 6750 to force OAuth2 clients to invalidate cached tokens
Fixed Flask and FastAPI integrations to properly propagate HTTP 401 status codes and WWW-Authenticate headers from MCP handler
Reduced excessive debug logging in MCP trust relationship lookup
Added conditional update check to prevent unnecessary trust relationship updates when client info hasn’t changed
v3.4.2: Nov 22, 2025
ADDED
Github action to push new package to pypi on merging PRs to master branch
FIXED
Fixed wrong URL in OAUTH2 discovery URL that prevented detection of dynamic client registration
CHANGED
Reduced unnecessary error logging for access and authentication
Ruff linting and formatting and pyright type fixes
v3.4.1: Nov 8, 2025
FIXED
MCP Tool Annotations
Fixed tool annotations not being serialized in
tools/listresponses in MCP handlerTool annotations (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint) are now properly included when decorators define themThis fix enables ChatGPT and other MCP clients to properly evaluate tool safety before execution
Aligns FastAPI/HTTP integration path with SDK server behavior
OAuth2 Trust Type Selection
Fixed OAuth2 authorization form ignoring user-selected trust type during Google/GitHub authentication
OAuth provider buttons now submit form to
/oauth/authorizePOST with selected trust_type before redirecting to providerTrust type is properly embedded in encrypted OAuth state and applied during callback
Fixes regression introduced when OAuth provider integration was added (previously worked with email form submission)
v3.4: Oct 26, 2025
ADDED
OAuth2 Security Enhancements
Added email verification system for OAuth2 actors when provider cannot verify email ownership
Added cross-actor authorization prevention in MCP OAuth2 flows (blocks attackers from authorizing access to other users’ actors)
Added session fixation prevention in web OAuth2 login flows
Added comprehensive security documentation in
docs/authentication-system.rstAdded security tests in
tests/integration/test_oauth2_security.pyAdded lifecycle hooks:
email_verification_requiredandemail_verifiedAdded email verification endpoint:
/<actor_id>/www/verify_emailAdded verified emails dropdown for GitHub OAuth2 (fetches verified emails via GitHub API)
Added provider ID support (stable identifiers like
google:suborgithub:user_id) as alternative to email addressesAdded 32-byte cryptographic verification tokens with 24-hour expiry
Added security logging for all authorization violations
Trust Relationship Enhancements
Added
last_accessedandlast_connected_viafields to trust relationships for tracking connection activityHandler now updates trust relationship timestamps on each connection with client metadata refresh
Trust relationship sorting by most recent connection in web UI (
/www/trust)
FIXED
Security Fixes
CRITICAL: Fixed OAuth2 callback to validate actor ownership before completing authorization (prevents cross-actor authorization attacks in MCP flows)
Fixed potential account hijacking vulnerability when OAuth2 providers don’t return verified email addresses
Fixed session fixation vulnerability where attackers could trick users into logging into attacker’s actor
OAuth2 and Integration Fixes
Fixed OAuth2 CORS preflight (OPTIONS) requests not being routed correctly in FastAPI integration, causing 404 errors on
/oauth/registerand/oauth/tokenendpoints
CHANGED
Code Quality & Type Safety
Type Checking: Achieved zero pyright errors and warnings across entire codebase (down from 430+ issues)
Linting: Achieved zero ruff errors (fixed all 15 remaining linting issues)
Configuration: Added pyright to dev dependencies and created
pyrightconfig.jsonfor consistent type checkingVSCode Integration: Updated
.vscode/settings.jsonfor optimal pylance and ruff integrationTest Improvements: Fixed pytest fixture names causing test failures, all 474 tests now passing
Type Annotations: Added comprehensive type ignore comments for test files (228+ annotations)
Import Management: Fixed unused imports, variables, and function warnings throughout codebase
Module Declarations: Fixed lazy-loaded module
__all__declarations with proper pyright ignores (34 modules)Dependencies: Updated Poetry to 2.2.1 and major package upgrades including
cryptography45.0.6 → 46.0.3,fastapi0.116.1 → 0.120.0,mcp1.12.4 → 1.19.0,pydantic2.11.7 → 2.12.3,pytest-cov6.2.1 → 7.0.0,ruff0.12.8 → 0.14.2, and 30+ other dependency updates
v3.3: Oct 4, 2025
BREAKING CHANGES
Legacy OAuth System Removed
Removed legacy
OAuthclass and related third-party service authenticationRemoved
/<actor_id>/oauthendpoints that used legacy OAuthRemoved legacy OAuth methods from
Authclass (oauth_get,oauth_post, etc.)Legacy OAuth auth type no longer supported in
select_auth_type()
Migration Path: Use the new unified third-party service integration system instead:
Replace
oauth.OAuth()withactor.services.get("service_name")Replace manual OAuth configuration with
app.add_dropbox(),app.add_gmail(), etc.Use clean service API:
service.get(),service.post(), etc.
FIXED
MCP tools/list response now applies client-specific formatting for better compatibility
Trust relationship descriptions now show friendly client names instead of raw identifiers when available
OAuth2 client trust relationships maintain proper client metadata across updates
Trust manager now keeps peer identifier in sync for OAuth2/MCP clients
Fixed FastAPI extra inadvertently importing Flask, forcing Flask as a dependency
Fixed trust handler to return empty list instead of 404 for graceful handling
Reduced logging noise by moving non-essential INFO logs to DEBUG
Made
actor.get_config()use the dynamic globalactingweb.__version__Fixed error in
DbPropertyListwhen properties table was missing in DynamoDBFixed
trustee_rootJSON to return stored value instead of input parameterFixed missing
trustee_rootin actor creation via REST APIFixed handling of
POSTto/<actor_id>/www/properties(including_method=DELETE)Fixed base path handling for
/<actor_id>/www(supports non-root base paths consistently)Fixed
www/hook not triggeredDevtest proxy: added Basic-auth fallback (
trustee:<peer passphrase>) when Bearer trust requests to peer/propertiesendpoints receive 302/401/403, avoiding OAuth2 redirects during testingFixed MCP OAuth2 trust relationship creation where MCP clients completed authentication but no trust relationships were created
Fixed hook permission operation mapping where “get” operations were incorrectly passed as “read”
Fixed OAuth2 callback
established_viato properly distinguish between MCP and regular OAuth2 flowsFixed singleton initialization dependency order causing Permission Evaluator to fail when Trust Permission Store was not yet initialized
Fixed OAuth2 trust-type filtering logic that was incorrectly placed inside exception handlers, causing 0 available trust types during OAuth2 flows
Fixed severe OAuth2 performance issue where lazy singleton loading during requests caused 4+ minute hangs during OAuth2 callbacks
Fixed
established_viafield being lost between database save and retrieval in trust relationship managementAdded explicit singleton initialization at application startup to prevent performance degradation during OAuth2 flows
Replaced all urlfetch HTTP calls with requests library for improved reliability, better timeout handling, and elimination of 30+ second timeout issues
CHANGED
If
unique_creator=False, ensure deterministic retrieval of the first actor available when doing OAuth2 auth and log in from rootRefactored OAuth2 server implementation to use Attributes system instead of underscore-prefixed properties for storing sensitive data (tokens, authorization codes, Google OAuth2 tokens)
Removed unused default resources in the MCP server (now only existing resources and hooks are presented)
Removed notes and usage as static resource in the library, leave this to the implementing application
Cleaned up the actor creation interfaces, ActorInterface.create() is now the only factory to be used.
Standardized global Attribute buckets for cross-actor data.
Enhanced
/trust/{relationship}/{peerid}API endpoints to support permission management alongside traditional trust relationship operationsModified
/meta/actingweb/supportedto dynamically include feature tags based on available system capabilitiesProperties, methods, and actions handlers now integrate with unified permission system while maintaining backward compatibility
Hook execution system now includes transparent permission checking with authentication context passing
Dependencies: Replaced
urlfetch ^2.0.1withrequests ^2.31.0for more reliable HTTP operations
ADDED
Integration Test Suite
Added comprehensive REST API integration test suite with 117 tests covering all mandatory ActingWeb protocol endpoints
Added
tests/integration/directory with test harness, fixtures, and test filesAdded Docker Compose configuration for local DynamoDB testing (
docker-compose.test.yml)Added GitHub Actions CI/CD workflow (
.github/workflows/integration-tests.yml) for automated testing on PRsAdded
make test-integrationtarget for running integration tests locallyAdded comprehensive testing documentation (
docs/TESTING.md)Test coverage: actor lifecycle, properties (nested/complex), meta, trust relationships, subscriptions with diffs
MCP Client Management Enhancements
Support for
allowed_clientsparameter in@mcp_tooldecorator to restrict tool access by client typeSupport for
client_descriptionsparameter in@mcp_tooldecorator for client-specific tool descriptionsClient-specific tool filtering for MCP endpoints based on client type detection (ChatGPT, Claude, Cursor, etc.)
Enhanced OAuth2 client trust relationship display with friendly client names in web UI
Automatic enrichment of OAuth2 trust relationships with missing client metadata
Unified Third-Party Service Integration
Added modern service integration system replacing legacy OAuth class
Added fluent API methods:
app.add_dropbox(),app.add_gmail(),app.add_github(),app.add_box()Added
ServiceConfig,ServiceClient, andServiceRegistryclassesAdded automatic token management and refresh for third-party services
Added
actor.services.get()interface for accessing authenticated service clientsAdded service OAuth2 callback endpoints:
/{actor_id}/services/{service_name}/callbackAdded service revocation endpoints:
DELETE /{actor_id}/services/{service_name}Added comprehensive documentation in
docs/service-integration.rstIntegrated service system with both Flask and FastAPI frameworks
Bot Handler Improvements
Fixed broken bot handler that tried to use removed legacy OAuth system
Simplified bot authentication to use direct bot token validation from config
Removed dependency on Auth class for bot endpoints - bots now use simpler token-based validation
Bot token now passed to hooks for service calls if needed
Simplified Authentication Interface
Added
require_authenticated_actor()method to BaseHandler for one-line auth + authorizationAdded
authenticate_actor()method returningAuthResultfor more granular controlNew interface reduces boilerplate from 6-8 lines to 2-3 lines per handler method
Maintains full compatibility with existing
init_actingweb()usageAutomatic HTTP response handling for common authentication and authorization failures
Unified Access Control System
Complete unified access control system with trust types, permissions, and pattern matching
Trust Type Registry with 6 built-in trust types (associate, viewer, friend, partner, admin, mcp_client) and support for custom types
Permission Evaluator with glob pattern matching, precedence rules, and fallback to legacy authorization
Per-relationship permission storage system allowing individual trust relationships to override trust type defaults
Permission Integration module providing transparent permission checking for all ActingWeb operations
Enhanced Trust API with permission management endpoints:
GET /trust/{relationship}/{peerid}?permissions=true- Include permission overrides in trust responsePUT /trust/{relationship}/{peerid}- Update permissions alongside trust relationship propertiesGET /trust/{relationship}/{peerid}/permissions- Dedicated permission management endpointPUT /trust/{relationship}/{peerid}/permissions- Create/update permission overridesDELETE /trust/{relationship}/{peerid}/permissions- Remove permission overrides
trustpermissionsfeature tag automatically included in/meta/actingweb/supportedwhen permission system is availableTransparent hook permission checking - existing hooks automatically get permission filtering without code changes
Enhanced MCP OAuth2 trust relationship creation with automatic trust type detection
Zero-migration design - existing applications work immediately while gaining new capabilities
Comprehensive permission structure supporting properties, methods, actions, tools, resources, and prompts
Pattern-based permissions with support for glob wildcards (
*,?) and URI schemesBackward compatibility with legacy authorization system as fallback
Other Additions
Added execution of property_hooks in the handler of www/*
Added support for list of hidden properties as variable to www/properties* templates
Added support for dynamic generation of resources in MCP based on hooks
Support for CORS in oauth2 flows
PKCE support in oauth2 flows
Support for OPTIONS method on OAUTH2 discovery endpoints
New explicit interface for managing list properties with actor.property_lists.listname syntax
Distributed list storage bypassing DynamoDB 400KB item limits by storing individual list items as separate properties
Added property_lists attribute to Actor class for list-specific operations
Lazy-loading iterator for efficient list traversal without loading entire lists into memory
Added singleton warmup module (
actingweb.singleton_warmup) for explicit initialization of performance-critical singletons at application startupComprehensive documentation for singleton initialization requirements in both CLAUDE.md and unified-access-control.rst
Intelligent caching system for MCP endpoint authentication providing 50x performance improvement (50ms → 1ms) for repeated requests with 90%+ cache hit rates
MCP authentication caching includes token validation, actor loading, and trust relationship lookup with automatic TTL-based cleanup and performance monitoring
OAuth2 Client Management
High-level
OAuth2ClientManagerinterface for creating, listing, validating, deleting clients, and regenerating client secretsClient secret regeneration with verification, audit timestamp (
secret_regenerated_at), and formatted display valuesGenerate access tokens via client-credentials flow directly from
OAuth2ClientManager.generate_access_token()
OAuth2 Authorization Server
Added support for
client_credentialsgrant type with token issuance and discovery updated (grant_types_supported)Added
trust_typeandactor_idto client registration/discovery responses; improved secret validation diagnosticsAdded client deletion capability to MCP client registry
MCP Integration
Captures and caches MCP
clientInfoduring initialize; persists to trust relationship after OAuth2 callbackPopulates trust context on authenticated MCP sessions for permission evaluation
Added Google OAuth2 token validation via Google TokenInfo API
Enhanced MCP client information capture and persistent storage across session establishment
Improved MCP authentication with proper HTTP 401 handling and WWW-Authenticate headers for FastAPI integration
Added global client info caching during session establishment with automatic cleanup
Each MCP client now gets unique trust relationship per user email, preventing clients from overwriting each other’s identities
OAuth2 client registration now automatically creates trust relationships, ensuring proper permission evaluation
All OAuth2 clients must pass permission evaluation before accessing MCP endpoints
Runtime Context System
New
actingweb.runtime_contextmodule providing structured request context for hook functionsRuntimeContextclass with type-safe context classes:MCPContext,OAuth2Context,WebContextget_client_info_from_context()helper function for unified client detection across all context typesSupport for custom context types via
set_custom_context()andget_custom_context()methodsRequest-scoped context lifecycle with automatic cleanup support
Comprehensive documentation and examples for using runtime context in hook functions
Web UI Enhancements
Consistent template URL variables across pages:
actor_root,actor_www, andurlTrust page displays registered OAuth2 clients (name, trust type, created time, status)
Trust creation form supports selecting trust type; consistent
form_actionand redirectsProperty pages: create/delete list properties, edit list metadata (description/explanation), and improved redirects after operations
Auth Utilities
Added
check_and_verify_auth()helper to verify authentication for custom (non-ActingWeb) routes with redirect-aware responses
v3.2.1: Aug 9, 2025
OAuth2 Authentication System and Enhanced Integrations
ADDED
OAuth2 Implementation: - New oauth2.py module with comprehensive OAuth2 authentication using oauthlib WebApplicationClient - Support for Google and GitHub OAuth2 providers with automatic provider detection - OAuth2CallbackHandler for secure callback processing with state parameter validation - Email validation system to prevent identity confusion attacks - Login hint parameter support for Google OAuth2 to improve user experience - State parameter encryption with CSRF protection and email validation
MCP OAuth2 Authorization Server: - Complete RFC 7591/RFC 8414 compliant OAuth2 authorization server for MCP (Model Context Protocol) clients - Dynamic Client Registration (DCR) endpoint for MCP client registration - OAuth2 authorization and token endpoints with proper scope handling - Separate token management system for ActingWeb tokens vs Google tokens - Per-actor MCP client credential storage using ActingWeb attribute bucket pattern - State parameter encryption with MCP context preservation for OAuth2 flows - Global index buckets for efficient MCP client lookup across actors - Integration with existing Google OAuth2 for user authentication proxying
Enhanced Authentication Flow: - Modified factory endpoint behavior: GET shows email form, POST triggers OAuth2 with email hint - Email validation step to ensure authenticated email matches form input - User-friendly error templates for authentication failures - Security enhancement preventing form email != OAuth2 email mismatch attacks - Dual OAuth2 callback handling supporting both ActingWeb and MCP flows
FastAPI Integration Enhancements: - Improved FastAPI integration with better async/await handling - Enhanced template and static file support for FastAPI applications - Better separation of GET/POST handling in factory routes - Improved error handling and response formatting for FastAPI
Integration Improvements: - Enhanced both Flask and FastAPI integrations with OAuth2 callback handling - Improved factory route handling with separate GET/POST methods - Better template variable population for authentication forms - Enhanced error handling across both integrations
CHANGED
Authentication System: - Factory routes now handle GET and POST separately for better UX - Enhanced OAuth callback processing with comprehensive validation - Improved state parameter handling with encryption and validation - Better error messaging and user guidance for authentication failures
Integration Layer: - Updated both Flask and FastAPI integrations to support new OAuth2 flow - Enhanced template rendering with better context and error handling - Improved factory handler logic with cleaner separation of concerns - Better support for custom authentication flows in integrations
Dependency Management: - Updated all dependencies to latest stable versions - Major version updates: Flask ^2.0.0 → ^3.1.1, Werkzeug ^2.0.0 → ^3.1.3 - FastAPI ^0.100.0 → ^0.116.1, uvicorn ^0.23.1 → ^0.35.0 - Core dependencies: boto3 ^1.26.0 → ^1.40.6, urlfetch ^1.0.2 → ^2.0.1, cryptography ^41.0.0 → ^45.0.6 - Development tools: pytest ^7.0.0 → ^8.4.1, black ^22.0.0 → ^25.1.0, ruff ^0.1.0 → ^0.12.8 - Documentation: sphinx ^5.0.0 → ^8.2.3, sphinx-rtd-theme ^1.0.0 → ^3.0.2 - Restructured optional dependencies into independent extras: flask, fastapi, mcp, and all
FIXED
Type Safety: - Fixed all pylance/mypy type annotation errors in OAuth2 implementation - Enhanced type safety for OAuth2 classes and methods - Better null safety checks in authentication flows - Improved Union type handling for request bodies
Authentication Issues: - Fixed OAuth callback handling edge cases - Resolved state parameter validation issues - Fixed email validation logic for OAuth2 providers - Enhanced error handling in authentication flows
Handler Integration Issues: - Fixed critical auth.py bug where handler objects were incorrectly treated as response objects - Resolved AttributeError: ‘SubscriptionRootHandler’ object has no attribute ‘write’ - Resolved AttributeError: ‘SubscriptionRootHandler’ object has no attribute ‘headers’ - Updated auth.init_actingweb() to properly access appreq.response.write() and appreq.response.headers - Added defensive checks for response object availability in authentication flows
DynamoDB Storage Issues: - Fixed DynamoDB ValidationException for authorization codes exceeding 2KB index key size limit - Fixed DynamoDB ValidationException for access tokens exceeding size limits - Implemented individual property storage pattern for large data structures - Separated Google token data storage from index keys to prevent size limit violations - Added reference key pattern for efficient lookup of separated token data
SECURITY
OAuth2 Security Enhancements: - Implemented comprehensive email validation to prevent identity attacks - Added state parameter encryption for CSRF protection - Enhanced callback validation with multiple security checks - Improved error handling to prevent information leakage
MCP Authorization Server Security: - RFC 7591 compliant Dynamic Client Registration with proper client credential generation - Per-actor client isolation using ActingWeb security boundary model - State parameter encryption with MCP context preservation prevents CSRF attacks - Secure token separation between ActingWeb internal tokens and Google OAuth2 tokens - Proper scope validation and authorization code flow implementation - Client credential storage encrypted at rest using ActingWeb property system
v3.1: Jul 28, 2025
BREAKING CHANGES
Removed legacy OnAWBase interface completely
Removed actingweb.on_aw module and OnAWBase class
Removed ActingWebBridge compatibility layer from interface module
Handler constructors now accept hooks: HookRegistry instead of on_aw: OnAWBase
Applications must now use the modern ActingWebApp interface exclusively
ADDED
FastAPI integration with app.integrate_fastapi() method
FastAPI integration automatically generates OpenAPI/Swagger documentation
Synchronous ActingWeb handlers run in thread pools to prevent event loop blocking
Pydantic models for all ActingWeb endpoints with automatic validation
Support for modern @app.actor_factory decorator in FastAPI integration
CHANGED
All handlers now use HookRegistry directly instead of OnAWBase bridge pattern
Flask integration now uses HookRegistry directly
Fixed hook method call signatures in properties.py, resources.py, and www.py
Fixed path handling in property hooks to prevent index out of bounds errors
Standardized hook parameter order across all handlers
Fixed missing arguments in execute_property_hooks calls
Resolved callback hook return type issues with any() function usage
v3.0.1: (Jul 17, 2025)
BREAKING CHANGES
Minimum Python version is now 3.11+
Removed deprecated Google App Engine (GAE) database implementation
Removed migrate_2_5_0 migration flag and related migration code
Database backend now only supports DynamoDB
Removed Google App Engine urlfetch abstraction layer
Environment types updated to remove APPENGINE, added AWS
Separated application-level callbacks (@app.app_callback_hook) from actor-level callbacks (@app.callback_hook)
ADDED
Comprehensive type hints using Python 3.11+ union syntax (str | None)
Custom exception hierarchy: ActorError, ActorNotFoundError, InvalidActorDataError, PeerCommunicationError, TrustRelationshipError
Constants module with AuthType, HttpMethod, TrustRelationship, ResponseCode enums
Modern build system with pyproject.toml and Poetry for dependency management
Modern developer interface with ActingWebApp class and fluent API
Decorator-based hook system for property, callback, subscription, and lifecycle events
ActorInterface, PropertyStore, TrustManager, and SubscriptionManager wrappers
Flask integration with automatic route generation
/methods endpoint support with JSON-RPC 2.0 protocol compatibility
/actions endpoint support for trigger-based functionality
Method hooks (@app.method_hook) and action hooks (@app.action_hook)
Development tooling (black, ruff, mypy) and comprehensive test suite with pytest
Type checking support with py.typed marker
__version__ attribute to actingweb module
CHANGED
Modernized string formatting with f-strings
Simplified HTTP client code to use urlfetch library directly
Removed config.env == “appengine” environment checks
Updated default actor type from gae-demo to demo
Enhanced type safety with comprehensive None-checking patterns
Applied systematic None validation patterns to prevent runtime errors
Improved IDE support with better type inference and error detection
Complete documentation overhaul with modern interface examples
FIXED
Eliminated potential bugs from dual interface inconsistencies
Removed unnecessary abstraction layers improving request handling speed
Single code path reduces potential for interface synchronization issues
Better type checking with direct HookRegistry usage instead of generic OnAWBase
Zero Pylance diagnostics errors across entire codebase
Comprehensive None safety checks across all core modules
Fixed handler method signatures for proper positional argument passing
Enhanced HTTP request safety with proper urlfetch module validation
Fixed OAuth configuration access with proper None checks
Applied systematic None safety patterns across all HTTP methods
Refactored actor creation to reduce coupling between factory handler and bridge implementation
Fixed template variables not being populated for web form POST to /
QUALITY
Legacy OnAWBase interface completely removed for better maintainability
Applications using OnAWBase must migrate to ActingWebApp interface
95%+ reduction in complexity for handler logic
Clean separation of concerns with direct hook execution
Much simpler debugging without bridge layer abstraction
All tests continue to pass with new interface (30/30)
90% reduction in boilerplate code for new applications
Proper circular import handling with TYPE_CHECKING
Enhanced developer experience with self-documenting type hints
MIGRATION GUIDE
For existing applications using OnAWBase:
Before (Legacy - NO LONGER SUPPORTED):
class MyApp(OnAWBase):
def get_properties(self, path, data):
return data
def post_callbacks(self, name):
return True
After (Modern Interface - REQUIRED):
app = ActingWebApp("my-app", "dynamodb", "myapp.com")
@app.property_hook("*")
def handle_properties(actor, operation, value, path):
if operation == "get":
return value
return value
@app.callback_hook("*")
def handle_callbacks(actor, name, data):
return {"status": "handled"}
Handler instantiation changes: - Before: Handler(webobj, config, on_aw=my_onaw_instance) - After: Handler(webobj, config, hooks=app.hooks)
Key Benefits of Migration: - 95% less boilerplate code - Better type safety and IDE support - Easier testing and debugging - Single source of truth for application logic - No more dual interface maintenance
v2.6.5: Apr 22, 2021
Fix bug in subscription_diff handling by replacing query with scan as query requires hash key
v2.6.4: Apr 11, 2021
Messed up release versioning, bump up to avoid confusion
v2.6.3: Apr 11, 2021
Fix bug in peertrustee handling by replacing dynamodb count() with scan() as count requires a hash key
v2.6.2: Oct 20, 2020
Security fix on oauth refresh
v2.6.1: Aug 30, 2020
Fix token refresh to also use Basic authorisation
v2.6.0: Aug 23, 2020
Add support for optional Basic authorisation in token request (e.g. Fitbit is requiring this)
v2.5.1: Jan 29, 2019
Move some annoying info messages to debug in auth/oauth
Fix bug in set_attr for store where struct is not initialised (attribute.py:70)
Enforce lower case on creator if @ (i.e. email) in value
v2.5.0: Nov 17, 2018
BREAKING: /www/properties template_values now return a dict with { ‘key’: value} instead of list of { ‘name’: ‘key’, ‘value’: value}
Add support for scope GET parameter in callback from OAUTH2 provider (useful for e.g. Google)
Add support for oauth_extras dict in oauth config to set additional oauth paramters forwarded to OAUTH2 provider (Google uses this)
Add support for dynamic:creator in oauth_extras to preset login hint etc when forwarding to OAuth2 auth endpoints (if creator==email, this allows you to send Google hint on which account to use with ‘login_hint’: ‘dynamic:creator’ in oauth_extras in config
Add support for actor get_from_creator() to initialise an actor from a creator (only usable together with config variable unique_creator)
Add support for get_properties(), delete_properties(), put_properties(), and post_properties in the on_aw() class. These allows on_aw overriding functions to process any old and new properties and return the resulting properties to be stored, deleted, or returned
Move all internal (oauth_token, oauth_token_expiry, oauth_refresh_token, oauth_token_refresh_token_expiry, cookie_redirect, and trustee_root) data from properties (where they are exposed on GET /<actor_id>/properties) to internal variable store (attributes). Introduce config variable migrate_2_5_0 (default True) that will look for properties with oauth variable names if not found in internal store and move them over to internal store (should be turned off when all actors have migrated their oauth properties over to store)
Add new interface InternalStore() (attribute.py) for storing and retrieving internal variables on an actor (i.e. attributes). All actors now have .store that can be used either as a dict or dot-notation. actor.store.var = ‘this’ or actor.store[‘var’] = ‘this’. Set the variable to None to delete it. All variables are immediately stored to the database. Note that variable values must be json serializable
Add new interface PropertyStore() (property.py) for storing and retrieving properties. Used just like InternalStore() and access through actor.property.my_var or actor.property[‘my_var’]
InternalStore(actor_id=None, config=None, bucket=None) can be used independently and the optional bucket parameter allows you to create an internal store that stores a set of variables in a specific bucket. A bucket is retrieved all at once and variables are written to database immediately
Fix issue where downstream (trusts) server processing errors resulted in 405 instead of 500 error code
Fix bug in oauth.put_request() where post was used instead of put
Fix issue where 200 had Forbidden text
v2.4.3: Sep 27, 2018
Don’t do relative import with import_module, AWS Lambda gets a hiccup
v2.4.2: Sep 27, 2018
Get rid of future requirement, just a pain
v2.4.1: Sep 26, 2018
Fix bad relative imports
Use extras_require for future (python2 support)
v2.4.0: Sep 22 2018
Support python3
v2.3.0: Dec 27, 2017
Entire API for handlers and Actor() as well as other objects changed to be PEP8 compliant
Add support for head_request(() in oauth and oauth_head() in auth
Change all uses of now() to utcnow()
db_gae for Google AppEngine is not kept updated, so folder deprecated and just kept for later reference
Full linting/PEP8 review
Add support for actor_id (set id) on Actor.create()
v2.2.2: Dec 3, 2017
Fix bug in region for properties and attributes resulting in using us-east-1 for these (and not us-west-1 as default)
v2.2.1: Dec 3, 2017
Add support for environment variable AWS_DB_PREFIX to support multiple actingweb tables in same DynamoDB region
v2.2.0: Nov 25, 2017
Add support for attribute.Attributes() and attribute.Buckets() (to be used for internal properties not exposed)
Various bug fixes to make the oauth flows work
v2.1.2: Nov 12, 2017
Split out actingweb module as a separate pypi library and repository
Python2 support, not python3
Support AWS DynamoDB and Google Datastore in sub-modules
Refactor out a set of handlers to allow easy integration into any web framework
actingwebdemo as a full-functioning demo app to show how the library is used
Jul 9, 2017
Fix bug with unique actor setting and actor already exists
Improve handling of enforce use of email property as creator
Fix auth bug for callbacks (401 when no auth is expected)
Add support for “lazy refresh” of oauth token, i.e. refresh if expired or refresh token has <24h to expiry
Add support for Actors() class in actor.py to get a list of all actors with id and creator (ONLY for admin usage)
Fix various bugs when subscriptions don’t exist
Improve logging when actor cannot be created
Apr 2, 2017
Changed license to BSD after approval from Cisco Systems
Fix bug in deletion of trust relationship that would not delete subscription
Add support for GET param ?refresh=true for web-based sessions to ignore set cookie and do oauth
Fix bug in oauth.oauth_delete() returning success when >299 is returned from upstream
Mar 11, 2017
Fix bug in aw_actor_callbacks.py on does exist test after db refactoring
Fix bug in handling of www/init form to set properties
Add support to enforce that creator (in actor) is unique (Config.unique_creator bool)
Add support to enforce that a creator field set to “creator” is overwritten if property “email” is set (Config.force_email_prop_as_creator bool, default True). Note that username for basic login then changes from creator to the value of email property. This functionality can be useful if actor is created by trustee and email is set later
Add new DbActor.py function get_by_creator() to allow retrieving an actor based on the creator value
Feb 25, 2016
Major refactoring of all database code
All db entities are now accessible only from the actingweb/* libraries
Each entity can be accessed one by one (e.g. trust.py exposes trust class) and as a list (e.g. trust.py exposes trusts class)
actor_id and any parameters that identify the entity must be set when the class is instantiated
get() must be called on the object to retrieve it from the database and the object is returned as a dictionary
Subsequent calls to get() will return the dictionary without database access, but any changes will be synced to database immediately
The actingweb/* libraries do not contain any database-specific code, but imports a db library that exposes the barebone db operations per object
The google datastore code can be found in actingweb/db_gae
Each database entity has its own .py file exposing get(), modify(), create(), delete() and some additional search/utility functions where needed
These db classes do not do anything at init, and get() and create() must include all parameters
The database handles are kept in the object, so modify() and delete() require a get() or create() before they can be called
Currently, Google Datastore is the only supported db backend, but the db_* code can now fairly easily be adapted to new databases
Nov 19, 2016
Create a better README in rst
Add readthedocs.org support with conf.py and index.rst files
Add the actingweb spec as an rst file
Add a getting-started rst file
Correct diff timestamps to UTC standard with T and Z notation
Fix json issue where diff sub-structures are escaped
Add 20 sec timeout on all urlfethc (inter-actor) communication
Support using creator passphrase as bearer token IF creator username == trustee and passphrase has bitstrength > 80
Added id, peerid, and subscriptionid in subscriptions to align with spec
Add modiify() for actor to allow change of creator username
Add support for /trust/trustee operations to align with spec
Add /devtest path and config.devtest bool to allow test scripts
Add /devtest testing of all aw_proxy functionality
Nov 17, 2016
Renaming of getPeer() and deletePeer() to get_peer_trustee() and delete_peer_trustee() to avoid confusion
Support for oauth_put() (and corresponding put_request()) and fix to accept 404 without refreshing token
aw_proxy support for get_resource(), change_resource((), and delete_resource(()
Support PUT on /resources
Nov 5, 2016
Add support for getResources in aw_proxy.py
Renamed peer to peerTrustee in peer.py to better reflect that it is created by actor as trustee
Nov 1, 2016
Add support for change_resource(() and delete_resource(() in aw_proxy.py
Add support for PUT to /resources and on_put_resources() in on_aw_resources.py
Oct 28, 2016
Add support for establishment and tear-down of peer actors as trustee, actor.getPeer() and actor.deletePeer()
Add new db storage for peers created as trustee
Add new config.actor section in config.py to define known possible peers
Add new actor support function: getTrustRelationshipByType()
Add new AwProxy() class with helper functions to do RPCish peer operations on trust relationships
Either use trust_target or peer_target to send commands to a specific trust or to the trust associated with a peer (i.e. peer created by this app as a trustee)
Support for create_resource() (POST on remote actor path like /resources or /properties)
Fix bug where clean up of actor did not delete remote subscription (actor.delete())
Add remoteSubscription deletion in aw-actor-subscription.py
Fix auth issue in aw-actor-callbacks.py revealed by ths bug
Oct 26, 2016
Add support for trustee by adding trustee_root to actor factory
Add debug logging in auth process
Fix bug where actors created within the same second got the same id
Oct 15, 2016
Added support for requests to /bot and a bot (permanent) token in config.py to do API requests without going through the /<actorid>/ paths. Used to support scenarios where users can communicate with a bot to initiate creation of an actor (or to do commands that don’t need personal oauth authorization.
Oct 12, 2016
Support for actor.get_from_property(property-name, value) to initialse an actor from db by looking up a property value (it must be unique)
Oct 9, 2016
Added support for GET, PUT, and DELETE for any sub-level of /properties, also below resource, i.e. /properties/<subtarget>/<resource>/something/andmore/…
Fixed bug where blob=’’, i.e. deletion, would not be registered
Oct 7, 2016
Added support for resource (in addition to target and subtarget) in subscriptions, thus allowing subscriptions to e.g. /resources/files/<fileid> (where <fileid> is the resource to subscribe to. /properties/subtarget/resource subscriptions are also allowed.
Oct 6, 2016
Added support for /resources with on_aw_resources.py in on_aw/ to hook into GET, DELETE, and POST requests to /resources
Added fixes for box.com specific OAUTH implementation
Added new function oauth_get(), oauth_post(), and oauth_delete() to Auth() class. These will refresh a token if necessary and can be used insted of oauth.get_request(), post_request(), and delete_request(()
Minor refactoring of inner workings of auth.py and oauth.py wrt return values and error codes
Sep 25, 2016
Added use_cache=False to all db operations to avoid cache issue when there are multiple instances of same app in gae
Sep 4, 2016
Refactoring of creation of trust: - ensure that secret is generated by initiating peer - ensure that a peer cannot have more than one relationship - ensure that a secret can only be used for one relationship
Aug 28, 2016
Major refactoring of auth.py. Only affects how init_actingweb() is used, see function docs
Aug 21, 2016: New features
Removed the possibility of setting a secret when initiating a new relationship, as well as ability to change secret. This is to avoid the possibility of detecting existing secrets (from other peers) by testing secrets
Aug 15, 2016: Bug fixes
Added new acl[“approved”] flag to auth.py indicating whether an authenticated peer has been approved
Added new parameter to the authorise() function to turn off the requirement that peer has been approved to allow access
Changed default relationship to the lowest level (associate) and turned off default approval of the default relationship
Added a new authorisation check to subscriptions to make sure that only peers with access to a path are allowed to subscribe to those paths
Added a new approval in trust to allow non-approved peers to delete their relationship (in case they want to “withdraw” their relationship request)
Fixed uncaught json exception in create_remote_subscription()
Fixed possibility of subpath being None instead of ‘’ in auth.py
Fixed handling of both bool json type and string bool value for approved parameter for trust relationships
Aug 6, 2016: New features
Support for deleting remote subscription (i.e. callback and subscription, dependent on direction) when an actor is deleted
New delete_remote_subscription() in actor.py
Added deletion to actor.delete()
New handler for DELETE of /callbacks in aw-actor-callbacks.py
New on_delete_callbacks() in on_aw_callbacks.py
Aug 6, 2016: Bug fixes
Fixed bug where /meta/nonexistent resulted in 500
Aug 3, 2016: New features
Support for doing callbacks when registering diffs
New function in actor.py: callback_subscription()
Added defer of callbacks to avoid stalling responses when adding diffs
Added new function get_trust_relationship() to get one specific relationship based on peerid (instead of searching using get_trust_relationships())
Improved diff registration
Totally rewrote register_diffs() to register diffs for subscriptions that are not exact matches (i.e. broader/higher-level and more specific)
Added debug logging to trace how diffs are registered
Owner-based access only to /callbacks/subscriptions
Support for handling callbacks for subscriptions
New function in on_aw_callbacks.py: on_post_subscriptions() for handling callbacks on subscriptions
Changed aw-actor-callbacks.py to handle POSTs to /callbacks/subscriptions and forward those to on_post_subscriptions()
Aug 3, 2016: Bug fixes
Added no cache to the rest of subscriptionDiffs DB operations to make sure that deferred subscription callbacks don’t mess up sequencing
Changed meta/raml to meta/specification to allow any type of specification language
Aug 1, 2016: New features
Added support for GET on subscriptions as peer, generic register diffs function, as well as adding diffs when changing /properties. Also added support for creator initiating creation of a subscription by distingushing on POST to /subscriptions (as creator to inititate a subscription with another peer) and to /subscriptions/<peerid> (as peer to create subscription)
Subscription is also created when initiating a remote subscription (using callback bool to set flag to identify a subscription where callback is expected). Still missing support for sending callbacks (high/low/none), as well as processing callbacks
Added support for sequence number in subscription, so that missing diffs can be detected. Specific diffs can be retrieved by doing GET as peer on /subscriptions/<peerid>/<subid>/<seqnr> (and the diff will be cleared)
Jul 27, 2016: New features
Started adding log statements to classes and methods
Added this file to track changes
Added support for requesting creation of subscriptions, GETing (with search) all subscriptions as creator (not peer), as well as deletion of subscriptions when an actor is deleted (still remaining GET all relationship as peer, GET on relationship to get diffs, DELETE subscription as peer, as well as mechanism to store diffs)
Jul 27, 2016: Bug fixes
Changed all ndb.fetch() calls to not include a max item number
Cleaned up actor delete() to go directly on database to delete all relevant items
Fixed a bug where the requested peer would not store the requesting actor’s mini-app type in db (in trust)
Added use_cache=False in all trust.py ndb calls to get rid of the cache issues experienced when two different threads communicate to set up a trust
Added a new check and return message when secret is not included in an “establish trust” request (requestor must always include secret)
July 12, 2016: New features
config.py cleaned up a bit
July 12, 2016: Bug fixes
Fix in on_aw_oauth_success where token can optionally supplied (first time oauth was done the token has not been flushed to db)
Fix in on_aw_oauth_success where login attempt with wrong Spark user did not clear the cookie_redirect variable
Fixed issue with wrong Content-Type header for GET and DELETE messages without json body