Commit Graph

8 Commits

Author SHA1 Message Date
Nicolò Boschi ac41cee604 feat(extensions): add hindsight-extensions registry and unbundle Supabase (#3988)
* feat(extensions): add hindsight-extensions registry and unbundle Supabase

Extensions were only ever bundle-able or nothing: shipping one meant putting
it in `hindsight_api.extensions.builtin`, where it becomes maintainer-owned
forever, lands in every image, and — because `extensions/__init__.py` eagerly
re-exported every implementation — drags its dependencies into core's import
graph. That pipe is why a third-party IdP's JWT client was a direct dependency
of every Hindsight install.

Add `hindsight-extensions/` as the registry for extensions distributed
separately from the server. Its README is the contract: slots and how config
env vars map onto them, how to write an extension, the package layout and
naming (`hindsight-extensions/<name>/` -> `hindsight-ext-<name>` ->
`hindsight_ext_<name>`), and Docker packaging.

Move `SupabaseTenantExtension` there as the first entry, published as
`hindsight-ext-supabase-tenant`. All 54 of its tests move with it, plus two new
ones asserting the documented `hindsight_ext_supabase_tenant:...` env value
actually resolves through `load_extension`.

Two decisions worth their comments:

- The extension does NOT declare `hindsight-api-slim` as a runtime dependency.
  The server is the host process that imports it, not something it installs;
  declaring it would let `pip install` of an extension silently move the server
  version underneath a running deployment. It is a dev extra, resolved from the
  local checkout via `tool.uv.sources` (dev-only metadata, verified absent from
  the built wheel).
- The Docker example installs with `uv pip install --python
  /app/api/.venv/bin/python`, matching docker-compose/custom-models: the image's
  venv was created by `uv sync` and ships no `pip`, so a bare `pip install`
  lands in user site-packages and is invisible to the server.

Core changes:

- `extensions/__init__.py` and `builtin/__init__.py` export interfaces only.
  Nothing needed the concrete re-exports — the loader imports by path — and
  dropping them is what lets an extension have optional dependencies at all.
- `builtin/supabase_tenant.py` stays for one minor release as a module whose
  `__getattr__` raises the migration instructions. `load_extension` wraps a
  missing *attribute*, not a failed import, so the ImportError propagates with
  its message intact instead of surfacing as "class not found".
- Drop the direct `PyJWT[crypto]` dependency: no core module imports `jwt` any
  more. Note this does not shrink the install — `mcp` pulls pyjwt transitively
  and `cryptography` is already pinned directly — so the win here is ownership
  and import graph, not bytes.

Locks are not checked in for extensions: `tool.uv.sources` pins the whole
api-slim tree, so every core dependency bump would leave them stale. CI runs
`uv sync --extra dev` and retriggers on `core` changes, since these tests run
against the server's interfaces.

Docs point at the registry rather than restating it, and the Deploying section's
Docker recipe was replaced — it named an image (`vectorize/hindsight-api`) and a
PYTHONPATH volume-mount pattern that no longer exist.

Also includes two one-line generated-file syncs in skills/hindsight-docs
(quickstart, installation) that were already stale on main; regenerating the
docs skill picks them up.

* refactor(extensions): ship extensions by image, drop the compat shim

Follow-up on review. Three changes to how an extension is distributed:

- Delete `builtin/supabase_tenant.py`. An install pinned to the old path now
  fails at startup with ModuleNotFoundError rather than a guided message. The
  docs carry the migration instead.
- Extensions are not published to PyPI. There is no wheel, no version and no
  release step: the unit of distribution is an image built on top of Hindsight
  that installs the extension's dependencies and copies the package onto
  PYTHONPATH. That drops the whole "declare hindsight-api-slim only as a dev
  extra" problem — nothing resolves dependencies against a running server any
  more.
- The pyproject is now test-harness only (`package = false`, no build backend,
  no distribution metadata), and says so in a comment so nobody re-adds
  packaging to it.

Docs say 0.9.3, not 0.10.

Since the Dockerfile is now the distribution mechanism rather than an example,
CI builds it — its final `import` step is the only thing proving the extension
is reachable from the interpreter the server actually runs. It builds against
`:latest-slim` via a HINDSIGHT_IMAGE build arg to keep the pull cheap.

Verified against the real image, not just locally:

  docker build -f hindsight-extensions/supabase-tenant/Dockerfile \
    --build-arg HINDSIGHT_IMAGE=ghcr.io/vectorize-io/hindsight:latest-slim ...
  -> load_extension('TENANT', TenantExtension) inside the container returns
     SupabaseTenantExtension with its config resolved from the env vars.

Worth noting from that build: `uv pip install 'PyJWT[crypto]' httpx` reports
"Checked 2 packages" — both are already in the base image transitively. The
line stays because the extension should pin what it imports rather than rely on
the server's transitive tree, but it costs nothing today.

56 extension tests pass; the 3 remaining core tests (which assert no
implementation is re-exported and no core module imports jwt) pass.

* fix(tests): import ApiKeyTenantExtension from its module, not the package

Dropping the concrete re-exports from `hindsight_api.extensions` broke
`tests/test_extensions.py`, which imported `ApiKeyTenantExtension` from the
package inside a multi-line parenthesised import. A collection ImportError
fails the whole shard, which is why all three test-api shards and all six LLM
acceptance jobs went red at once on the previous push.

I'd checked for this with a single-line grep, which cannot see a name inside a
parenthesised import list. Re-checked with an AST scan over every package in
the repo (this was the only occurrence) and by collecting the full suite:
7780 tests collect clean.
2026-09-01 14:47:37 +02:00
rares-mollie 27d4386deb feat(api): forward allowlisted request headers to extensions (#3428)
* feat(api): forward allowlisted request headers to extensions

A custom TenantExtension only ever sees the Authorization header, via
RequestContext.api_key. That is not enough for deployments behind an
authenticating proxy that presents a single shared identity to Hindsight
and carries the per-caller identity in a separate header: every request
looks identical to the extension, so it cannot attribute actions to the
actual caller or enforce per-caller rules.

Add HINDSIGHT_API_EXTENSION_PASSTHROUGH_HEADERS, a comma-separated
allowlist of header names copied into a new RequestContext.extra_headers
field, keyed by lower-cased name. Both transports populate it: the HTTP
dependency and the MCP middleware, the latter resolving headers before
authentication so authenticate_mcp() can read them, and threading them
through a contextvar so per-tool-call contexts carry them too.

Opt-in and default-off: unset means extra_headers stays empty, so nothing
changes for existing deployments and no header data reaches extension code
unless an operator asks for it. The setting is server-level only and
deliberately not per-bank configurable, so a tenant cannot widen the set
of headers its own extension sees.

RequestContext's docstring already anticipated this ("can be extended to
include additional context like headers, tokens, user info"); this fills
it in and documents the mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(api): harden passthrough-header collection and regenerate docs skill

Review follow-ups on the allowlisted-header forwarding:

- The MCP middleware decoded every header of every request as UTF-8 once the
  allowlist was non-empty, so a single obs-text byte anywhere (a latin-1
  User-Agent is enough) raised UnicodeDecodeError and failed the request.
  Header bytes are latin-1 on the wire; decode them that way.
- The two transports disagreed on a duplicated header: Starlette's Headers
  returns the first copy, the middleware's dict comprehension kept the last.
  For a header carrying caller identity that decides whether a spoofed copy
  wins. Neither answer is safe, so a duplicated header is now dropped with a
  warning on both transports — the extension sees nothing and fails the
  request instead of silently trusting one of the copies.
- Both now share collect_passthrough_headers(), which takes raw ASGI header
  pairs (Starlette exposes them as request.headers.raw), so decoding,
  case-folding and the duplicate rule cannot drift apart again.
- get_current_extra_headers() returns a copy, so one tool call mutating
  extra_headers cannot change what the next one sees.
- MCPToolsConfig.extra_headers_resolver moved to the end of the dataclass; it
  had been inserted mid-list, shifting the meaning of positional construction.
- Regenerate skills/hindsight-docs (verify-generated-files was failing on the
  un-regenerated docs), and document the duplicate rule plus the fact that
  deferred work carries no headers.

Tests: duplicate handling and non-UTF-8 header bytes on both transports, the
shared collector's rules, contextvar copying, and an end-to-end check that the
headers reach an OperationValidatorExtension hook.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-08-13 14:07:48 +02:00
Evo 9bde15331e docs: document MCP trace and precheck content length (#2264) 2026-06-17 15:19:20 +02:00
Nicolò Boschi f890479705 feat(worker): DeferOperation exception for extension-driven requeue (#1105)
Extensions that need to apply backpressure (rate-limited upstream,
quota window not yet open, dependency warming up) can now raise
DeferOperation(exec_date, reason) from any task-handler hook to
requeue the operation for a future time, without counting as a retry.
Unlike RetryTaskAt this does not increment retry_count or write
error_message. The poller already filters claim_batch by next_retry_at,
so no migration is needed.

Documented as worker-only — raising it from validate_recall /
validate_reflect in synchronous HTTP request paths will surface as
a 500 since there is no queue to defer to.
2026-04-16 18:36:32 +02:00
Nicolò Boschi 15ea23d5d6 feat: introduce hindsight-api-slim and hindsight-all-slim packages (#560)
* feat: introduce hindsight-api-slim and hindsight-all-slim packages

Closes #552

- Move all source code from hindsight-api/ to new hindsight-api-slim/
- hindsight-api-slim has heavy ML deps (torch, sentence-transformers,
  transformers, einops, flashrank, mlx, mlx-lm, safetensors) and
  pg0-embedded as optional extras: [local-ml], [embedded-db], [all]
- hindsight-api becomes a zero-code meta-package depending on
  hindsight-api-slim[all] for full backward compatibility
- Add hindsight-all-slim meta-package: hindsight-api-slim + client + embed
- hindsight-all updated to depend on hindsight-api-slim[all]
- pg0.py: lazy-import pg0 with clear ImportError pointing to [embedded-db]
- Dockerfile: replace sed hack with proper uv sync --extra flags
- Update release.yml, test.yml, lint.sh, release.sh, CLAUDE.md and
  all path references throughout the repo

* refactor: rename hindsight/ directory to hindsight-all/

* docs: document hindsight-api-slim and hindsight-all-slim package variants

Add package variants table and extras explanation to installation.md

* docs: remove emojis from installation.md, use professional tone

* docs: link Docker slim variant to pip package variants section

* docs: consolidate Docker image variants into single table

* ci: fix working-directory paths after package restructure

- Replace all hindsight-api → hindsight-api-slim in test.yml
- Replace hindsight → hindsight-all in test.yml
- Add --extra embedded-db to test-embed API install step

* ci: add local-ml and embedded-db extras to API sync steps

These extras were previously implicit in the old hindsight-api package
(which bundled everything). Now that hindsight-api-slim uses optional
extras, we must explicitly request local-ml and embedded-db in CI.

* ci: add API install step with embedded-db to test-embed smoke test

The smoke test starts hindsight-api as a daemon, which requires pg0-embedded.
Add a dedicated install step for hindsight-api-slim with embedded-db extra
so the daemon can start successfully.

* ci: remove --no-install-project when using optional extras

When --no-install-project is combined with --extra, the optional deps
are not installed because extras require the project to be active.
Remove --no-install-project from steps that need local-ml or embedded-db.

* ci: fix ordering of uv sync steps to preserve optional extras

When uv sync runs for a different workspace member, it removes optional
extras installed for other members. Fix by always running extra-requiring
API sync last, after other workspace member syncs.

Also remove --no-install-project from embedded-db sync in test-embed,
as --no-install-project prevents optional extras from being active.

* ci: add local-ml extra to test-embed API install for smoke test

The smoke test starts the full API server which needs sentence-transformers
for local embeddings (default provider). Add local-ml extra to the install.

* ci: simplify extras with --all-extras and add slim pip smoke test

- Replace explicit --extra local-ml --extra embedded-db with --all-extras
  for cleaner, more maintainable sync steps
- Add test-pip-slim job: tests hindsight-api-slim[embedded-db] without
  local ML models, using Cohere for embeddings/reranking (mirrors Docker
  slim smoke test approach)

* ci: simplify slim smoke test to health check only (mirrors Docker test)
2026-03-13 13:50:03 +01:00
DK09876 e407f4bc55 feat: add extension hooks for root routing and error headers (#470)
* feat: add OAuth extension hooks for MCP authentication

Add extension points in core that allow cloud extensions to support
OAuth 2.1 (RFC 9728 / RFC 7591) for MCP server authentication:

- HttpExtension.get_root_router() for well-known endpoint mounting
- AuthenticationError.headers for WWW-Authenticate propagation
- MCP middleware forwards auth error headers to clients

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: document get_root_router and AuthenticationError.headers

Add documentation for the new extension points introduced in the
OAuth extension hooks commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove OAuth-specific wording from extension docs

Make the AuthenticationError headers example generic instead of
OAuth-specific, since these are general-purpose extension hooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 10:50:03 +01:00
Nicolò Boschi a3a9d7b37d doc: prepare doc for 0.4.10 (#325)
* doc: prepare doc for 0.4.10

* fixe

* ci
2026-02-09 11:42:37 +01:00
Nicolò Boschi 26850a0156 doc: add documentation for extensions (#62)
* add doc for extensions

* add doc for extensions
2025-12-22 11:58:05 +01:00