mirror of
https://github.com/vectorize-io/hindsight.git
synced 2026-09-14 19:31:49 +08:00
perf/profiling-env
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.
|
||
|
|
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>
|
||
|
|
9bde15331e | docs: document MCP trace and precheck content length (#2264) | ||
|
|
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. |
||
|
|
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) |
||
|
|
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> |
||
|
|
a3a9d7b37d |
doc: prepare doc for 0.4.10 (#325)
* doc: prepare doc for 0.4.10 * fixe * ci |
||
|
|
26850a0156 |
doc: add documentation for extensions (#62)
* add doc for extensions * add doc for extensions |