Skip to content

feat(etl-uvicorn): own the /invoke transport and invocation-context model (0.1.0) - #74

Draft
CyMule wants to merge 7 commits into
mainfrom
feat/etl-uvicorn-invocation-settings
Draft

feat(etl-uvicorn): own the /invoke transport and invocation-context model (0.1.0)#74
CyMule wants to merge 7 commits into
mainfrom
feat/etl-uvicorn-invocation-settings

Conversation

@CyMule

@CyMule CyMule commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

This package now owns the transport for the reserved /invoke fields and the identity model they carry:
unstructured_platform_plugins.invocation_settings holds the ASGI middleware, the /metadata
capability route, the request-scoped binding, and http_status_for — the HTTP spelling of the
library's normative blame → status rule; unstructured_platform_plugins.invocation_context
holds the invocation_context contract (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

  • The absence rule (absent may fall back, anything that arrived must open or fail) is the
    tenant-confusion vector. It belongs beside the crypto and the threat model, not here.
  • The invocation_context model is /invoke protocol identity — no crypto, no secrets — so it
    lives with the plugin protocol this package defines. Its errors subclass the shared
    InvocationSettingsError taxonomy, so hosts classify context failures with the same
    reason/blame machinery as settings failures. This module is the public home for the surface
    utic-invocation-settings 0.2.x carried and its 0.3.0 removed.
  • Body buffering, 413 limits, replay, route registration, and the HTTP spelling of blame belong
    here, where a web framework is already a hard dependency and the middleware emits the responses.
  • The library stays cryptography + pydantic with no framework test dependencies.

What the wrapper does

  • wrap_in_fastapi / generate_fast_api install the middleware and register /metadata at
    construction. Repeated installation is safe: the middleware installs once, and the last
    /metadata registration wins regardless of route order.
  • Opt-in invoke_with_sealed_dag_node_settings (and --sealed-dag-node-settings) advertises the
    capability. Opt-in because it asserts the wrapped function consumes
    current_invocation_settings(), not merely that the host can resolve it.
  • invoke_func copies the current context into the executor thread. run_in_executor drops
    contextvars, so a sync plugin would otherwise see request-scoped bindings as absent and could
    take an unintended fallback path.
  • SettingsScopedCache + settings_cache_key give per-invoke consumers one home for deriving
    state (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

  • Resolution runs off the event loop (asyncio.to_thread). A cold resolve is an RSA unwrap of
    ~2.2 ms and this middleware fronts every invoke on the pod.
  • Failure responses declare whose fault it is, instead of encoding blame in status codes.
    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 (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 reason code alongside detail for 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).
  • Failures map through the shared blame taxonomy via http_status_for, not a flat 500. Only
    a 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

  • requires utic-invocation-settings >=0.4.0,<1.0.0
  • Python 3.10 users remain on the 0.0.x release line

Validation

  • 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, /metadata last-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)
  • Ruff clean on all changed files

Draft follow-ups

  • regenerate uv.lock once utic-invocation-settings 0.4.0 is published
  • update CI, release, and dependency-compilation configuration to use Python 3.11 as the minimum

CyMule added 2 commits July 31, 2026 16:28
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.
@CyMule CyMule changed the title feat(etl-uvicorn): install invocation-settings handling (0.1.0) feat(etl-uvicorn): own the /invoke transport for invocation settings (0.1.0) Aug 3, 2026
CyMule added 4 commits August 3, 2026 16:25
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
CyMule force-pushed the feat/etl-uvicorn-invocation-settings branch from 3159a2c to 2d66570 Compare August 4, 2026 16:01
…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.
@CyMule CyMule changed the title feat(etl-uvicorn): own the /invoke transport for invocation settings (0.1.0) feat(etl-uvicorn): own the /invoke transport and invocation-context model (0.1.0) Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant