feat(etl-uvicorn): own the /invoke transport and invocation-context model (0.1.0) - #74
Draft
CyMule wants to merge 7 commits into
Draft
feat(etl-uvicorn): own the /invoke transport and invocation-context model (0.1.0)#74CyMule wants to merge 7 commits into
CyMule wants to merge 7 commits into
Conversation
Moves the ASGI middleware, the /metadata capability route and the request-scoped binding out of utic-invocation-settings and into this package, as unstructured_platform_plugins.invocation_settings. The split follows what the two halves actually are. The contract — which keys carry settings, how a sealed envelope is told from plaintext, what an absent field is allowed to mean — stays in the library: the absence rule is the tenant-confusion vector, and it belongs next to the crypto it governs and the threat model that describes it. Buffering a request body and registering a route do not. Two things this buys immediately: - No more duck-typing. Living in the library forced the middleware to reach into `app.router.routes` through getattr chains to avoid importing Starlette. Here fastapi is already a dependency, so route eviction and the ASGI signature are typed against the real thing. - The library goes back to cryptography + pydantic with no framework test dependencies at all; its packaging suite asserts that against the built wheel. Requires utic-invocation-settings >=0.4.0 for resolve_invocation_settings, http_status_for and the contract constants. Tests: 109 passed (84 + 25 ported transport tests), ruff clean.
A plugin consuming current_invocation_settings() builds its handler per distinct settings payload instead of once at boot, and construction typically does network work (model resolution, prechecks). This gives that pattern one home next to the accessor that creates the need: settings_cache_key digests the canonical settings JSON so secret-bearing payloads are never raw keys, and SettingsScopedCache memoizes derived state bounded by both size and age — age matters because state built from since-rotated credentials must not outlive them on a quiet pod. Stdlib-only, so the package's dependency set is unchanged.
An invocation_context with an unreadable schema_version is deployment skew between platform components; answering 422 let an upstream blame classifier pin it on the caller. Context failures now take their status from http_status_for like settings failures already did: malformed fields stay the caller's 422, version skew answers 500 with the class name only. Also documents the two capability tiers on /metadata: the unconditional strings are transport-level facts the middleware makes true for every wrapped app; invoke_with_sealed_dag_node_settings is the consumption claim and stays a per-plugin opt-in.
…ding it in status codes Status codes carry transport semantics for the immediate caller and cannot also carry business blame: a 422 for a malformed reserved field (composed by the platform) and a 422 for a customer's unreadable file are different faults wearing the same number. Failure responses now say whose fault it is explicitly: - the invoke envelope gains an optional `blame`, set to "user" only when the plugin raised the UserError family — a fault in something the customer owns. Absent means not-the-customer's: an orchestrator must never infer customer fault from the status class alone. - middleware error bodies carry the invocation-settings taxonomy `reason` code alongside `detail`, so an orchestrator can recognize a platform-composed payload failure whatever status answered the hop.
CyMule
force-pushed
the
feat/etl-uvicorn-invocation-settings
branch
from
August 4, 2026 16:01
3159a2c to
2d66570
Compare
…tus spelling unstructured_platform_plugins.invocation_context holds the /invoke identity contract: InvocationContext, extract_context, dimensions, the reserved context key, the dimension fields, the supported versions, and UnsupportedContextVersionError. The context is protocol identity - no crypto, no secrets - so it ships with the plugin protocol; its errors subclass the shared InvocationSettingsError taxonomy so hosts classify context failures with the same reason/blame machinery as settings failures. http_status_for - the HTTP spelling of the library's normative blame -> status rule - lives with the middleware that emits the responses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This package now owns the transport for the reserved
/invokefields and the identity model they carry:unstructured_platform_plugins.invocation_settingsholds the ASGI middleware, the/metadatacapability route, the request-scoped binding, and
http_status_for— the HTTP spelling of thelibrary's normative
blame→ status rule;unstructured_platform_plugins.invocation_contextholds the
invocation_contextcontract (InvocationContext,extract_context,dimensions,RESERVED_CONTEXT_KEY,DIMENSION_FIELDS,SUPPORTED_CONTEXT_VERSIONS,UnsupportedContextVersionError).It sits on
utic-invocation-settings >=0.4.0(Unstructured-IO/utic-public-libs#67), which owns the settings contract — which key carries
settings, how a sealed envelope is told from plaintext, and what an absent field is allowed to
mean. Nothing about the sealed-settings wire format is decided in this repository.
Why the split lands this way
tenant-confusion vector. It belongs beside the crypto and the threat model, not here.
invocation_contextmodel is/invokeprotocol identity — no crypto, no secrets — so itlives with the plugin protocol this package defines. Its errors subclass the shared
InvocationSettingsErrortaxonomy, so hosts classify context failures with the samereason/blamemachinery as settings failures. This module is the public home for the surfaceutic-invocation-settings 0.2.xcarried and its0.3.0removed.blamebelonghere, where a web framework is already a hard dependency and the middleware emits the responses.
cryptography+pydanticwith no framework test dependencies.What the wrapper does
wrap_in_fastapi/generate_fast_apiinstall the middleware and register/metadataatconstruction. Repeated installation is safe: the middleware installs once, and the last
/metadataregistration wins regardless of route order.invoke_with_sealed_dag_node_settings(and--sealed-dag-node-settings) advertises thecapability. Opt-in because it asserts the wrapped function consumes
current_invocation_settings(), not merely that the host can resolve it.invoke_funccopies the current context into the executor thread.run_in_executordropscontextvars, so a sync plugin would otherwise see request-scoped bindings as absent and could
take an unintended fallback path.
SettingsScopedCache+settings_cache_keygive per-invoke consumers one home for derivingstate (clients, models, handlers) from the settings the middleware binds: memoized on a digest
of the canonical settings JSON (secret-bearing payloads are never raw keys), bounded by both
size and age so state built from since-rotated credentials cannot outlive them. Stdlib-only.
Two behaviours worth review
asyncio.to_thread). A cold resolve is an RSA unwrap of~2.2 ms and this middleware fronts every invoke on the pod.
The invoke envelope gains an optional
blame, set to"user"only when the plugin raised theUserErrorfamily — a fault in something the customer owns (their file, their credentials,their provider). Absent means not-the-customer's: an orchestrator must never infer customer
fault from the status class, which also carries transport semantics. Middleware error bodies
carry the invocation-settings taxonomy
reasoncode alongsidedetailfor the same purpose —a platform-composed payload failure is recognizable whatever status answered the hop. Both
fields are inert until an orchestrator consumes them (Unstructured-IO/platform-plugins#1925).
blametaxonomy viahttp_status_for, not a flat 500. Onlya caller-fixable fault answers 422; sealing drift, an envelope for another recipient and a broken
local mount are all 5xx, which keeps the controller's blame classification off the customer.
Responses carry the error's class name and never its message, which can embed request-controlled
values.
Impact
utic-invocation-settings >=0.4.0,<1.0.0Validation
145 passed: 84 pre-existing tests unchanged (installation is inert for existing plugins) + 25 transport tests (sealed/composite/plaintext/absent resolution, blame-mapped statuses, the 413 cap, disconnect replay,/metadatalast-call-wins, sync-executor context propagation, secrets never in logs or response text) + 12 settings-scoped cache tests (key hygiene, TTL and LRU bounds) + 4 blame/reason declaration tests + 20 invocation-context and status-mapping tests (extraction fail-closed rules, version gate, dimension policy,http_status_for)Draft follow-ups
uv.lockonceutic-invocation-settings 0.4.0is published