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 without re.DOTALL, so * could not cross a newline and $ tolerated a trailing one: for a friend or partner peer, excluded_patterns: ["private/*", "security/*", "_internal/*"] did not match private/\nx. The reachable vector is the JSON body of POST /{actor}/properties (keys are property names); at the previous release a friend peer’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 grants friend or partner, or a custom trust type with excluded_patterns or a bare-literal denied entry, 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 for secret\n now that the anchor is exact. Denials are logged at WARNING with the identifier’s repr().

    • New property and list names containing a control character are refused at the store (ValueError) and answered 400 by the REST layer, for every path segment of a PUT and every key of a POST (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>\n peer id is how a remote:<id>\n bucket came to exist), the two encrypted-state classifiers, the v1 list index and orphan-row patterns, and MCP resource URI template matching. Legacy uri_pattern resource dispatch uses re.fullmatch instead of the prefix match re.match.

    • The six MCP single-item permission checks (tools/call, prompts/get, resources/read on both transports) now fail closed: an evaluator that raises denies with -32003 and an ERROR log line carrying the traceback, instead of serving the request at DEBUG. The */list filters 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/get and resources/read with -32003 until it is fixed, where it used to let them through — an availability cost taken deliberately on a security path, and the ERROR log line names it. The equivalent methods/actions deny 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 reserves None for 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) after get_bucket() == {} answers None without a backend read for the life of that instance. DynamoDB still reports most faults by raising rather than returning None, because the guarded region there is Query construction and PynamoDB fires the request lazily. The five library call sites that end in get_bucket(...) or {} (attribute_list_store, callback_processor, remote_storage twice, fanout) still fold a fault into “empty” and are knowingly deferred.

  • The two backends stored different attribution for a colliding attribute row. bucket_name is bucket + ":" + name and both halves may contain :, so bucket remote:abc/name x and bucket remote/name abc:x share a primary key. DynamoDB’s PutItem reattributed the row to the last writer; PostgreSQL’s ON CONFLICT DO UPDATE refreshed only data/timestamp and kept the first writer’s bucket. The upsert now sets bucket and name too, 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_conditional and conditional_update_attr keyed on bucket_name alone and would answer, or delete, the colliding sibling’s row; they now apply the same exact bucket compare get_bucket() and delete_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 the resource_documentation target 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 (from with_mcp(server_name=)). Changed: mcp_enabled follows the configured value (it was a literal True), description is the app’s own (ActingWeb app: {aw_type} unless the app sets desc), and supported_features lists only what the registry actually exposes. Read the tool list from tools/list. No library version is disclosed. This changes a response shape in a patch release: a client that reads tools_count, prompts_count or actor_lookup from /mcp/info will 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 literal True capabilities regardless of configuration while the initialize handshake answered the configured name; the three surfaces now share one derivation. A deployment that never set server_name sees "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 .env file) is stripped; a double quote, backslash, interior whitespace or control character raises ValueError naming the character, APP_HOST_FQDN / APP_HOST_PROTOCOL, and the accepted form host[:port][/base] with no scheme. ActingWebApp strips at its boundary too. A scheme prefix on fqdn is 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-server from OAuth2EndpointsHandler, the method was never rendered in the API docs, and while it lived it advertised scopes_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() and migrate_to_v2() each read the actor’s whole property partition through fetch_all_including_lists() and ended the line in or {}. PostgreSQL’s fetch_all_including_lists returns None on a caught exception, so a throttle or a dropped connection presented itself as an empty partition: compact() computed ordered_values = [], deleted rows 0..stored_length-1 and wrote length: 0 — and a following verify() reported the now-empty list healthy: true. All three now read only their own list’s rows, through get_range(_v1_bounds()), which raises DbError on 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 to 0..n-1 then deletes the tail, migrate_to_v2() deletes v1 rows 0..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. AuthenticatedPropertyListStore defined 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 to NOT_FOUND and only DENIED raises — and returned a _PermissionEnforcingListView wrapping 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 both names and rows, in one bulk permission evaluation. Rows are narrowed with the library’s own attribution logic, never a bare startswith() prune, which for a denied list foo would also strip permitted sibling foo-old’s item rows while leaving its -meta row — 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 raises AttributeError for 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 is bucket + ":" + name, but get_bucket() and delete_bucket() queried it with begins_with(bucket) — no delimiter — so a bucket saw, and in delete_bucket()’s case destroyed, the rows of every bucket having its name as a prefix. RemotePeerStore.delete_all() tears down bucket remote:{peer_id} when a trust relationship ends, and most call sites build that id with validate_peer_id=False, so the ids are remote-party-chosen: ending trust with peer abc deleted peer abcd’s entire dataset. Both methods now query with the delimiter and compare the stored bucket exactly, the guard delete_by_chain() and subscription_suspension’s cascade check already carried. The delimiter alone is not enough — bucket names contain : and attribute names contain :, so bucket remote:abc/name x and bucket remote/name abc:x produce an identical range key and are in fact the same row. PostgreSQL compared bucket exactly 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 PropertyListStore and on the ActorInterface wrapper. Both halves of the return are scoped: names holds only the matching lists, so code migrating from list_all_with_rows() that keeps iterating names silently stops seeing every list outside the prefix — this is a contract, not a caveat. prefix is a prefix, not a namespace: it also matches a list named exactly prefix and 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 raises ValueError, and a backend fault raises DbError rather than swallowing to ([], {}) as list_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-only list_prefix() sibling: a keys-only projection saves no DynamoDB read capacity.

  • ``actingweb.property.rows_for(names, rows)`` — the subset of a rows dict attributable to a given set of list names, using the library’s own row encoding. For narrowing a (names, rows) pair after pruning names. A bare startswith(f"list:{name}-") is wrong here: for list foo it also claims sibling foo-old’s rows, and used to prune it strips a permitted sibling’s item rows while keeping its -meta row, after which to_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_range cannot 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 native begins_with; PostgreSQL uses starts_with() with a bound parameter — not LIKE, so _ and % are literal, and not a COLLATE "C" bound pair, because byte ordering itself disagrees between collations while starts_with does 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() answers None for 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-lived Attributes instance loses the accidental first-miss re-read, so an attribute written by another process after this instance loaded the bucket is not seen by get_attr() on it. Library call sites are unaffected — every Attributes in the permission and token paths is constructed per call — but note that handlers/mcp.py caches an ActorInterface on a sliding five-minute TTL, so instances there can outlive a single request. Call delete_bucket(), or construct a fresh Attributes, 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 make get_bucket() report names that have no stored row; and a delete_attr() (or falsy set_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 data as a delete and return Truedelete_attr() is literally set_attr(data=None) — while the in-memory dict cached {"data": <falsy>, "timestamp": ...}. So set_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 holding null still 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() and migrate_to_v2() read one list through get_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, behind ACTINGWEB_LAZY_MIGRATION_MAX_LENGTH), which runs all three inside a user’s append()/insert(): three whole-partition dumps in one request become three one-list reads. Reports are unchanged, including foreign_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 401 challenge — 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 reading WWW-Authenticate. A conformant client instead parsed this handler’s bespoke JSON body as the protected-resource metadata and failed on the missing resource field, never reaching the real metadata — which was correct all along. Codex reported exactly that: “Metadata error: Protected resource metadata missing required resource field”. An unauthenticated GET — including one carrying Accept: text/event-stream, the spec’s stream opener — is now the same 401 challenge as every other MCP method. Behavior change: a caller that read the discovery document from GET /mcp without a token now gets 401. The document is unchanged and has not moved — an authenticated GET /mcp still 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/info is a separate endpoint with a different shape, not a substitute. The .with_mcp(enable=False) 404 still takes precedence, so a disabled endpoint never advertises an authorization server.

  • The MCP ``401`` challenge did not point at the protected-resource metadata. WWW-Authenticate carried only a non-standard authorization_uri, omitting the resource_metadata parameter that RFC 9728 section 5.1 (and, through it, the MCP authorization spec from 2025-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 carries resource_metadata="<base>/.well-known/oauth-protected-resource/mcp" alongside the existing error="invalid_token" and authorization_uri hints, 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_token grant; 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 as mcp.

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 called execute_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_hook was silently never invoked. The legacy fallback branch in actingweb/handlers/callbacks.py now also calls execute_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_hook handles (returns truthy for) now gets 204 instead of 400 — 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 /mcp unconditionally, and neither MCPHandler.get()/.post() nor AsyncMCPHandler.post_async() checked config.mcp before serving a full response — an app that disabled MCP still had a live, responding MCP server. All three now return 404 when config.mcp is False. Behavior change: found while consolidating examples/demo/ (which disables MCP) — a consumer relying on the endpoint silently responding regardless of with_mcp() will now see 404 unless it explicitly enables MCP. MCPHandler()’s default (test-only) Config now defaults mcp=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; use app.get_config()), execute_action_hooks’s argument order (action_name comes first, not actor), and the @resource_hook example in actingweb/mcp/decorators.py’s docstring (the real mechanism is @app.method_hook(...) + @mcp_resource(...)). Also fixed: docs passing a literal peer_id="peer123" after binding create_relationship()’s real return value, in docs/guides/trust-relationships.rst and docs/quickstart/getting-started.rst.

  • with_sync_callbacks()’s docstring said its default was True; the underlying setting defaults to False until the method is called. Clarified that calling the method at all is the opt-in.

  • from actingweb.interface import lifecycle_hook raised ImportError; 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 approved column (invalid input syntax for type boolean) when a caller omits the argument. DynamoDB silently tolerated the empty string. Both backends now default to False, matching the TrustProtocol signature. 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 and subscribe_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.py now registers an on_trust_request_received lifecycle 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.py had the same missing proto="http://" override as the p2p example did before this release — the OAuth2 redirect URI generated for Stage 2 pointed at https://localhost:..., which nothing on the plain-HTTP uvicorn server it starts answers on.

CHANGED

  • CI now enforces ruff format --check alongside ruff 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/...rst paths, 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 initialize 401s 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 before tools/list and tools/call work, 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”; keywords gained mcp, ai, llm, agent, model-context-protocol, and actor; added the Topic :: Scientific/Engineering :: Artificial Intelligence and Programming Language :: Python :: 3.13 trove classifiers; homepage/documentation URLs are now https://; 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 to Callable[..., Any] despite py.typed shipping — 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 nonexistent thoughts/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 to CLAUDE.md plus MCP/peer-to-peer quickstart links for anyone building an application with the library. Also removed AGENTS.md from .github/workflows/claude-code-review.yml’s paths-ignore — that exemption is what let it go eight months without automated review while CLAUDE.md stayed current.

  • Superseded-API warnings added to every migration guide (docs/migration/v3.1.rst through v3.14.rst) and inline markers on illustrative (non-real) signatures in docs/contributing/style-guide.rst and architecture.rst. Grepping docs/ 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.py for that) moved into this repository from the separate actingwebdemo repository, 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, the demo.actingweb.io custom domain) stays in actingwebdemo, which remains the deployment pipeline for this code — see examples/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’s acl_rules, look up an actor by property value — for AI coding agents working in a repository that merely pip 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 clone this repo and point your agent at the directory, or npx skills add actingweb/actingweb.

  • ``llms.txt`` / ``llms-full.txt`` are now generated on every docs build via sphinx-llms-txt, and will be served at https://actingweb.readthedocs.io/en/latest/llms.txt once 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.rst and docs/guides/oauth-login-flow.rst were previously .md files — invisible to the Sphinx build (source_suffix is .rst only) 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 .md files under docs/ in the same situation (docs/guides/postgresql-migration.md, docs/contributing/TESTING.md) duplicated a larger, current .rst twin 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 the count field 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. See docs/guides/property-lists.rst for 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() or update_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 ListMetadataContentionError below.

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. See docs/guides/property-lists.rst for 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 from actingweb): 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. See docs/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 None now replicates to subscribed peers like any other value. Previously a diff for such an item omitted its item/old_item field entirely, and the receiving side dropped the whole notification as unrecognized – silently, the same failure mode as the remove() 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 core ListProperty layer 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-orphans re-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 uses remove(), 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:

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

  2. The three entries marked Breaking under CHANGED — the /properties/<name>/items response shape, list reads failing fast on corruption instead of silently compacting, and out-of-bounds PUT now returning 404.

  3. 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 releaseactingweb-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 since v3.3 (2025-10-04) through v3.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 default mcp_client trust type’s narrow pattern set (public/*, shared/*, notes://*, usage://*, actingweb://properties/all), see the audit step in docs/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/list and a distinct -32003 error naming the cause on tools/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 /trust peer protocol could satisfy without ever going through MCP client registration. The resolver now requires an exact oauth_client_id match, or — for legacy rows created before that field existed — an exact, fully-reconstructed peer-id match gated on the row’s established_via being an OAuth2-family value. See docs/migration/v3.13.rst for how to find trust rows that predate oauth_client_id and 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, and last_connected_via may 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_table defaults to True; the deprecated legacy GSI/index mode can be pinned with with_legacy_property_index(True) or USE_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 new scripts/backfill_property_lookup.py is run — see docs/migration/v3.13.rst for 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>_properties tables no longer carry the legacy value-keyed property-index GSI — 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_v2 table 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 return None with 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>/properties now 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>_peertrustees too.)

  • 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 Scan with 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 partition Query calls; 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 DescribeTable call (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 ActorInterface to every request for a hot actor — so context could leak between requests and, under concurrency, between callers. It is now stored in a contextvars.ContextVar keyed 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 with set_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). See docs/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 express tools as allowed/denied lists, which do not consult operation), so this changes nothing unless your application defines a patterns/operations-based tools rule — in which case a rule that matched invoke on FastAPI must now match use.

  • MCP ``structuredContent`` is now opt-in. tools/call results emit structuredContent only 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 into structuredContent.

    Why this changed: at least one major MCP client discards every text content block when structuredContent is 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 emits structuredContent only when content is 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’s secret to the model; properties.to_dict() can likewise carry oauth_token / oauth_refresh_token. Only an explicitly named structuredContent now leaves the process. Note the win is scoped to the content branch — the legacy text-wrap path still stringifies the whole dict.

    Migration: nest the data you want structured under an explicit structuredContent key, and keep the same object serialized in a text content block (the spec’s backwards-compatibility guidance — some clients ignore structuredContent entirely). For tools whose payload is prose, drop the extras instead. The explicit passthrough already exists in v3.11.0 and v3.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 length a 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) – ListProperty raises ValueError immediately 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.py now 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 new scripts/migrate_property_lists.py (dry-run by default; --migrate to perform it; reports refused names and duplicate residue; --downgrade ACTOR_ID/list_name is 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() raise ListCorruptionError (an IndexError subclass) when an item within the list’s recorded length is missing from storage, matching AttributeList’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 with ListProperty.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}. index is the storage index, matching what action=update/action=delete already expect in item_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 None up to N (an unbounded-write DoS vector as well as a spec violation — the spec requires 404 for index > length; index == length still MAY append). docs/protocol/actingweb-spec.rst “List Property PUT”.

  • The bulk list-item POST (POST /properties with 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=N follows. 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 with None one 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_identities report 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 same id at positions 31 and 36 and called the list healthy). duplicate_identities compares the identifying field across the whole list and survives later edits, and the report carries identity_checked_count so 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-lists and actingweb-migrate-property-lists (implementation moved to actingweb.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_PREFIX is an unset default. The library defaults it to demo_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.py no 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.py and reading the output — but doing it silently under an ordinary append() destroyed the evidence: the hole disappeared, the lost item stayed lost, duplicate residue was promoted to real data, and verify() began reporting the list healthy. Repair (compact() or verify_property_lists.py --repair) is now always an explicit operator action; damaged lists keep serving v1 and keep raising ListCorruptionError until then.

  • Automatic conversion of existing lists to the v2 format is OFF by default, controlled by the new ACTINGWEB_LAZY_MIGRATION_MAX_LENGTH environment variable (default 0; set a positive number to allow v1 lists of at most that size to convert on their next write, or use scripts/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 no length field 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 --downgrade cannot 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 one append() 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.py now uses it instead of verify() 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 --migrate and ListProperty.migrate_to_v2() refuse a list whose verify() reports missing or orphaned indices, and the dry run reports those lists as needing repair rather than counting them as “would migrate” — it exits 1 when any exist, so 0 means 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 next clear()/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 as foreign_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 fresh ListProperty on every attribute access, so ordinary actor.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’s DbProperty exposes; 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-Version this server does not speak still gets HTTP 400 with JSON-RPC -32600 and no data payload — 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 to initialize. What changed: the error message now 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-Id or the User-Agent; the number of tracked origins is capped.

    The response code and the absence of data.supported are now covered by regression tests that state why, since “fixing” this to a spec-shaped -32022 with data.supported would 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 paths revoke_access_token() / revoke_refresh_token() (which is what /oauth/revoke reaches), revoke_token_chain() (the refresh-token-reuse theft response), revoke_all_tokens(), trust deletion, and both the store and delete paths of TrustPermissionStore. It is actor-wide by necessity rather than scoped to one client: the cached ActorInterface carries 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. outputSchema comes solely from @mcp_tool; a schema passed to @app.action_hook(..., output_schema=...) or derived from a TypedDict return 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-structuredContent warning 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 outputSchema for every TypedDict-annotated tool, and each one that does not also return structuredContent would begin failing on spec-conforming clients. That decision belongs with the structuredContent work 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=false or call with_dynamodb(auto_create_tables=False). With auto-creation off, the library never calls DescribeTable/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) returns DeletionStatus.DELETED / NOT_DELETED / UNKNOWN from a tombstone written before the actor_deleted hook runs — so an external call made from that hook, and any provider callback racing it, already sees DELETED — 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, so get_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_deleted cancels an external subscription; the provider’s callback races the wipe). Checking harder is not available either, because get_by_id() returns None for 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: UNKNOWN means 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 one GetItem, 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=None and actor_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_deleted is 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 in actor_deleted, act in actor_deleted_complete — which removes the race at its source independently of the tombstone. The absent ActorInterface is 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 on get_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 add get_attr_strict() to keep satisfying isinstance() checks against the protocol. Both bundled backends implement it.

  • Operator-facing table verifier: python -m actingweb.db.verify_tables reports 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 an AccessDenied per 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. --list prints the names without calling AWS. Exit codes: 0 all present, 1 one or more missing, 2 the 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). Dropping DescribeTable/CreateTable from the runtime role affects the whole role, not just the library, so application code that probes its own tables (boto3 table.load()/describe_table, pynamodb exists()) needs the same switch — previously it had to re-implement the environment parsing. set_auto_create() and reset_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 advertises outputSchema in tools/list but 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_schema and structuredContent remain independent: declaring a schema has never caused structured output to be emitted, and the library still does not validate structuredContent against 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. None is exempt and stays silent — it carries no payload, both reference clients read a null structuredContent as 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 as null.

  • 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 preserving description/explanation/ created_at (unlike the previous clear() + 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 through actor.property_lists.<name>.verify()/.compact(). New operator script: scripts/verify_property_lists.py (dry-run by default; --repair invokes compact() on unhealthy lists).

  • Opt-in diagnostics for the PostgreSQL attribute ``DELETE`` path. Set ACTINGWEB_PG_DELETE_DIAGNOSTICS=1 to log, per attribute delete, the statement’s rowcount, the deleting connection’s resolved schema and search_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 the DELETE and 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. ActingWebApp stamped its own hardcoded lookup-table defaults onto the config on every with_*() call (a guard intended to detect “explicitly set” was always true), so the documented USE_PROPERTY_LOOKUP_TABLE and INDEXED_PROPERTIES environment 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 DynamoDB GetItem and 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, no DescribeTable, no Scan.

  • A failed subscription-suspension check was indistinguishable from “not suspended”. Actor.is_subscription_suspended() logged any failure at DEBUG and returned False, so a missing suspensions table (pynamodb raises TableDoesNotExist, which is not DoesNotExist) or a denied read made every target read as un-suspended: a bulk import’s suspend() 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. PropertyStore registers every diff with the property name as the subtarget, so the check looked up "properties:<name>" while suspend() 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 via ActorInterface.subscriptions or the authenticated-peer view was invisible to register_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 cached Actor, still leaves that instance’s list stale until it is rebuilt — tracked in thoughts/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() and get_cached_capabilities_store() each take a config argument 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 different Config instance, and still returns the cached instance for the same one. Single-application deployments — the overwhelming majority — are unaffected either way.

    Note that ActingWebOAuth2Server composes three of these, so rebinding the server alone was not sufficient: client_registry, token_manager and state_manager had 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 no content key previously reached the wire with no isError field at all, so the failure was reported to the client as a success. isError is 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. The str(result) text serialization is unchanged, so isError appears both inside that text and as a wire field.

  • ``list.index()`` semantics for negative ``start``/``stop``. index(value, -1) could previously return -1 as an index (the loop ran range(-1, n) and then indexed negatively). Both storage formats now normalize bounds exactly as list.index does, 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, so pop() always returns exactly what it removed and remove() always removes exactly what it matched. __delitem__/__setitem__ remain unconditional by design.

  • pop() on a v2 list no longer raises pop from empty list against 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 cached DbProperty handle 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 raises actingweb.db.exceptions.DbError on a genuine backend fault instead of returning None (None means the row does not exist, and only that, on both backends). Every ListProperty mutation (append, __setitem__, __delitem__, insert, clear, delete, metadata writes) now checks set()’s return value and raises RuntimeError instead of continuing past a failed write. This is a breaking change for any code that relied on a backend fault degrading to None/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) — ListProperty now raises ValueError instead, 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.rst had 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 — from 3.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 of rc1 through rc6) — followed by what changed after rc6 for 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, that get_by_id() keeps resolving throughout the wipe, that its None means “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 unknown actor_id creates rows nothing will clean up. That is by design (actor_id is 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_deleted is the right hook there (/auth/revoke is 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=false makes this a precondition, and it previously existed only in source. <prefix>_subscription_suspensions is 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_v2 first, failing the stack with ResourceInUseException. Sequence the deploys, or let auto-creation own the tables — not both.

  • The rollback instruction is qualified. Legacy mode needs the property-index GSI on the properties table; a table created before that index existed has no rollback path for reverse lookup. Check describe-table before 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 a before-call.dynamodb session handler counts nothing under pynamodb (get_session() returns a fresh session per call), which makes assert scans == 0 pass 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 None during 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 with scan --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 --repair will 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, and compact()’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 app templates_dir is searched first). Both integrations also register a /login route that renders the sign-in page.

FIXED

  • Programmatically-enabled property lookup tables leaked stale reverse-lookup rows on bulk delete. DbPropertyList.delete() and DbProperty.get_actor_id_from_property() constructed a fresh Config() internally to decide whether to maintain the property lookup table. A fresh Config() 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’s Config — 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 injects use_lookup_table / indexed_properties into DbPropertyList (matching get_property()), and both methods use the injected settings instead of a throwaway Config. 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 by actor_id).

  • ``with_mcp(server_name=…, instructions=…, enable=…)`` were silently ignored. ActingWebApp.__init__ builds the Config eagerly (permission warmup), and the runtime config-sync did not re-apply the MCP fields, so builder-set MCP options never reached Config. config.mcp is now a first-class attribute, kept in sync (including the advertised /meta capability tag).

  • ``@mcp_tool`` metadata no longer appears blank in ``GET /<id>/actions``. Stacking the required @app.action_hook("name") decorator over @mcp_tool attached empty explicit metadata that shadowed the MCP metadata. Hook metadata resolution now merges per field: explicit non-empty values win, otherwise the @mcp_tool metadata (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.root like https://host/base/) posted to and linked at the domain root instead of the mounted app. The factory handler now passes a base_path (derived from config.root) into the template context and the templates prepend it.

DOCUMENTATION

  • Rewrote README.rst to 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/PropertyStore method names and signatures, AwProxy usage, ActorInterface attributes, authenticated views, and the ActorInterface.create(hooks=app.hooks) requirement for lifecycle hooks to fire. Stopped recommending actor.is_owner() (a placeholder that always returns True) as an access guard.

  • Fixed quickstart friction found by a cold-build usability pass: DynamoDB Local prerequisites, the migrate_db.py download URL (master branch), MCP endpoint auth (no dev bypass), scalar-property stringification, templates_dir being 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 + .p8 private key). Apple’s ES256 client_secret JWT is minted on demand and cached per 5-minute bucket; the id_token is validated against Apple’s JWKS (no userinfo endpoint). The .p8 key is supplied via private_key_path / APPLE_PRIVATE_KEY_PATH (file wins) or private_key_pem / APPLE_PRIVATE_KEY_PEM and is validated eagerly at config-build time. See docs/guides/apple-sign-in.rst.

  • New ``app.with_github(…)`` builder mirroring with_apple_sign_in / with_google_native: fills in GitHub’s endpoints and, with mobile_redirect_uri, registers a github-mobile provider that uses the server-side ticket flow (GitHub issues no OIDC id_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 audiences or derives them from the per-platform client IDs).

  • Native mobile OAuth code exchange: the authorization_code grant on POST /oauth/spa/token lets native mobile apps exchange an OAuth code received via deep link for ActingWeb SPA tokens (RFC 8252). exchange_code_for_token() accepts an optional redirect_uri override so provider classes honor custom URL schemes, and the SPA authorize endpoint validates the requested provider via _is_known_provider(). Provider-name variants such as google-mobile / github-mobile are resolved by prefix matching in create_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 provider id_token (assertion) plus nonce for an ActingWeb session. The validator is dispatched by the declared provider and the token iss must 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’s nonce claim 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_post callback, 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 / sub plus passthrough) across all providers. Apple’s first-sign-in user payload (name) is merged before the hook fires. GitHub carries the profile name in name (falling back to login when unset), normalized consistently across the web-login, SPA-via-callback and token-exchange paths.

  • actor.store.oauth_provider is now written on every sign-in (create and existing-actor paths), so account-deletion / revocation logic can rely on it.

  • /oauth/config provider entries gain additive response_mode (form_post for Apple, query otherwise) and platform fields.

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/authorize redirect_uri allowlist.

  • ``ActingWebApp.with_spa_cors_origins(*origins)`` builder — restrict the CORS Access-Control-Allow-Origin for the SPA OAuth endpoints (default "*"). spa_cors_origins is now a first-class Config attribute, 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/token endpoint opportunistically calls the new OAuth2SessionManager.maybe_purge_expired_tokens(), which runs at most once per SPA_TOKEN_PURGE_INTERVAL (1 hour) per process via a process-local throttle. The underlying purge_expired_tokens() / backend DbAttribute.delete_expired(now_epoch=None, buckets=None) issues a single set-based DELETE on PostgreSQL backed by the existing idx_attributes_ttl partial index (O(expired rows), not O(all tokens)). On DynamoDB the purge is a no-op: cleanup relies on the table’s native TTL on ttl_timestamp, which must be enabled once per environment (see docs/reference/database-backends.rst → “Expired Token Cleanup (TTL)”).

  • Indexed refresh-token family revocation (PostgreSQL). revoke_token_chain delegates to a new backend DbAttribute.delete_by_chain(actor_id, buckets, chain_id). On PostgreSQL this is a single DELETE backed by a new partial expression index idx_attributes_chain_id on (data ->> 'chain_id') — O(chain) regardless of how large the shared token partition grows (requires the new Alembic migration ``d4e5f6a7b8c9``; run alembic upgrade head). On DynamoDB it remains a bounded scan of the two token buckets (no GSI on the JSON-embedded chain_id); a GSI on a promoted top-level chain_id attribute is the documented optimization path for very large DynamoDB deployments.

MCP:

  • MCP protocol version negotiation: the /mcp handler negotiates the protocol version during initialize instead of hardcoding 2024-11-05. It echoes the client’s requested protocolVersion when supported, otherwise returns the server’s latest supported version (maintained in actingweb/mcp/protocol.py, currently through 2025-11-25). The MCP-Protocol-Version request header is honored on post-initialize requests (defaulting to 2025-03-26 when absent, 400 when present-but-unsupported), and GET discovery reports the full supported-version set. Backward compatible: 2024-11-05-only clients still negotiate 2024-11-05. The new actingweb/mcp/protocol.py module exposes the version constants and negotiate_protocol_version / is_supported_protocol_version / supports_structured_content helpers as a single source of truth (also used by the OAuth2 discovery endpoint).

  • Structured tool output (``structuredContent``): tools/call results populate the spec structuredContent field when the negotiated protocol version supports it (>= 2025-06-18). A hook returning a dict with content plus extra top-level keys has those extras promoted into structuredContent; an explicit structuredContent from the hook is passed through, and a hook-supplied _meta is preserved. For older negotiated versions structuredContent is omitted (the payload is still carried by content).

    Warning

    The promotion described above was removed in v3.13.0rc4. structuredContent is now emitted only when a hook sets that key explicitly; extra top-level keys are no longer promoted. See the v3.13.0rc4 entry and docs/migration/v3.13.rst.

  • MCP tool per-actor visibility: @mcp_tool accepts a visibility_predicate(actor) -> bool to omit tools from tools/list for actors that should not see them (fail-closed on predicate errors). Note: visibility filtering applies to tools/list only — 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_tool accepts a description_predicate(actor) -> str | None to override the tool description per actor, taking precedence over client_descriptions and the static description.

  • Configurable MCP server name: ActingWebApp.with_mcp(server_name="myapp") sets the name announced in the MCP initialize handshake and surfaced on serverInfo.name (some clients use it as the default tool prefix, myapp:search vs actingweb:search).

  • Configurable MCP server instructions: ActingWebApp.with_mcp(instructions="...") sets the server-level orientation string emitted on the InitializeResult.instructions field — useful for pointing new LLMs at an entry-point tool (e.g. how_to_use()). serverInfo.version reports the ActingWeb version.

  • Per-MCP-session identity on ``MCPContext``: new optional transport_session_id and client_info fields expose per-session identity distinct from peer_id / trust_relationship (which are per-OAuth2- credential and shared across concurrent sessions on the same credential). transport_session_id is taken from the spec’s Mcp-Session-Id header and is None when the transport supplies none; client_info carries the live clientInfo captured at the session’s initialize call. get_client_info_from_context() prefers this live per-session client_info so two MCP sessions sharing one OAuth2 credential no longer see each other’s identity. Backward compatible: both fields default to None.

CHANGED

  • PyJWT[crypto] is now a core dependency (required for Apple’s ES256 client_secret and RS256 id_token validation).

  • FastAPI minimum raised to ``>=0.112`` (the fastapi extra). The FastAPI integration uses the Starlette 1.0 request-first TemplateResponse signature (adopted in 3.10.1), which requires Starlette >=1.0 / FastAPI >=0.112; the dependency floor now matches, so actingweb[fastapi] can no longer resolve an incompatible older FastAPI/Starlette that raised unhashable type: 'dict'.

  • OAuth2Authenticator was refactored to a strategy pattern: provider-specific behavior now lives on OAuth2Provider subclasses (GoogleOAuth2Provider / GitHubOAuth2Provider / AppleOAuth2Provider). Public method signatures and the actingweb.oauth2.requests patch 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 (nulling oauth_token, oauth_token_expiry and oauth_token_timestamp, leaving only the oauth_provider identity 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_assertion and 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": [...]} without isError was 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 no content key, or a bare value), which emits no isError at all; see the v3.13.0rc4 entry, which makes that path honour an explicitly set isError.

  • 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_id identifying their rotation family: rotation propagates the parent’s chain_id while each fresh login starts a new chain, and reuse-after-grace now calls OAuth2SessionManager.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 called revoke_all_tokens(actor_id), so a single stale token from one device logged the user out everywhere (and clients that did not degrade the resulting 401 to a login screen went blank). The actor’s other devices/sessions keep working. Legacy refresh tokens minted before chain_id existed 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 iteration when an actor has a matching token to revoke: both loops now snapshot list(<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_denied and no authorization code. The OAuth callback now detects spa_mode from the OAuth state and bounces the error back to the SPA’s callback URL as error / error_description query params (validated with is_safe_spa_redirect, falling back to the configured root), so the app can show a friendly message instead of aw-root-failed.html / a 500. Apple’s response_mode=form_post callback 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 until maximum 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_tool has long accepted title and output_schema, but the /mcp tools/list builder dropped both when constructing the tool definition. Hosts that key on the MCP 2025-11-25 Tool.title (e.g. Claude Code’s tool-permission dialog) therefore saw only the internal name, and clients supporting structured output had no schema to validate against. tool_def now includes title and outputSchema when set on the decorator.

  • MCP ``tools/call`` formatting parity between Flask and FastAPI: the sync (MCPHandler) and async (AsyncMCPHandler) handlers now share a single format_call_tool_result implementation, so both frameworks format tool-call responses identically.

  • A leftover debug log in the actor-by-creator lookup (db.postgresql.actor and db.dynamodb.actor) that was emitted at WARNING on every match — a hot path during OAuth sign-in — is now DEBUG with 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 ActingWebMCPServer was never wired to serve requests). Removing it also drops ~a dozen transitive packages (the SSE/streaming + jsonschema + pydantic-settings stack). The mcp install 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_template helper moved to actingweb/mcp/uri.py as match_uri_template.

SECURITY

  • SPA OAuth open-redirect / session-token leak fixed. The redirect_uri passed to POST /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 new Config.spa_redirect_origins (for split-domain SPA deployments). An off-origin redirect_uri is rejected with 400 at 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-controlled redirect_uri could 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 the PyJWKClient / JWKS decode path, which is exactly the flow the new native id_token validation (Apple / Google) relies on. cryptography >= 48.0.1 (was >= 43.0), requests >= 2.32.4 (was >= 2.20; clears the proxy-Authorization and .netrc credential leaks and the verify=False persistence bug), and oauthlib >= 3.2.2 (was >= 3.2; clears the redirect_uri DoS 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 TemplateResponse calls in fastapi_integration.py to use the new Starlette 1.0 signature TemplateResponse(request, name, context=...) instead of the deprecated TemplateResponse(name, {"request": request, ...}) convention. This fixes an unhashable 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 to provider_token_data to 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/config returns 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_required hook 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 the oauth2 module 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=[...]). New TrustManager methods: 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). New TrustManager methods: 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 with ActingWebApp.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 with ActingWebApp.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, and FanOutManager modules handle incoming subscription callbacks with automatic sequence validation, gap detection, resync triggering, and back-pressure support.

  • Pull-Based Subscription Sync: New SubscriptionManager.sync_subscription() and sync_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 SubscriptionSuspension database 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_deleted lifecycle event triggered when an inbound subscription is deleted, receiving actor, peer_id, subscription_id, subscription_data, and initiated_by_peer flag.

  • Inbound Subscription Query: New SubscriptionManager.get_subscriptions_from_peer(peer_id) for querying inbound subscriptions (peers subscribed to our data). Complements existing get_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 sync MCPHandler.

  • 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 in actingweb.db.protocols provide full type safety and IDE support.

  • Attribute List Storage: New ListAttribute and AttributeListStore classes for storing distributed lists in attribute buckets (not exposed via REST API). Same semantics as ListProperty/PropertyListStore but stored in attributes, bypassing the 400 KB property size limit.

  • List Metadata Access: New get_metadata() method on both ListProperty and ListAttribute exposes 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 timeout parameter on the AwProxy constructor. 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 the X-Request-ID header 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/token now accepts grant_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_deleted lifecycle 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() and get_all_properties() methods on RemotePeerStore for 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: GET requests on list properties now accept ?format=short to 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/supported automatically advertises the permissioncallback and permissionquery capability 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.store in 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() on OAuth2Authenticator (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 (TrustPermissions storage, PeerPermissions callbacks, AccessControlConfig.add_trust_type()). Shorthand format defaults to read-only operations.

SECURITY

  • GitHub Email Verification: _get_github_primary_email() now requires both primary and verified flags when selecting the email for actor linking. Previously, an unverified primary email was accepted, which could allow account-linking attacks via the GitHub /user/emails API.

  • 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_value B-tree index on the properties.value column, which blocked storage of values larger than ~2700 bytes (e.g., embeddings, JSON blobs). The property_lookup table handles reverse-index lookups for properties that require value-based search. Includes Alembic migration c3d4e5f6a7b8 to drop the index on existing databases.

  • MCP OAuth Flow Verified Email Requirement: The MCP OAuth flow now returns a clear invalid_grant error 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 receive subtarget="myList" instead of subtarget="list:myList"; applications that were stripping this prefix can remove that workaround.

  • Fix FastAPI double logout invocation: The FastAPI /oauth/logout handler was calling the underlying logout handler twice when a Bearer token was present alongside an oauth_token cookie, 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 AwProxy resource methods (get_resource(), create_resource(), change_resource(), delete_resource(), and async variants) now return structured error dicts with code and message keys for all error conditions, including when the peer returns a string-typed error field. 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 and sync_subscription_callbacks config 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 function

    • Improved logging with sequence numbers and peer IDs for callback debugging

  • Subscription Sequence in GET Response: Added sequence field 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(), and execute_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 via asyncio.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_PROPERTIES

    • Automatic cleanup: lookup entries deleted with properties/actors

    • PostgreSQL foreign key CASCADE for automatic orphan cleanup

  • actingweb.db.dynamodb.property_lookup module with PropertyLookup model and DbPropertyLookup class

  • actingweb.db.postgresql.property_lookup module with DbPropertyLookup class

  • actingweb.interface.ActingWebApp.with_indexed_properties() builder method for configuration

  • actingweb.interface.ActingWebApp.with_legacy_property_index() builder method to control mode

  • PostgreSQL migration 70d60420526_add_property_lookup_table.py for lookup table schema

  • Comprehensive test suite (tests/test_property_lookup.py) with 26 tests for both backends

  • Documentation in docs/quickstart/configuration.rst with migration guide and best practices

  • Native 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(), and execute_lifecycle_hooks_async()

    • Async handler variants: AsyncMethodsHandler and AsyncActionsHandler with *_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 def for 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/index

  • DbProperty.set() now syncs lookup entries for indexed properties

  • DbProperty.delete() now removes lookup entries for indexed properties

  • DbPropertyList.delete() now cleans up all lookup entries when deleting actor properties

  • FastAPI integration now preferentially uses async handler variants (AsyncMethodsHandler, AsyncActionsHandler) for methods and actions endpoints

  • Synchronous hook execution methods (execute_*_hooks()) now support async hooks via asyncio.run() fallback

  • Handler factory (get_handler_class()) now supports creating async handler variants based on framework preference

v3.8.3: Jan 12, 2026

FIXED

  • Fixed Flask integration TypeError in cookie handling by extracting cookie name as positional argument instead of kwarg

  • Fixed missing subscription callbacks when deleting properties via WWW handler with ?_method=DELETE

  • Fixed 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>/methods and GET /<actor_id>/actions now 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 deleted

  • OAuth 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 scenarios

  • Atomic Token Marking: Added try_mark_refresh_token_used() method in OAuth2SessionManager that 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_list cache after creating new subscription, ensuring register_diffs() immediately sees newly created subscriptions for callback delivery

  • Trust 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() flow

  • PostgreSQL Backend: Fixed SQL queries to quote desc column as reserved keyword (PostgreSQL compatibility)

  • PostgreSQL Backend: Fixed Attributes class to handle None values from PostgreSQL for non-existent attribute buckets

  • Database Backend Abstraction: Removed hardcoded DynamoDB imports in TrustManager and PermissionEvaluator to use configured database backend dynamically

  • Test Fixtures: Fixed test_trust_manager_oauth mock to properly structure DbTrust module for compatibility with backend abstraction

ADDED

  • Migration Helper: Added scripts/migrate_db.py helper 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_dynamodb to hierarchical package structure actingweb.db.dynamodb for better organization

  • Installation Extras: Added optional dependency groups - pip install 'actingweb[postgresql]' or 'actingweb[dynamodb]' for backend-specific installations

  • Database Backend Selection: Environment variable DATABASE_BACKEND (or database parameter in ActingWebApp()) 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__ pattern

  • Logging Configuration: Added centralized logging configuration with configure_actingweb_logging() helper functions

  • Log 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.postgresql package 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 backends

  • Protocol compliance tests to ensure both backends implement the same interface

  • scripts/migrate_dynamodb_to_postgresql.py - Data migration tool with export, import, and validate operations

  • Performance benchmarks (tests/performance/) for comparing backend performance

  • Comprehensive 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 recommendations

  • GitHub Actions matrix testing for both DynamoDB and PostgreSQL backends

  • Backend-specific pytest markers (@pytest.mark.dynamodb, @pytest.mark.postgresql)

  • actingweb.logging_config module with production/development/testing configuration helpers

  • Performance-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 ActorInterface instances

  • Trust Lifecycle Hooks: Added trust_initiated hook - fires when actor initiates trust request to peer (outgoing)

  • Trust Lifecycle Hooks: Added trust_request_received hook - fires when actor receives trust request from peer (incoming)

  • Trust Lifecycle Hooks: Added trust_fully_approved_local hook - fires when THIS actor approves, completing mutual trust

  • Trust Lifecycle Hooks: Added trust_fully_approved_remote hook - 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=None now returns all subscriptions, callback=False returns inbound subscriptions, callback=True returns outbound subscriptions

  • Trust Deletion Hook: Enhanced trust_deleted lifecycle hook to include relationship and trust_data parameters for consistency with trust_approved hook

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 and with_web_ui() configuration.

  • Browser Redirect to /login: Unauthenticated browser requests to /<actor_id> now redirect to /login for 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>/app instead of /<actor_id>/www.

  • Integration tests for actor root endpoint content negotiation and redirect behavior.

CHANGED

  • OAuth2 callback handler now respects config.ui setting - redirects to /<actor_id>/app when 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 www callback 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 /methods and /actions endpoints for creator, friend, partner, and admin trust types.

  • Added template_name attribute to AWResponse for 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/logout now delegates to the main /oauth/logout handler 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 path and samesite parameters for proper session cookie behavior across browser security policies

  • Simplified 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 patterns AND excluded_patterns arrays by default - base security exclusions (private/, security/, oauth_*) can no longer be accidentally cleared by individual trust relationship overrides

  • Cleaned 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.rst for comprehensive migration guide

  • HTTP 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 listall endpoint now filters properties and list properties based on peer permissions - prevents unauthorized data exposure

  • Properties listall now includes list properties even when all regular properties are filtered by permissions

  • Subscription 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 with callable(x) for better type safety

ADDED

  • Permission Merge Control: Added merge_base parameter to merge_permissions() function - defaults to True for fail-safe union merging of patterns/excluded_patterns; set to False for explicit full override capability

  • Developer 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_root property

  • Wrapper Classes: Added SubscriptionWithDiffs wrapper providing clean access to subscription data and diffs

  • Async Authentication: Added async versions of authentication methods (check_token_auth_async(), check_and_verify_auth_async()) to avoid blocking event loop during OAuth2 validation

  • OAuth2 Token Heuristic: Added Auth._looks_like_oauth2_token() method to avoid unnecessary network calls for non-OAuth tokens

  • List 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-parallel

  • Added pytest-xdist dependency for parallel test execution

  • GitHub 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.rst and updated docs/reference/security.rst cheatsheet

  • Architecture: 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_approved lifecycle 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_deleted lifecycle hook trigger in trust DELETE handler.

ADDED

  • ACL Rules for Custom Trust Types: add_trust_type() now accepts an acl_rules parameter to specify HTTP endpoint access permissions. This enables custom trust types (like subscriber) 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 read permission on are included in callbacks (fail-closed design)

  • NEW ENDPOINT: Added /trust/{relationship}/{peerid}/shared_properties endpoint for discovering properties available for subscription

  • BREAKING: 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 httpx as a new dependency for async HTTP client operations

CHANGED

  • Subscription handlers now use unified permission evaluator (evaluate_property_access) instead of legacy check_authorisation

  • Permission changes made after subscription creation now affect subsequent callbacks (dynamic permission enforcement)

  • Actor callback_subscription method 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._hooks was never set, causing actor_created and other lifecycle hooks to be silently ignored during OAuth-based actor creation

  • Added comprehensive regression tests for OAuth2 lifecycle hook integration

CHANGED

  • ActingWebApp now automatically attaches HookRegistry to Config object’s _hooks attribute in get_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 bypassed

  • Fixed FastAPI cookie setting using key parameter instead of name (FastAPI/Starlette API difference)

  • Fixed SPA refresh token cookie not being stored by browser (changed path="/" and samesite="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 field

  • Removed sensitive token values from debug log messages (security improvement)

  • Fixed pytest marker registration for integration tests (added integration marker)

CHANGED

  • SPA OAuth2 authorize endpoint only includes trust_type in 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 - Returns 200 OK with [] when no trust relationships exist (was 404)

  • GET /trust?relationship=<type> - Returns 200 OK with [] when no matches (was 404)

  • GET /properties - Returns 200 OK with {} when no properties exist (was 404)

  • GET /subscriptions - Returns 200 OK with {"id": ..., "data": []} when no subscriptions (was 404)

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 behavior

  • Added listproperties option tag to ActingWeb specification for list property support

  • Added comprehensive List Properties section to docs/actingweb-spec.rst documenting ordered collections

  • Documented 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=true documentation to reference listproperties option tag

ADDED

  • SPA mode support in OAuth2 callback handler (spa_mode=true in state parameter returns JSON instead of redirect)

  • JSON API responses in email verification handler (based on Accept: application/json header)

  • GET /{actor_id}/meta/trusttypes endpoint for trust type enumeration

  • GET/PUT /{actor_id}/properties/{name}/metadata endpoint for list property metadata

  • Factory JSON API: GET /?format=json or Accept: application/json returns OAuth configuration for SPAs

  • New 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:

  1. User Login (ActingWeb as OAuth client to Google/GitHub): No trust relationship created, no trust_type needed

  2. 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/list responses in MCP handler

  • Tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are now properly included when decorators define them

  • This 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/authorize POST with selected trust_type before redirecting to provider

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

  • Added security tests in tests/integration/test_oauth2_security.py

  • Added lifecycle hooks: email_verification_required and email_verified

  • Added email verification endpoint: /<actor_id>/www/verify_email

  • Added verified emails dropdown for GitHub OAuth2 (fetches verified emails via GitHub API)

  • Added provider ID support (stable identifiers like google:sub or github:user_id) as alternative to email addresses

  • Added 32-byte cryptographic verification tokens with 24-hour expiry

  • Added security logging for all authorization violations

Trust Relationship Enhancements

  • Added last_accessed and last_connected_via fields to trust relationships for tracking connection activity

  • Handler 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/register and /oauth/token endpoints

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.json for consistent type checking

  • VSCode Integration: Updated .vscode/settings.json for optimal pylance and ruff integration

  • Test 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 cryptography 45.0.6 → 46.0.3, fastapi 0.116.1 → 0.120.0, mcp 1.12.4 → 1.19.0, pydantic 2.11.7 → 2.12.3, pytest-cov 6.2.1 → 7.0.0, ruff 0.12.8 → 0.14.2, and 30+ other dependency updates

v3.3: Oct 4, 2025

BREAKING CHANGES

Legacy OAuth System Removed

  • Removed legacy OAuth class and related third-party service authentication

  • Removed /<actor_id>/oauth endpoints that used legacy OAuth

  • Removed legacy OAuth methods from Auth class (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() with actor.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 global actingweb.__version__

  • Fixed error in DbPropertyList when properties table was missing in DynamoDB

  • Fixed trustee_root JSON to return stored value instead of input parameter

  • Fixed missing trustee_root in actor creation via REST API

  • Fixed handling of POST to /<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 triggered

  • Devtest proxy: added Basic-auth fallback (trustee:<peer passphrase>) when Bearer trust requests to peer /properties endpoints receive 302/401/403, avoiding OAuth2 redirects during testing

  • Fixed 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_via to properly distinguish between MCP and regular OAuth2 flows

  • Fixed 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_via field being lost between database save and retrieval in trust relationship management

  • Added 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 root

  • Refactored 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 operations

  • Modified /meta/actingweb/supported to dynamically include feature tags based on available system capabilities

  • Properties, 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.1 with requests ^2.31.0 for 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 files

  • Added 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 PRs

  • Added make test-integration target for running integration tests locally

  • Added 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_clients parameter in @mcp_tool decorator to restrict tool access by client type

  • Support for client_descriptions parameter in @mcp_tool decorator for client-specific tool descriptions

  • Client-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, and ServiceRegistry classes

  • Added automatic token management and refresh for third-party services

  • Added actor.services.get() interface for accessing authenticated service clients

  • Added service OAuth2 callback endpoints: /{actor_id}/services/{service_name}/callback

  • Added service revocation endpoints: DELETE /{actor_id}/services/{service_name}

  • Added comprehensive documentation in docs/service-integration.rst

  • Integrated 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 + authorization

  • Added authenticate_actor() method returning AuthResult for more granular control

  • New interface reduces boilerplate from 6-8 lines to 2-3 lines per handler method

  • Maintains full compatibility with existing init_actingweb() usage

  • Automatic 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 response

    • PUT /trust/{relationship}/{peerid} - Update permissions alongside trust relationship properties

    • GET /trust/{relationship}/{peerid}/permissions - Dedicated permission management endpoint

    • PUT /trust/{relationship}/{peerid}/permissions - Create/update permission overrides

    • DELETE /trust/{relationship}/{peerid}/permissions - Remove permission overrides

  • trustpermissions feature tag automatically included in /meta/actingweb/supported when permission system is available

  • Transparent 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 schemes

  • Backward 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 startup

  • Comprehensive 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 OAuth2ClientManager interface for creating, listing, validating, deleting clients, and regenerating client secrets

  • Client secret regeneration with verification, audit timestamp (secret_regenerated_at), and formatted display values

  • Generate access tokens via client-credentials flow directly from OAuth2ClientManager.generate_access_token()

OAuth2 Authorization Server

  • Added support for client_credentials grant type with token issuance and discovery updated (grant_types_supported)

  • Added trust_type and actor_id to client registration/discovery responses; improved secret validation diagnostics

  • Added client deletion capability to MCP client registry

MCP Integration

  • Captures and caches MCP clientInfo during initialize; persists to trust relationship after OAuth2 callback

  • Populates 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_context module providing structured request context for hook functions

  • RuntimeContext class with type-safe context classes: MCPContext, OAuth2Context, WebContext

  • get_client_info_from_context() helper function for unified client detection across all context types

  • Support for custom context types via set_custom_context() and get_custom_context() methods

  • Request-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, and url

  • Trust page displays registered OAuth2 clients (name, trust type, created time, status)

  • Trust creation form supports selecting trust type; consistent form_action and redirects

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