10xgraph-api 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- tenxgraph_api/src/app/worker.py +91 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 10xGraph project instructions
|
|
6
|
+
|
|
7
|
+
This repo uses **10xGraph** — a multi-agent framework that wraps the official OpenAI and Google SDKs behind a unified graph, agent, tool, state, storage, API, CLI, and TypeScript-client interface.
|
|
8
|
+
|
|
9
|
+
When generating, refactoring, or debugging code in this repo, prefer 10xGraph's own abstractions over hand-rolled equivalents.
|
|
10
|
+
|
|
11
|
+
## Public package names (use these in user-facing examples)
|
|
12
|
+
|
|
13
|
+
- Python core SDK: `10xgraph` — `pip install 10xgraph` — source under `tenxgraph/` in https://github.com/10xGraph/10xGraph
|
|
14
|
+
- Python API/CLI SDK: `10xgraph-api` — `pip install 10xgraph-api` — source under `tenxgraph_api/` in https://github.com/10xGraph/10xgraph-api
|
|
15
|
+
- TypeScript SDK: `@10xscale/agentflow-client` — `npm install @10xscale/agentflow-client` — source under `agentflow-client/src`
|
|
16
|
+
- Docs: https://10xgraph.com/ (source of truth for public API names)
|
|
17
|
+
- Playground: `10xgraph play` (after the CLI is installed)
|
|
18
|
+
|
|
19
|
+
Never use repository folder names (e.g. `10xgraph-api`) in install commands or user-facing docs — use the published package names above.
|
|
20
|
+
|
|
21
|
+
## Architecture overview
|
|
22
|
+
|
|
23
|
+
Three published packages, one request flow:
|
|
24
|
+
|
|
25
|
+
1. `@10xscale/agentflow-client` or another HTTP caller sends messages with `config.thread_id`.
|
|
26
|
+
2. FastAPI (`10xgraph-api`) receives the request through auth and routers.
|
|
27
|
+
3. `GraphService` invokes or streams against the compiled graph loaded at startup.
|
|
28
|
+
4. The compiled graph loads state through the checkpointer when a `thread_id` exists.
|
|
29
|
+
5. Graph nodes run and update `AgentState`.
|
|
30
|
+
6. Checkpointer saves state, messages, and thread metadata.
|
|
31
|
+
7. API returns JSON or SSE chunks to the caller.
|
|
32
|
+
|
|
33
|
+
Compile graphs once at startup. Keep graph code storage-agnostic; wire dependencies through compile arguments, `InjectQ`, or `10xgraph.json`.
|
|
34
|
+
|
|
35
|
+
## Core abstractions to reach for
|
|
36
|
+
|
|
37
|
+
- Build workflows with `StateGraph`, `Agent`, `ToolNode`, `AgentState`, and `Message`.
|
|
38
|
+
- **Use prebuilt agents for common patterns before hand-writing graph loops:**
|
|
39
|
+
- `ReactAgent` — standard tool-calling loop (use by default)
|
|
40
|
+
- `PlanActReflectAgent` — plan → act → reflect loop for complex multi-step tasks
|
|
41
|
+
- `StructuredOutputAgent` — forces JSON output matching a Pydantic schema
|
|
42
|
+
- `SupervisorTeamAgent` — central supervisor routes to specialist worker agents
|
|
43
|
+
- `SwarmAgent` — peer-to-peer agent handoff without a central supervisor
|
|
44
|
+
- `RAGAgent` — retrieves documents from a vector store before each LLM call
|
|
45
|
+
- `AudioAgent` — realtime full-duplex voice agent (Gemini Live); needs the `[realtime]` extra and is driven by `arealtime()`, not `invoke`/`stream`
|
|
46
|
+
- Persist conversation state with **checkpointers**. Use **stores** only for cross-thread memory.
|
|
47
|
+
- Inject business services through **`InjectQ`**, not module-level globals.
|
|
48
|
+
- Keep API/CLI graph modules storage-agnostic; wire dependencies via `10xgraph.json`.
|
|
49
|
+
- Every persisted interaction must include `config.thread_id`.
|
|
50
|
+
- Tools need docstrings and type annotations so model-facing schemas are useful.
|
|
51
|
+
- Injectable parameters (`state`, `config`, `tool_call_id`) are hidden from the model schema.
|
|
52
|
+
- For production, avoid process-local storage for shared state — use durable checkpointer/store backends.
|
|
53
|
+
- Add observability, audit, or business-logic side effects by registering a `GraphLifecycleHook` on `CallbackManager` — do not wrap `ainvoke()` / `astream()` calls in application code to achieve the same result.
|
|
54
|
+
|
|
55
|
+
## Agent configuration quick reference
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
Agent(
|
|
59
|
+
model="gpt-4o", # required; provider auto-detected
|
|
60
|
+
provider="openai", # explicit or omit for auto-detect
|
|
61
|
+
base_url="http://localhost:11434/v1", # for Ollama / DeepSeek / OpenRouter
|
|
62
|
+
output_type="text", # "text" | "image" | "video" | "audio"
|
|
63
|
+
output_schema=MyPydanticModel, # force structured JSON output
|
|
64
|
+
system_prompt=[{"role": "system", "content": "Help {user_name}."}], # state interpolation
|
|
65
|
+
tool_node=tool_node, # ToolNode instance or graph node name string
|
|
66
|
+
tools_tags={"search"}, # expose only matching-tagged tools
|
|
67
|
+
extra_messages=[...], # injected after system_prompt every call
|
|
68
|
+
trim_context=True, # trim old messages to fit token limits
|
|
69
|
+
reasoning_config={"effort": "medium"}, # on by default; None to disable
|
|
70
|
+
retry_config=True, # default 3 retries with backoff
|
|
71
|
+
fallback_models=["gpt-4o-mini"], # tried in order after retries exhausted
|
|
72
|
+
memory=MemoryConfig(...), # long-term memory store wiring
|
|
73
|
+
skills=SkillConfig(...), # skills discovery and exposure
|
|
74
|
+
api_style="chat", # "chat" | "responses" (OpenAI only)
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Key configuration (`10xgraph.json`)
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"agent": "graph.agent:app",
|
|
83
|
+
"env": ".env",
|
|
84
|
+
"auth": null,
|
|
85
|
+
"checkpointer": null,
|
|
86
|
+
"injectq": null,
|
|
87
|
+
"store": null,
|
|
88
|
+
"redis": null,
|
|
89
|
+
"thread_name_generator": null
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Set `"auth": {"method": "jwt"}` and `JWT_SECRET_KEY` in `.env` to enable JWT authentication.
|
|
94
|
+
|
|
95
|
+
## Important conventions
|
|
96
|
+
|
|
97
|
+
- A compiled graph is loaded once at API startup and reused per request.
|
|
98
|
+
- `thread_id` is the continuity key for conversation state — always pass it for persisted interactions.
|
|
99
|
+
- Auth and request permissions belong in API middleware/routers, not inside graph nodes.
|
|
100
|
+
- Long-term memory store records are cross-thread knowledge, not thread history.
|
|
101
|
+
- Rate limiting config lives in `10xgraph.json` under `"rate_limit"`: backend options are `"memory"` or `"redis"`.
|
|
102
|
+
- `reasoning_config` is on by default — disable explicitly with `reasoning_config=None` when not needed.
|
|
103
|
+
|
|
104
|
+
## Verifying behavior
|
|
105
|
+
|
|
106
|
+
Public names and behavior should match `agentflow-docs/docs` or https://10xgraph.com/. Implementation in the `10xgraph` and `10xgraph-api` repos and `agentflow-client/src/` shows *how* — only consult source after docs establish *what*.
|
|
107
|
+
|
|
108
|
+
That file points Copilot at the installed skill bundle under `.github/skills/10xgraph/`.
|
tenxgraph_api/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2021 Shudipto Trafder
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .config.settings import LOGGER_NAME, Settings, get_settings, logger
|
|
2
|
+
from .config.setup_logs import init_logger
|
|
3
|
+
from .config.setup_middleware import setup_middleware
|
|
4
|
+
from .exceptions.handle_errors import init_errors_handler
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"LOGGER_NAME",
|
|
9
|
+
"Settings",
|
|
10
|
+
"get_settings",
|
|
11
|
+
"init_errors_handler",
|
|
12
|
+
"init_logger",
|
|
13
|
+
"logger",
|
|
14
|
+
"setup_middleware",
|
|
15
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, Request, Response
|
|
4
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
5
|
+
from injectq.integrations import InjectAPI
|
|
6
|
+
|
|
7
|
+
from tenxgraph_api.src.app.core import logger
|
|
8
|
+
from tenxgraph_api.src.app.core.auth.base_auth import BaseAuth
|
|
9
|
+
from tenxgraph_api.src.app.core.config.graph_config import GraphConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_current_user(
|
|
13
|
+
request: Request,
|
|
14
|
+
response: Response,
|
|
15
|
+
credential: HTTPAuthorizationCredentials = Depends(
|
|
16
|
+
HTTPBearer(auto_error=False),
|
|
17
|
+
),
|
|
18
|
+
config: GraphConfig = InjectAPI(GraphConfig),
|
|
19
|
+
auth_backend: BaseAuth = InjectAPI(BaseAuth),
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
# check auth backend
|
|
22
|
+
user = {}
|
|
23
|
+
backend = config.auth_config()
|
|
24
|
+
if not backend:
|
|
25
|
+
return user
|
|
26
|
+
|
|
27
|
+
if not auth_backend:
|
|
28
|
+
logger.error("Auth backend is not configured")
|
|
29
|
+
return user
|
|
30
|
+
|
|
31
|
+
user: dict | None = auth_backend.authenticate(
|
|
32
|
+
request,
|
|
33
|
+
response,
|
|
34
|
+
credential,
|
|
35
|
+
)
|
|
36
|
+
if user and "user_id" not in user:
|
|
37
|
+
logger.error("Authentication failed: 'user_id' not found in user info")
|
|
38
|
+
return user or {}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authorization backend system for 10xGraph CLI.
|
|
3
|
+
|
|
4
|
+
This module provides an authorization interface that developers can implement
|
|
5
|
+
to add resource-level access control to their 10xGraph applications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("tenxgraph_api.authorization")
|
|
14
|
+
|
|
15
|
+
# The /v1/store routes require these. Core releases up to 0.9.1 named them "memory:*", so a
|
|
16
|
+
# "*" role would never reach the memory routes; they are added here until core is bumped.
|
|
17
|
+
STORE_SCOPES = frozenset({"store:read", "store:write", "store:delete"})
|
|
18
|
+
LEGACY_MEMORY_SCOPES = frozenset({"memory:read", "memory:write", "memory:delete"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def all_scopes() -> frozenset[str]:
|
|
22
|
+
"""Every scope the API can require, which is what a ``"*"`` role is granted."""
|
|
23
|
+
from tenxgraph.core.authz import ALL_SCOPES
|
|
24
|
+
|
|
25
|
+
return (frozenset(ALL_SCOPES) - LEGACY_MEMORY_SCOPES) | STORE_SCOPES
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthorizationBackend(ABC):
|
|
29
|
+
"""
|
|
30
|
+
Abstract base class for authorization backends.
|
|
31
|
+
|
|
32
|
+
Developers should implement this class to define custom authorization logic
|
|
33
|
+
for their 10xGraph applications. The authorize method is called before
|
|
34
|
+
any resource operation to check if the user has permission.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
class MyAuthorizationBackend(AuthorizationBackend):
|
|
38
|
+
async def authorize(self, user, resource, action, resource_id=None, **context):
|
|
39
|
+
# Check if user has permission
|
|
40
|
+
if user.get("role") == "admin":
|
|
41
|
+
return True
|
|
42
|
+
# Add custom logic here
|
|
43
|
+
return False
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
async def authorize(
|
|
48
|
+
self,
|
|
49
|
+
user: dict[str, Any],
|
|
50
|
+
resource: str,
|
|
51
|
+
action: str,
|
|
52
|
+
resource_id: str | None = None,
|
|
53
|
+
**context: Any,
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Check if user can perform action on resource.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
user: User information dictionary containing at least 'user_id'
|
|
60
|
+
resource: Resource type (e.g., 'graph', 'checkpointer', 'store')
|
|
61
|
+
action: Action to perform (e.g., 'invoke', 'stream', 'read', 'write', 'delete')
|
|
62
|
+
resource_id: Optional specific resource identifier (e.g., thread_id, namespace)
|
|
63
|
+
**context: Additional context for authorization decision
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bool: True if authorized, False otherwise
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
Exception: Can raise exceptions for auth failures or errors
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def isolation_scope(self) -> str:
|
|
73
|
+
"""The data-isolation policy this backend implies, sent to storage per request.
|
|
74
|
+
|
|
75
|
+
The API stamps this (server-side, non-hijackable) into ``config["policy"]`` so the
|
|
76
|
+
checkpointer scopes storage the *same* way this backend decides access -- no
|
|
77
|
+
contradiction between "allow_all" authz and a data layer that still filters.
|
|
78
|
+
|
|
79
|
+
- ``"owner"`` -> storage is scoped to the caller's ``user_id`` (owner-only).
|
|
80
|
+
- ``"none"`` -> storage is not scoped (any authenticated user sees everything).
|
|
81
|
+
|
|
82
|
+
Defaults to ``"none"``. Override (or return ``"owner"``) to make a custom backend
|
|
83
|
+
drive per-user storage isolation too.
|
|
84
|
+
"""
|
|
85
|
+
return "none"
|
|
86
|
+
|
|
87
|
+
def scopes_for(self, user: dict[str, Any]) -> list[str] | None:
|
|
88
|
+
"""Return the scopes this identity is granted, or None for "unrestricted".
|
|
89
|
+
|
|
90
|
+
``RequirePermission`` checks the required ``"<resource>:<action>"`` scope against
|
|
91
|
+
this list. Returning ``None`` means the identity is not scope-restricted
|
|
92
|
+
(permissive -- the default, so nothing breaks until scopes are issued).
|
|
93
|
+
|
|
94
|
+
The default passes through scopes the identity already carries
|
|
95
|
+
(``user["scopes"]``, e.g. from a JWT ``scope`` claim). A role-based backend
|
|
96
|
+
overrides this to map ``user["roles"]`` to scopes.
|
|
97
|
+
"""
|
|
98
|
+
scopes = user.get("scopes") if isinstance(user, dict) else None
|
|
99
|
+
if isinstance(scopes, list | tuple | set | frozenset):
|
|
100
|
+
return list(scopes)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DefaultAuthorizationBackend(AuthorizationBackend):
|
|
105
|
+
"""
|
|
106
|
+
Default authorization backend that allows all authenticated users.
|
|
107
|
+
|
|
108
|
+
This implementation performs basic authentication check (user has user_id)
|
|
109
|
+
but allows all operations. Use this as a starting point or for development.
|
|
110
|
+
|
|
111
|
+
For production use, implement a custom AuthorizationBackend with proper
|
|
112
|
+
access control logic based on your application's requirements.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
async def authorize(
|
|
116
|
+
self,
|
|
117
|
+
user: dict[str, Any],
|
|
118
|
+
resource: str,
|
|
119
|
+
action: str,
|
|
120
|
+
resource_id: str | None = None,
|
|
121
|
+
**context: Any,
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Allow all authenticated users to perform any action.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
user: User information dictionary
|
|
128
|
+
resource: Resource type (not used in default implementation)
|
|
129
|
+
action: Action to perform (not used in default implementation)
|
|
130
|
+
resource_id: Optional resource identifier (not used in default implementation)
|
|
131
|
+
**context: Additional context (not used in default implementation)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
bool: True if user has 'user_id', False otherwise
|
|
135
|
+
"""
|
|
136
|
+
# Only check if user is authenticated (has user_id)
|
|
137
|
+
return bool(user.get("user_id"))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class OwnershipAuthorizationBackend(AuthorizationBackend):
|
|
141
|
+
"""Object-level authorization: a user may only touch threads they own.
|
|
142
|
+
|
|
143
|
+
This is the built-in "secure" backend. It enforces the rule most multi-tenant
|
|
144
|
+
apps want without the developer writing any code: *a thread can be read, mutated,
|
|
145
|
+
stopped or fixed only by the user who owns it.* Ownership is resolved from the
|
|
146
|
+
configured checkpointer, whose thread registry records the owning ``user_id``.
|
|
147
|
+
|
|
148
|
+
Decision table (for the thread-scoped resources ``graph`` and ``checkpointer``):
|
|
149
|
+
|
|
150
|
+
- No ``user_id`` on the request -> deny (must be authenticated).
|
|
151
|
+
- No ``resource_id`` (list / create-without-id, e.g. ``GET /v1/threads``, the graph
|
|
152
|
+
schema endpoint) -> allow; those paths are already user-scoped by the service.
|
|
153
|
+
- The thread does not exist yet -> allow (a brand-new session / empty read). For
|
|
154
|
+
``invoke``/``stream`` this starts a new thread owned by the caller.
|
|
155
|
+
- The thread exists and the caller owns it -> allow.
|
|
156
|
+
- The thread exists and is owned by someone else -> **deny, for every action**
|
|
157
|
+
including ``invoke`` and ``stream``. This is the whole point: you cannot read,
|
|
158
|
+
continue, stop, fix, or run another user's thread.
|
|
159
|
+
|
|
160
|
+
Resources other than ``graph``/``checkpointer`` (store, files) are allowed through:
|
|
161
|
+
they enforce their own per-user scoping (the store keys on ``user_id``; media checks
|
|
162
|
+
file ownership in ``MediaService``).
|
|
163
|
+
|
|
164
|
+
Ownership is resolved through a :class:`ThreadOwnershipResolver` in front of
|
|
165
|
+
:meth:`BaseCheckpointer.aget_thread_owner`. Ownership is immutable, so the resolver
|
|
166
|
+
caches it (in-process, plus optional shared Redis) -- after the first lookup a check is
|
|
167
|
+
an in-memory hit, not a database round-trip per request. A checkpointer that cannot
|
|
168
|
+
resolve ownership (raises ``NotImplementedError``) or that is not configured leaves
|
|
169
|
+
nothing to enforce, so requests pass through with a warning. The in-memory checkpointer
|
|
170
|
+
only records a thread when one is explicitly written, which is why this backend is the
|
|
171
|
+
production default (Postgres) while development defaults to
|
|
172
|
+
:class:`DefaultAuthorizationBackend`.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
# Resources whose ``resource_id`` is a thread_id and therefore ownership-checkable.
|
|
176
|
+
THREAD_RESOURCES = frozenset({"graph", "checkpointer"})
|
|
177
|
+
|
|
178
|
+
def isolation_scope(self) -> str:
|
|
179
|
+
# Owner-only access -> storage must be scoped to the caller too.
|
|
180
|
+
return "owner"
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
checkpointer: Any | None = None,
|
|
185
|
+
*,
|
|
186
|
+
resolver: Any | None = None,
|
|
187
|
+
redis: object | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
self._checkpointer = checkpointer
|
|
190
|
+
self._resolver = resolver
|
|
191
|
+
self._redis = redis
|
|
192
|
+
self._resolver_built = resolver is not None
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def checkpointer(self) -> Any | None:
|
|
196
|
+
"""Resolve the checkpointer lazily from the DI container if not injected."""
|
|
197
|
+
if self._checkpointer is None:
|
|
198
|
+
try:
|
|
199
|
+
from injectq import InjectQ
|
|
200
|
+
from tenxgraph.storage.checkpointer import BaseCheckpointer
|
|
201
|
+
|
|
202
|
+
self._checkpointer = InjectQ.get_instance().try_get(BaseCheckpointer)
|
|
203
|
+
except Exception: # pragma: no cover - defensive
|
|
204
|
+
self._checkpointer = None
|
|
205
|
+
return self._checkpointer
|
|
206
|
+
|
|
207
|
+
def _get_resolver(self) -> Any | None:
|
|
208
|
+
"""Return the cached ownership resolver, building it once from the checkpointer.
|
|
209
|
+
|
|
210
|
+
Returns None when no checkpointer is available (nothing to resolve).
|
|
211
|
+
"""
|
|
212
|
+
if self._resolver_built:
|
|
213
|
+
return self._resolver
|
|
214
|
+
self._resolver_built = True
|
|
215
|
+
cp = self.checkpointer
|
|
216
|
+
if cp is None:
|
|
217
|
+
self._resolver = None
|
|
218
|
+
else:
|
|
219
|
+
from tenxgraph_api.src.app.core.auth.ownership_resolver import (
|
|
220
|
+
ThreadOwnershipResolver,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
self._resolver = ThreadOwnershipResolver(cp.aget_thread_owner, redis=self._redis)
|
|
224
|
+
return self._resolver
|
|
225
|
+
|
|
226
|
+
def evict(self, thread_id: str | int) -> Any:
|
|
227
|
+
"""Invalidate a thread's cached ownership (call on thread deletion).
|
|
228
|
+
|
|
229
|
+
Returns the resolver's ``evict`` coroutine when a resolver exists, so callers may
|
|
230
|
+
``await`` it; returns None when there is nothing to evict.
|
|
231
|
+
"""
|
|
232
|
+
resolver = self._get_resolver()
|
|
233
|
+
if resolver is None:
|
|
234
|
+
return None
|
|
235
|
+
return resolver.evict(thread_id)
|
|
236
|
+
|
|
237
|
+
async def aclose(self) -> None:
|
|
238
|
+
"""Close the L2 Redis client this backend created (called on app shutdown)."""
|
|
239
|
+
client = self._redis
|
|
240
|
+
if client is not None and hasattr(client, "aclose"):
|
|
241
|
+
try:
|
|
242
|
+
await client.aclose()
|
|
243
|
+
except Exception as exc: # pragma: no cover - best-effort shutdown
|
|
244
|
+
logger.debug("Ownership Redis client close failed: %s", exc)
|
|
245
|
+
|
|
246
|
+
async def authorize(
|
|
247
|
+
self,
|
|
248
|
+
user: dict[str, Any],
|
|
249
|
+
resource: str,
|
|
250
|
+
action: str,
|
|
251
|
+
resource_id: str | None = None,
|
|
252
|
+
**context: Any,
|
|
253
|
+
) -> bool:
|
|
254
|
+
user_id = user.get("user_id") if user else None
|
|
255
|
+
if not user_id:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
# Only thread-scoped resources are ownership-checked here.
|
|
259
|
+
if resource not in self.THREAD_RESOURCES:
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
# Endpoints without a specific thread (list/create) are user-scoped by the
|
|
263
|
+
# service layer; nothing to authorize at the object level.
|
|
264
|
+
if resource_id is None:
|
|
265
|
+
return True
|
|
266
|
+
|
|
267
|
+
resolver = self._get_resolver()
|
|
268
|
+
if resolver is None:
|
|
269
|
+
logger.warning(
|
|
270
|
+
"Ownership authorization is active but no checkpointer is configured; "
|
|
271
|
+
"allowing %s:%s (no persisted threads to protect).",
|
|
272
|
+
resource,
|
|
273
|
+
action,
|
|
274
|
+
)
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
return await self._authorize_ownership(
|
|
278
|
+
resolver, str(resource_id), str(user_id), resource, action
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def _authorize_ownership(
|
|
282
|
+
self,
|
|
283
|
+
resolver: Any,
|
|
284
|
+
resource_id: str,
|
|
285
|
+
user_id: str,
|
|
286
|
+
resource: str,
|
|
287
|
+
action: str,
|
|
288
|
+
) -> bool:
|
|
289
|
+
try:
|
|
290
|
+
owner = await resolver.owner_of(resource_id)
|
|
291
|
+
except NotImplementedError:
|
|
292
|
+
logger.warning(
|
|
293
|
+
"Checkpointer %s cannot resolve thread ownership; ownership "
|
|
294
|
+
"authorization is inactive for %s:%s. Use a checkpointer that "
|
|
295
|
+
"implements aget_thread_owner (e.g. PgCheckpointer) or configure a "
|
|
296
|
+
"custom AuthorizationBackend.",
|
|
297
|
+
type(self.checkpointer).__name__,
|
|
298
|
+
resource,
|
|
299
|
+
action,
|
|
300
|
+
)
|
|
301
|
+
return True
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
# Fail closed: a resolution error must not silently grant access.
|
|
304
|
+
logger.warning(
|
|
305
|
+
"Ownership check failed for thread %s (user %s): %s; denying.",
|
|
306
|
+
resource_id,
|
|
307
|
+
user_id,
|
|
308
|
+
exc,
|
|
309
|
+
)
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
# Unknown thread -> brand-new session (or an empty read); allow. Otherwise the
|
|
313
|
+
# caller must be the owner, for EVERY action (invoke/stream included).
|
|
314
|
+
if owner is None:
|
|
315
|
+
return True
|
|
316
|
+
return str(owner) == user_id
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class RoleBasedAuthorizationBackend(OwnershipAuthorizationBackend):
|
|
320
|
+
"""RBAC: map a user's roles to scopes, on top of owner-only object isolation.
|
|
321
|
+
|
|
322
|
+
Combines two things in one backend:
|
|
323
|
+
|
|
324
|
+
- **Scopes** (what actions are allowed) -- ``scopes_for`` maps ``user["roles"]`` (or
|
|
325
|
+
``user["role"]``) to a set of scopes via the ``role_scopes`` table. ``RequirePermission``
|
|
326
|
+
then enforces the required ``"<resource>:<action>"`` scope. A role granting ``"*"``
|
|
327
|
+
gets every scope in :func:`all_scopes`.
|
|
328
|
+
- **Isolation** (which rows are visible) -- inherited from
|
|
329
|
+
:class:`OwnershipAuthorizationBackend` (owner-only threads), configurable via
|
|
330
|
+
``isolation``.
|
|
331
|
+
|
|
332
|
+
Example::
|
|
333
|
+
|
|
334
|
+
RoleBasedAuthorizationBackend(
|
|
335
|
+
role_scopes={
|
|
336
|
+
"admin": ["*"],
|
|
337
|
+
"member": ["graph:invoke", "graph:stream", "checkpointer:read"],
|
|
338
|
+
},
|
|
339
|
+
default_scopes=["graph:read"], # granted to everyone, even with no role
|
|
340
|
+
)
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
def __init__(
|
|
344
|
+
self,
|
|
345
|
+
role_scopes: dict[str, Any] | None = None,
|
|
346
|
+
*,
|
|
347
|
+
default_scopes: Any = (),
|
|
348
|
+
isolation: str = "owner",
|
|
349
|
+
checkpointer: Any | None = None,
|
|
350
|
+
resolver: Any | None = None,
|
|
351
|
+
redis: object | None = None,
|
|
352
|
+
) -> None:
|
|
353
|
+
super().__init__(checkpointer=checkpointer, resolver=resolver, redis=redis)
|
|
354
|
+
self._role_scopes = {str(r): list(s) for r, s in (role_scopes or {}).items()}
|
|
355
|
+
self._default_scopes = list(default_scopes)
|
|
356
|
+
self._isolation = isolation if isolation in ("owner", "none") else "owner"
|
|
357
|
+
|
|
358
|
+
def isolation_scope(self) -> str:
|
|
359
|
+
return self._isolation
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def _roles_of(user: dict[str, Any]) -> list[str]:
|
|
363
|
+
if not isinstance(user, dict):
|
|
364
|
+
return []
|
|
365
|
+
roles = user.get("roles")
|
|
366
|
+
if roles is None:
|
|
367
|
+
role = user.get("role")
|
|
368
|
+
roles = [role] if role else []
|
|
369
|
+
elif isinstance(roles, str):
|
|
370
|
+
roles = [roles]
|
|
371
|
+
return [str(r) for r in roles]
|
|
372
|
+
|
|
373
|
+
def scopes_for(self, user: dict[str, Any]) -> list[str]:
|
|
374
|
+
granted: set[str] = set(self._default_scopes)
|
|
375
|
+
for role in self._roles_of(user):
|
|
376
|
+
granted.update(self._role_scopes.get(role, []))
|
|
377
|
+
if "*" in granted:
|
|
378
|
+
return sorted(all_scopes())
|
|
379
|
+
return sorted(granted)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fastapi import Request, Response
|
|
5
|
+
from fastapi.security import HTTPAuthorizationCredentials
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseAuth(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def authenticate(
|
|
11
|
+
self,
|
|
12
|
+
request: Request,
|
|
13
|
+
response: Response,
|
|
14
|
+
credential: HTTPAuthorizationCredentials,
|
|
15
|
+
) -> dict[str, Any] | None:
|
|
16
|
+
"""Authenticate the user based on the provided credentials.
|
|
17
|
+
IT should return an empty dict if no authentication is required.
|
|
18
|
+
If authentication fails, it should raise an appropriate exception.
|
|
19
|
+
In case authentication is successful, it should return a dict with user information,
|
|
20
|
+
containing at least a 'user_id' key, and optionally other user details.
|
|
21
|
+
Example:
|
|
22
|
+
return {"user_id": "12345", "username": "johndoe", "email": "johndoe@example.com"}
|
|
23
|
+
|
|
24
|
+
What ever keys are returned that will be merged with config in the main graph app.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
raise NotImplementedError
|