From 19b82b24ede91fcc729cf3489dbfbae02a1710f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:12:45 +0000 Subject: [PATCH 001/345] chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /frontend Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f2e0ccfd..143954c2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12060,9 +12060,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { From ff3525e46ba95981bf0a1c4cee43cd941f562ef9 Mon Sep 17 00:00:00 2001 From: Rousseau Vincent Date: Wed, 8 Apr 2026 10:22:33 +0200 Subject: [PATCH 002/345] Update surrealdb user and add gpu for ollama Added user root for surrealdb service and nvidia GPU support in ollama service. --- docs/0-START-HERE/quick-start-local.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/0-START-HERE/quick-start-local.md b/docs/0-START-HERE/quick-start-local.md index 3a5c9a02..07d48d05 100644 --- a/docs/0-START-HERE/quick-start-local.md +++ b/docs/0-START-HERE/quick-start-local.md @@ -32,6 +32,7 @@ services: surrealdb: image: surrealdb/surrealdb:v2 command: start --user root --pass password --bind 0.0.0.0:8000 rocksdb:/mydata/mydatabase.db + user: root ports: - "8000:8000" volumes: @@ -65,10 +66,15 @@ services: - "11434:11434" volumes: - ./ollama_models:/root/.ollama - environment: - # Optional: set GPU support if available - - OLLAMA_NUM_GPU=0 restart: always + # Optional: set GPU support if available + #deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] ``` From 3755b1f2d4f60e736de73a4087dd35e138f4c743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:55:21 +0000 Subject: [PATCH 003/345] chore(deps): bump cryptography from 46.0.6 to 46.0.7 Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.6 to 46.0.7. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/46.0.6...46.0.7) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 72 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/uv.lock b/uv.lock index 10608e2d..994ae139 100644 --- a/uv.lock +++ b/uv.lock @@ -464,47 +464,47 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.6" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, - { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] From 58e9998bb8f6e3228bed938c754db60627a99156 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:28:22 +0000 Subject: [PATCH 004/345] chore(deps): bump langchain-core from 1.2.22 to 1.2.28 Bumps [langchain-core](https://github.com/langchain-ai/langchain) from 1.2.22 to 1.2.28. - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/langchain-core==1.2.22...langchain-core==1.2.28) --- updated-dependencies: - dependency-name: langchain-core dependency-version: 1.2.28 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 10608e2d..ac5e5bcd 100644 --- a/uv.lock +++ b/uv.lock @@ -1446,7 +1446,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.2.22" +version = "1.2.28" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1458,9 +1458,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/a3/c4cd6827a1df46c821e7214b7f7b7a28b189e6c9b84ef15c6d629c5e3179/langchain_core-1.2.22.tar.gz", hash = "sha256:8d8f726d03d3652d403da915126626bb6250747e8ba406537d849e68b9f5d058", size = 842487, upload-time = "2026-03-24T18:48:44.9Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/317a1a3ac1df33a64adb3670bf88bbe3b3d5baa274db6863a979db472897/langchain_core-1.2.28.tar.gz", hash = "sha256:271a3d8bd618f795fdeba112b0753980457fc90537c46a0c11998516a74dc2cb", size = 846119, upload-time = "2026-04-08T18:19:34.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/a6/2ffacf0f1a3788f250e75d0b52a24896c413be11be3a6d42bcdf46fbea48/langchain_core-1.2.22-py3-none-any.whl", hash = "sha256:7e30d586b75918e828833b9ec1efc25465723566845dd652c277baf751e9c04b", size = 506829, upload-time = "2026-03-24T18:48:43.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/32f785f077c7e898da97064f113c73fbd9ad55d1e2169cf3a391b183dedb/langchain_core-1.2.28-py3-none-any.whl", hash = "sha256:80764232581eaf8057bcefa71dbf8adc1f6a28d257ebd8b95ba9b8b452e8c6ac", size = 508727, upload-time = "2026-04-08T18:19:32.823Z" }, ] [[package]] From 70a466a640d75de398e34cf6e492aeb576cbffcf Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Thu, 9 Apr 2026 11:58:16 -0300 Subject: [PATCH 005/345] fix: prevent RCE via SSTI, path traversal file write, and LFI file read - Bump ai-prompter to >=0.4.0 which uses Jinja2 SandboxedEnvironment, preventing arbitrary code execution via user-provided transformation prompts - Sanitize uploaded filenames with os.path.basename() and validate resolved path stays within upload directory to prevent path traversal - Validate file_path in source creation is within UPLOADS_FOLDER to prevent arbitrary file read via Local File Inclusion --- CHANGELOG.md | 10 ++++++++++ api/routers/sources.py | 27 ++++++++++++++++++++++----- pyproject.toml | 4 ++-- tests/test_sources_api.py | 6 ++++-- uv.lock | 8 ++++---- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe263bfd..e763d5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.8.4] - 2026-04-09 + +### Security +- Fix Remote Code Execution (RCE) via Jinja2 Server-Side Template Injection in transformations (CVSS 9.2 Critical) +- Fix arbitrary file write via path traversal in file upload (CVSS 7.0 High) +- Fix arbitrary file read via Local File Inclusion in source creation (CVSS 8.2 High) + +### Dependencies +- Bump ai-prompter to >=0.4.0 (uses Jinja2 SandboxedEnvironment to prevent SSTI) + ## [1.8.3] - 2026-04-07 ### Security diff --git a/api/routers/sources.py b/api/routers/sources.py index 38e75a3d..e6d6eff1 100644 --- a/api/routers/sources.py +++ b/api/routers/sources.py @@ -43,21 +43,30 @@ def generate_unique_filename(original_filename: str, upload_folder: str) -> str: file_path = Path(upload_folder) file_path.mkdir(parents=True, exist_ok=True) + # Strip directory components to prevent path traversal + safe_filename = os.path.basename(original_filename) + if not safe_filename: + raise ValueError("Invalid filename") + # Split filename and extension - stem = Path(original_filename).stem - suffix = Path(original_filename).suffix + stem = Path(safe_filename).stem + suffix = Path(safe_filename).suffix # Check if file exists and generate unique name counter = 0 while True: if counter == 0: - new_filename = original_filename + new_filename = safe_filename else: new_filename = f"{stem} ({counter}){suffix}" full_path = file_path / new_filename - if not full_path.exists(): - return str(full_path) + # Verify resolved path stays within upload folder + resolved = full_path.resolve() + if not str(resolved).startswith(str(file_path.resolve())): + raise ValueError("Invalid filename: path traversal detected") + if not resolved.exists(): + return str(resolved) counter += 1 @@ -325,6 +334,14 @@ async def create_source( status_code=400, detail="File upload or file_path is required for upload type", ) + # Validate file_path is within the uploads directory to prevent LFI + uploads_resolved = Path(UPLOADS_FOLDER).resolve() + file_resolved = Path(final_file_path).resolve() + if not str(file_resolved).startswith(str(uploads_resolved)): + raise HTTPException( + status_code=400, + detail="Invalid file path: must be within the uploads directory", + ) content_state["file_path"] = final_file_path content_state["delete_source"] = source_data.delete_source elif source_data.type == "text": diff --git a/pyproject.toml b/pyproject.toml index f526a903..8814dc2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "open-notebook" -version = "1.8.3" +version = "1.8.4" description = "An open source implementation of a research assistant, inspired by Google Notebook LM" authors = [ {name = "Luis Novo", email = "lfnovo@gmail.com"} @@ -33,7 +33,7 @@ dependencies = [ "python-dotenv>=1.0.1", "httpx[socks]>=0.27.0", "content-core>=1.14.1,<2", - "ai-prompter>=0.3,<1", + "ai-prompter>=0.4,<1", "esperanto>=2.20.0,<3", "surrealdb>=1.0.4", "podcast-creator>=0.12.0,<1", diff --git a/tests/test_sources_api.py b/tests/test_sources_api.py index 06db1c46..f33cf6bd 100644 --- a/tests/test_sources_api.py +++ b/tests/test_sources_api.py @@ -1,10 +1,12 @@ """Tests for the sources API endpoint.""" +import os from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi.testclient import TestClient +from open_notebook.config import UPLOADS_FOLDER from open_notebook.domain.notebook import Source @@ -71,7 +73,7 @@ class TestAsyncSourceAssetPersistence: ): """POST /sources with type=upload and async_processing=true persists Asset(file_path=...).""" mock_nb_get.return_value = MagicMock() - mock_upload.return_value = "/tmp/uploads/video.mp4" + mock_upload.return_value = os.path.join(os.path.abspath(UPLOADS_FOLDER), "video.mp4") mock_submit.return_value = "command:123" saved_sources = [] @@ -97,7 +99,7 @@ class TestAsyncSourceAssetPersistence: source = saved_sources[0] assert source.asset is not None - assert source.asset.file_path == "/tmp/uploads/video.mp4" + assert source.asset.file_path == os.path.join(os.path.abspath(UPLOADS_FOLDER), "video.mp4") assert source.asset.url is None @pytest.mark.asyncio diff --git a/uv.lock b/uv.lock index 10608e2d..a2235dbc 100644 --- a/uv.lock +++ b/uv.lock @@ -9,16 +9,16 @@ resolution-markers = [ [[package]] name = "ai-prompter" -version = "0.3.1" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "pip" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/88/cadc58aac8bbb599d8c6d25f2cd357e938ddabe3756b1eed68e8860eed22/ai_prompter-0.3.1.tar.gz", hash = "sha256:cec7fddac5edf7d836c13fe77613e16b12b23aea625133846d11ae22455ed397", size = 84837, upload-time = "2025-06-20T19:31:36.879Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/a6/0703f5de846d4ccdc6cdfc7e44473f2722125e95316a50d44303b80edbe6/ai_prompter-0.4.0.tar.gz", hash = "sha256:63a8f2c19f13c0313ee7cef58f07cc00e948546c83c335c42737e076ae764385", size = 86469, upload-time = "2026-04-09T14:54:32.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/f6/490bf534d6b04755998142ec7412b78e7466dc6d9a3f31e90541506b7971/ai_prompter-0.3.1-py3-none-any.whl", hash = "sha256:ec26566f7f246f511325e1c0f260970963a6e49fb19ed0767152d30b82cd2b79", size = 13386, upload-time = "2025-06-20T19:31:34.952Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/881c31c26a7a131ed9e1a2c1c3838b5bb95e3822ac88c68fcda7738c5395/ai_prompter-0.4.0-py3-none-any.whl", hash = "sha256:230662484a27de34e8677ebe20a8220c78f8b605cb68005253c5dbf3e5cc254e", size = 13777, upload-time = "2026-04-09T14:54:33.017Z" }, ] [[package]] @@ -2126,7 +2126,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "ai-prompter", specifier = ">=0.3,<1" }, + { name = "ai-prompter", specifier = ">=0.4,<1" }, { name = "babel", specifier = ">=2.18.0" }, { name = "content-core", specifier = ">=1.14.1,<2" }, { name = "esperanto", specifier = ">=2.20.0,<3" }, From 2f75c5978c93836ca4605693786667830135eea4 Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Thu, 9 Apr 2026 12:05:38 -0300 Subject: [PATCH 006/345] fix: harden path validation to prevent sibling directory bypass Append os.sep to the directory path before startswith() check so that paths like /app/data/uploads_evil/ cannot bypass the uploads directory validation. --- api/routers/sources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/routers/sources.py b/api/routers/sources.py index e6d6eff1..9c4b7452 100644 --- a/api/routers/sources.py +++ b/api/routers/sources.py @@ -63,7 +63,7 @@ def generate_unique_filename(original_filename: str, upload_folder: str) -> str: full_path = file_path / new_filename # Verify resolved path stays within upload folder resolved = full_path.resolve() - if not str(resolved).startswith(str(file_path.resolve())): + if not str(resolved).startswith(str(file_path.resolve()) + os.sep): raise ValueError("Invalid filename: path traversal detected") if not resolved.exists(): return str(resolved) @@ -337,7 +337,7 @@ async def create_source( # Validate file_path is within the uploads directory to prevent LFI uploads_resolved = Path(UPLOADS_FOLDER).resolve() file_resolved = Path(final_file_path).resolve() - if not str(file_resolved).startswith(str(uploads_resolved)): + if not str(file_resolved).startswith(str(uploads_resolved) + os.sep): raise HTTPException( status_code=400, detail="Invalid file path: must be within the uploads directory", From 8ee18d1fb7ac625a5deec5b5abef30eec076a247 Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Thu, 9 Apr 2026 12:16:09 -0300 Subject: [PATCH 007/345] docs: add security guidelines for contributors Add security.md covering database query safety, template rendering, file handling, secrets management, and a code review checklist. Informed by CERT-EU coordinated vulnerability disclosures. --- docs/7-DEVELOPMENT/index.md | 10 ++ docs/7-DEVELOPMENT/security.md | 193 +++++++++++++++++++++++++++++++++ uv.lock | 2 +- 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 docs/7-DEVELOPMENT/security.md diff --git a/docs/7-DEVELOPMENT/index.md b/docs/7-DEVELOPMENT/index.md index 81826bc8..5e5f9a95 100644 --- a/docs/7-DEVELOPMENT/index.md +++ b/docs/7-DEVELOPMENT/index.md @@ -14,6 +14,15 @@ Start with **[Contributing Guide](contributing.md)** for the workflow, then chec **First time?** Check out our [Contributing Guide](contributing.md) for the issue-first workflow. +### 🔒 I Want to Understand Security Practices + +**[Security Guidelines](security.md)** covers: +- Database query safety (preventing SurrealQL injection) +- Template rendering safety (preventing SSTI) +- File handling safety (preventing path traversal and LFI) +- Secrets management and CORS configuration +- Code review security checklist + --- ### 🏗️ I Want to Understand the Architecture @@ -50,6 +59,7 @@ For deeper dives, check `/open_notebook/` CLAUDE.md for component-specific guida | [Architecture](architecture.md) | Understanding system | System design, tech stack, workflows | | [Design Principles](design-principles.md) | All developers | What guides our decisions | | [API Reference](api-reference.md) | Building integrations | Complete REST API documentation | +| [Security](security.md) | All developers | Security practices and vulnerability prevention | | [Maintainer Guide](maintainer-guide.md) | Maintainers | Managing issues, PRs, releases | --- diff --git a/docs/7-DEVELOPMENT/security.md b/docs/7-DEVELOPMENT/security.md new file mode 100644 index 00000000..5eb394ac --- /dev/null +++ b/docs/7-DEVELOPMENT/security.md @@ -0,0 +1,193 @@ +# Security Guidelines + +This document outlines security practices for Open Notebook development. It is informed by real vulnerabilities discovered through coordinated disclosure with [CERT-EU](https://cert.europa.eu) and should be treated as mandatory reading for all contributors. + +## Reporting Vulnerabilities + +If you discover a security vulnerability, **do not open a public GitHub issue**. Instead: + +1. Use [GitHub Security Advisories](https://github.com/lfnovo/open-notebook/security/advisories/new) to report privately +2. Or email the maintainers directly + +We follow coordinated vulnerability disclosure and will work with you on a fix before any public announcement. + +--- + +## Database Queries (SurrealQL Injection) + +**Rule: Never interpolate user input into SurrealQL queries via f-strings.** + +SurrealQL injection is the equivalent of SQL injection. User-controlled values must be passed as parameterized bind variables using `$variable` syntax. + +### Parameterized queries (safe) + +```python +# Good: parameterized query +result = await repo_query( + "SELECT * FROM source WHERE id = $id", + {"id": ensure_record_id(source_id)} +) +``` + +### F-string interpolation (vulnerable) + +```python +# Bad: user input in f-string +result = await repo_query(f"SELECT * FROM source WHERE id = {source_id}") +``` + +### ORDER BY and other clauses that can't be parameterized + +`ORDER BY`, `LIMIT`, and similar clauses typically cannot accept bind variables in SurrealDB. Use **allowlist validation** instead: + +```python +# Good: validate against allowlist, then interpolate +allowed_fields = {"name", "created", "updated"} +allowed_directions = {"asc", "desc"} + +parts = order_by.strip().lower().split() +if parts[0] not in allowed_fields: + raise HTTPException(status_code=400, detail="Invalid sort field") +if len(parts) > 1 and parts[1] not in allowed_directions: + raise HTTPException(status_code=400, detail="Invalid sort direction") + +query = f"SELECT * FROM notebook ORDER BY {validated_order_by}" +``` + +See `api/routers/sources.py` for the reference implementation of sort parameter validation. + +### Checklist + +- [ ] All user-provided values use `$variable` binding +- [ ] Any f-string in a query only contains validated/hardcoded values +- [ ] `ORDER BY`, `LIMIT`, etc. use allowlist validation +- [ ] Database values used in subsequent queries are also parameterized (prevents second-order injection) + +--- + +## Template Rendering (Server-Side Template Injection) + +**Rule: Always use `SandboxedEnvironment` when rendering Jinja2 templates that contain user-provided content.** + +The [ai-prompter](https://github.com/lfnovo/ai-prompter) library (>= 0.4.0) uses `SandboxedEnvironment` by default, which blocks access to dangerous Python attributes like `__globals__`, `__subclasses__`, and `__init__`. + +### What SandboxedEnvironment prevents + +```jinja2 +{# These are blocked and raise SecurityError #} +{{ cycler.__init__.__globals__.os.popen('id').read() }} +{{ ''.__class__.__mro__[1].__subclasses__() }} +``` + +### Guidelines + +- Never downgrade ai-prompter below 0.4.0 +- If using Jinja2 directly (outside ai-prompter), always use `jinja2.sandbox.SandboxedEnvironment` +- Never pass user-provided strings to `jinja2.Environment` or `jinja2.Template` directly + +--- + +## File Handling (Path Traversal and Local File Inclusion) + +### File uploads + +**Rule: Always sanitize filenames and validate resolved paths.** + +```python +import os +from pathlib import Path + +# 1. Strip directory components +safe_filename = os.path.basename(original_filename) + +# 2. Validate resolved path stays within target directory +resolved = (Path(upload_folder) / safe_filename).resolve() +if not str(resolved).startswith(str(Path(upload_folder).resolve()) + os.sep): + raise ValueError("Path traversal detected") +``` + +Key points: + +- Use `os.path.basename()` to strip directory components from user-provided filenames +- Use `Path.resolve()` to resolve symlinks and `..` components +- Use `startswith()` with a **trailing `os.sep`** to prevent sibling directory bypass (e.g., `/uploads_evil/` matching `/uploads`) + +### File path inputs + +**Rule: Validate that any user-provided file path is within the expected directory.** + +```python +uploads_resolved = Path(UPLOADS_FOLDER).resolve() +file_resolved = Path(user_provided_path).resolve() +if not str(file_resolved).startswith(str(uploads_resolved) + os.sep): + raise HTTPException(status_code=400, detail="Invalid file path") +``` + +Never pass user-provided file paths directly to file reading or content extraction functions without validation. + +### Checklist + +- [ ] Filenames from uploads are sanitized with `os.path.basename()` +- [ ] Resolved paths are validated with `startswith(directory + os.sep)` +- [ ] User-provided `file_path` values are validated before use +- [ ] No directory creation from user input (`mkdir` with traversal paths) + +--- + +## Authentication and CORS + +### Authentication + +Open Notebook currently uses simple password-based middleware (`PasswordAuthMiddleware`). This is suitable for single-user self-hosted deployments but should be hardened for production: + +- Change the default password (`OPEN_NOTEBOOK_PASSWORD`) +- Change the default encryption key (`OPEN_NOTEBOOK_ENCRYPTION_KEY`) +- Consider deploying behind a reverse proxy with proper authentication (OAuth, OIDC) + +### CORS + +The default CORS configuration allows all origins (`allow_origins=["*"]`). This is tracked for improvement in [#730](https://github.com/lfnovo/open-notebook/issues/730). For production deployments, restrict origins to only the frontend URL. + +--- + +## Secrets Management + +### Encryption key + +`OPEN_NOTEBOOK_ENCRYPTION_KEY` is used to encrypt API keys stored in SurrealDB. In production: + +- Set a strong, unique key (do not use the default) +- Use Docker secrets via `OPEN_NOTEBOOK_ENCRYPTION_KEY_FILE` when possible +- Never log or expose this value + +### Environment variables + +- Sensitive values (API keys, passwords, encryption keys) should never appear in logs +- Use `loguru` with caution — avoid logging full request bodies or environment dumps +- The Docker container runs as root by default; consider running as a non-root user + +--- + +## Code Review Security Checklist + +When reviewing PRs, check for: + +1. **Query injection**: Any f-string containing user input in a SurrealQL query +2. **Template injection**: User-provided strings passed to Jinja2 without sandboxing +3. **Path traversal**: User-provided filenames or paths used without sanitization +4. **Information disclosure**: Error messages that expose internal paths, stack traces, or configuration +5. **SSRF**: User-provided URLs passed to server-side HTTP requests without validation +6. **Secrets in logs**: Sensitive values logged at any level + +--- + +## Past Vulnerabilities + +These vulnerabilities were reported by CERT-EU and are documented here as learning examples: + +| Version | Vulnerability | Severity | Advisory | +|---------|--------------|----------|----------| +| <= 1.8.2 | SurrealDB injection via `order_by` parameter | High (8.7) | [GHSA-5wj9-f8q5-8f9c](https://github.com/lfnovo/open-notebook/security/advisories/GHSA-5wj9-f8q5-8f9c) | +| <= 1.8.3 | RCE via Jinja2 SSTI in transformations | Critical (9.2) | [GHSA-f35w-wx37-26q7](https://github.com/lfnovo/open-notebook/security/advisories/GHSA-f35w-wx37-26q7) | +| <= 1.8.3 | Arbitrary file write via path traversal | High (7.0) | [GHSA-x4q2-89g5-594v](https://github.com/lfnovo/open-notebook/security/advisories/GHSA-x4q2-89g5-594v) | +| <= 1.8.3 | Arbitrary file read via LFI | High (8.2) | [GHSA-842v-h4cj-r646](https://github.com/lfnovo/open-notebook/security/advisories/GHSA-842v-h4cj-r646) | diff --git a/uv.lock b/uv.lock index a2235dbc..f8ba6b01 100644 --- a/uv.lock +++ b/uv.lock @@ -2072,7 +2072,7 @@ wheels = [ [[package]] name = "open-notebook" -version = "1.8.3" +version = "1.8.4" source = { editable = "." } dependencies = [ { name = "ai-prompter" }, From 4222329451d1bed5a2d1df70d4d176084fe379c7 Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Thu, 9 Apr 2026 13:22:00 -0300 Subject: [PATCH 008/345] fix: map base_url to endpoint for Azure credentials (#741) * fix: map base_url to endpoint for Azure credentials The Azure credential form only exposes a base_url field, but the connection tester, key provisioner, and Esperanto config all expect an endpoint field. This maps base_url to endpoint for Azure providers so credentials work without requiring a dedicated endpoint form field. Closes #727 * docs: update Azure credential docs to reflect base_url mapping --- docs/3-USER-GUIDE/api-configuration.md | 22 +++++++++------------- open_notebook/ai/connection_tester.py | 4 +++- open_notebook/ai/key_provider.py | 6 ++++-- open_notebook/domain/credential.py | 3 +++ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/3-USER-GUIDE/api-configuration.md b/docs/3-USER-GUIDE/api-configuration.md index 96c3bc5f..940e0eff 100644 --- a/docs/3-USER-GUIDE/api-configuration.md +++ b/docs/3-USER-GUIDE/api-configuration.md @@ -107,7 +107,7 @@ Navigation: Settings → API Keys | Provider | Required Fields | Optional Fields | |----------|-----------------|-----------------| -| Azure OpenAI | API Key, Endpoint, API Version | Service-specific endpoints (LLM, Embedding, STT, TTS) | +| Azure OpenAI | API Key, URL Base (Azure endpoint) | Service-specific endpoints (LLM, Embedding, STT, TTS) | | OpenAI-Compatible | Base URL | API Key, Service-specific configs | | Vertex AI | Project ID, Location, Credentials Path | — | @@ -208,17 +208,13 @@ Ollama allows localhost and private IPs since it runs locally. ### Azure OpenAI -Azure requires multiple fields: +1. Add credential with provider **Azure OpenAI** +2. Enter your API key +3. Enter your Azure endpoint in the **URL Base** field (e.g., `https://myresource.openai.azure.com`) +4. Test connection +5. Discover and register models -| Field | Example | Required | -|-------|---------|----------| -| API Key | `abc123...` | Yes | -| Endpoint | `https://myresource.openai.azure.com` | Yes | -| API Version | `2024-02-15-preview` | Yes | -| LLM Endpoint | `https://myresource-llm.openai.azure.com` | No | -| Embedding Endpoint | `https://myresource-embed.openai.azure.com` | No | - -Service-specific endpoints override the main endpoint for that service type. +The URL Base field is automatically mapped to the Azure endpoint. The API version defaults to `2024-10-21` if not set via environment variable. ### OpenAI-Compatible @@ -377,8 +373,8 @@ API keys stored in the database are encrypted using Fernet (AES-128-CBC + HMAC-S - Ensure Ollama server is running ### Azure OpenAI -- Endpoint format: `https://{resource-name}.openai.azure.com` -- API version format: `YYYY-MM-DD` or `YYYY-MM-DD-preview` +- Enter your Azure endpoint in the **URL Base** field (format: `https://{resource-name}.openai.azure.com`) +- API version defaults to `2024-10-21`; override via `AZURE_OPENAI_API_VERSION` environment variable if needed - Deployment names configured separately when registering models via the credential's Discover Models dialog --- diff --git a/open_notebook/ai/connection_tester.py b/open_notebook/ai/connection_tester.py index 99e52d40..2825edb8 100644 --- a/open_notebook/ai/connection_tester.py +++ b/open_notebook/ai/connection_tester.py @@ -223,7 +223,9 @@ async def test_provider_connection( return await _test_openai_compatible_connection(test_base_url, test_api_key) if normalized_provider == "azure": - return await _test_azure_connection(endpoint, api_key, api_version) + # For Azure, base_url from the UI form maps to endpoint + azure_endpoint = endpoint or base_url + return await _test_azure_connection(azure_endpoint, api_key, api_version) # Get test model for provider if normalized_provider not in TEST_MODELS: diff --git a/open_notebook/ai/key_provider.py b/open_notebook/ai/key_provider.py index 9a54d9ed..d7a621de 100644 --- a/open_notebook/ai/key_provider.py +++ b/open_notebook/ai/key_provider.py @@ -190,8 +190,10 @@ async def _provision_azure() -> bool: os.environ["AZURE_OPENAI_API_VERSION"] = cred.api_version logger.debug("Set AZURE_OPENAI_API_VERSION from Credential") any_set = True - if cred.endpoint: - os.environ["AZURE_OPENAI_ENDPOINT"] = cred.endpoint + # For Azure, base_url from the UI form maps to endpoint + azure_endpoint = cred.endpoint or cred.base_url + if azure_endpoint: + os.environ["AZURE_OPENAI_ENDPOINT"] = azure_endpoint logger.debug("Set AZURE_OPENAI_ENDPOINT from Credential") any_set = True if cred.endpoint_llm: diff --git a/open_notebook/domain/credential.py b/open_notebook/domain/credential.py index a99cd3e2..0e442960 100644 --- a/open_notebook/domain/credential.py +++ b/open_notebook/domain/credential.py @@ -76,6 +76,9 @@ class Credential(ObjectModel): config["api_key"] = self.api_key.get_secret_value() if self.base_url: config["base_url"] = self.base_url + # For Azure, base_url from the UI form maps to endpoint + if self.provider and self.provider.lower() == "azure" and not self.endpoint: + config["endpoint"] = self.base_url if self.endpoint: config["endpoint"] = self.endpoint if self.api_version: From b7788ec05a57815fd58886aa864341a5a6742357 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:23:46 -0300 Subject: [PATCH 009/345] chore(deps): bump tornado from 6.5.4 to 6.5.5 (#668) Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.4 to 6.5.5. - [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.5.4...v6.5.5) --- updated-dependencies: - dependency-name: tornado dependency-version: 6.5.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/uv.lock b/uv.lock index 812994be..b3528780 100644 --- a/uv.lock +++ b/uv.lock @@ -3441,21 +3441,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.4" +version = "6.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, - { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, ] [[package]] From 9c73299da0bccc8f8408f0d05aeadfefa874439d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 04:20:11 +0000 Subject: [PATCH 010/345] chore(deps): bump next from 16.1.7 to 16.2.3 in /frontend Bumps [next](https://github.com/vercel/next.js) from 16.1.7 to 16.2.3. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v16.1.7...v16.2.3) --- updated-dependencies: - dependency-name: next dependency-version: 16.2.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 82 +++++++++++++++++++------------------- frontend/package.json | 2 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f2e0ccfd..542f17b2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,7 @@ "i18next": "^25.7.3", "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.525.0", - "next": "^16.1.7", + "next": "^16.2.3", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -1829,9 +1829,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", - "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1844,9 +1844,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", - "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -1860,9 +1860,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", - "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -1876,9 +1876,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", - "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -1892,9 +1892,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", - "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -1908,9 +1908,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", - "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -1924,9 +1924,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", - "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -1940,9 +1940,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", - "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -1956,9 +1956,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", - "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -9612,12 +9612,12 @@ "dev": true }, "node_modules/next": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", - "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.1.7", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -9631,15 +9631,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.7", - "@next/swc-darwin-x64": "16.1.7", - "@next/swc-linux-arm64-gnu": "16.1.7", - "@next/swc-linux-arm64-musl": "16.1.7", - "@next/swc-linux-x64-gnu": "16.1.7", - "@next/swc-linux-x64-musl": "16.1.7", - "@next/swc-win32-arm64-msvc": "16.1.7", - "@next/swc-win32-x64-msvc": "16.1.7", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/frontend/package.json b/frontend/package.json index cb8b2108..004769c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ "i18next": "^25.7.3", "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.525.0", - "next": "^16.1.7", + "next": "^16.2.3", "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", From 6b23e7cee8e0bc0893bb6e82c741f03ea42f803e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:40:02 +0000 Subject: [PATCH 011/345] chore(deps): bump axios from 1.13.5 to 1.15.0 in /frontend Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.15.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 20 ++++++++++++-------- frontend/package.json | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f2e0ccfd..d6b974a3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,7 +29,7 @@ "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.83.0", "@uiw/react-md-editor": "^4.0.8", - "axios": "^1.13.5", + "axios": "^1.15.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -5365,14 +5365,14 @@ } }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/axobject-query": { @@ -10131,9 +10131,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", diff --git a/frontend/package.json b/frontend/package.json index cb8b2108..03a66114 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,7 +33,7 @@ "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.83.0", "@uiw/react-md-editor": "^4.0.8", - "axios": "^1.13.5", + "axios": "^1.15.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", From ba01f7df4e8d12acdfd032a5a52c59cba8f54d86 Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Sun, 12 Apr 2026 21:22:37 -0300 Subject: [PATCH 012/345] fix: handle credential decryption errors gracefully (#740) - Credential.get_all() now uses per-row error handling instead of failing on first bad row - Broken credentials include decryption_error field with descriptive message - DELETE endpoint falls back to direct DB delete when credential can't be decrypted - Frontend shows amber warning alert for broken credentials with disabled test/edit/discover - Added i18n translation keys for decryption error warning in all 9 locales --- api/credentials_service.py | 1 + api/models.py | 1 + api/routers/credentials.py | 46 ++++++++++++++-- .../(dashboard)/settings/api-keys/page.tsx | 17 +++++- frontend/src/lib/api/credentials.ts | 1 + frontend/src/lib/locales/bn-IN/index.ts | 2 + frontend/src/lib/locales/en-US/index.ts | 2 + frontend/src/lib/locales/fr-FR/index.ts | 2 + frontend/src/lib/locales/it-IT/index.ts | 2 + frontend/src/lib/locales/ja-JP/index.ts | 2 + frontend/src/lib/locales/pt-BR/index.ts | 2 + frontend/src/lib/locales/ru-RU/index.ts | 2 + frontend/src/lib/locales/zh-CN/index.ts | 2 + frontend/src/lib/locales/zh-TW/index.ts | 2 + open_notebook/domain/credential.py | 54 +++++++++++++++---- 15 files changed, 120 insertions(+), 18 deletions(-) diff --git a/api/credentials_service.py b/api/credentials_service.py index 9ad77f3e..8f288eb7 100644 --- a/api/credentials_service.py +++ b/api/credentials_service.py @@ -223,6 +223,7 @@ def credential_to_response(cred: Credential, model_count: int = 0) -> Credential created=str(cred.created) if cred.created else "", updated=str(cred.updated) if cred.updated else "", model_count=model_count, + decryption_error=cred.decryption_error, ) diff --git a/api/models.py b/api/models.py index 7ea8967e..4be00496 100644 --- a/api/models.py +++ b/api/models.py @@ -617,6 +617,7 @@ class CredentialResponse(BaseModel): created: str updated: str model_count: int = 0 + decryption_error: Optional[str] = None class CredentialDeleteResponse(BaseModel): diff --git a/api/routers/credentials.py b/api/routers/credentials.py index b3cf86f0..38e3a2dc 100644 --- a/api/routers/credentials.py +++ b/api/routers/credentials.py @@ -27,16 +27,22 @@ from pydantic import SecretStr from api.credentials_service import ( credential_to_response, discover_with_config, - migrate_from_env as svc_migrate_from_env, - migrate_from_provider_config as svc_migrate_from_provider_config, + get_provider_status, register_models, require_encryption_key, - test_credential as svc_test_credential, validate_url, ) from api.credentials_service import ( get_env_status as svc_get_env_status, - get_provider_status, +) +from api.credentials_service import ( + migrate_from_env as svc_migrate_from_env, +) +from api.credentials_service import ( + migrate_from_provider_config as svc_migrate_from_provider_config, +) +from api.credentials_service import ( + test_credential as svc_test_credential, ) from api.models import ( CreateCredentialRequest, @@ -48,6 +54,7 @@ from api.models import ( RegisterModelsResponse, UpdateCredentialRequest, ) +from open_notebook.database.repository import ensure_record_id, repo_delete, repo_query from open_notebook.domain.credential import Credential router = APIRouter(prefix="/credentials", tags=["credentials"]) @@ -260,7 +267,36 @@ async def delete_credential( - Otherwise, linked models are cascade-deleted automatically """ try: - cred = await Credential.get(credential_id) + try: + cred = await Credential.get(credential_id) + except Exception as decrypt_err: + # Credential exists but can't be decrypted (wrong encryption key). + # Fall back to direct DB operations for deletion. + logger.warning( + f"Cannot decrypt credential {credential_id}, " + f"falling back to direct delete: {decrypt_err}" + ) + + # Delete linked models directly + linked = await repo_query( + "SELECT * FROM model WHERE credential = $cred_id", + {"cred_id": ensure_record_id(credential_id)}, + ) + deleted_models = 0 + for model_row in linked: + model_id = str(model_row.get("id", "")) + if model_id: + await repo_delete(model_id) + deleted_models += 1 + + # Delete the credential itself + await repo_delete(credential_id) + + return CredentialDeleteResponse( + message="Credential deleted successfully", + deleted_models=deleted_models, + ) + linked_models = await cred.get_linked_models() deleted_models = 0 diff --git a/frontend/src/app/(dashboard)/settings/api-keys/page.tsx b/frontend/src/app/(dashboard)/settings/api-keys/page.tsx index 27bde0ec..086fb1a1 100644 --- a/frontend/src/app/(dashboard)/settings/api-keys/page.tsx +++ b/frontend/src/app/(dashboard)/settings/api-keys/page.tsx @@ -22,6 +22,7 @@ import { RefreshCw, Key, ShieldAlert, + AlertTriangle, Plus, Edit, Trash2, @@ -822,7 +823,7 @@ function CredentialItem({ - @@ -214,7 +214,7 @@ export function RebuildEmbeddings() { - {t.advanced.rebuild.failed}: {(rebuildMutation.error as Error)?.message || t.common.error} + {t('advanced.rebuild.failed')}: {(rebuildMutation.error as Error)?.message || t('common.error')} )} @@ -232,21 +232,21 @@ export function RebuildEmbeddings() { {status.status === 'failed' && }
- {status.status === 'queued' && t.advanced.rebuild.queued} - {status.status === 'running' && t.advanced.rebuild.running} - {status.status === 'completed' && t.advanced.rebuild.completed} - {status.status === 'failed' && t.advanced.rebuild.failed} + {status.status === 'queued' && t('advanced.rebuild.queued')} + {status.status === 'running' && t('advanced.rebuild.running')} + {status.status === 'completed' && t('advanced.rebuild.completed')} + {status.status === 'failed' && t('advanced.rebuild.failed')} {status.status === 'running' && ( - {t.advanced.rebuild.leavePageHint} + {t('advanced.rebuild.leavePageHint')} )}
{(status.status === 'completed' || status.status === 'failed') && ( )} @@ -254,9 +254,9 @@ export function RebuildEmbeddings() { {progressData && (
- {t.common.progress} + {t('common.progress')} - {t.advanced.rebuild.itemsProcessed + {t('advanced.rebuild.itemsProcessed') .replace('{processed}', processedItems.toString()) .replace('{total}', totalItems.toString()) .replace('{percent}', progressPercent.toFixed(1))} @@ -265,7 +265,7 @@ export function RebuildEmbeddings() { {failedItems > 0 && (

- ⚠️ {t.advanced.rebuild.failedItems.replace('{count}', failedItems.toString())} + ⚠️ {t('advanced.rebuild.failedItems').replace('{count}', failedItems.toString())}

)}
@@ -274,19 +274,19 @@ export function RebuildEmbeddings() { {stats && (
-

{t.navigation.sources}

+

{t('navigation.sources')}

{sourcesProcessed}

-

{t.common.notes}

+

{t('common.notes')}

{notesProcessed}

-

{t.common.insights}

+

{t('common.insights')}

{insightsProcessed}

-

{t.advanced.rebuild.time}

+

{t('advanced.rebuild.time')}

{processingTimeSeconds !== undefined ? `${processingTimeSeconds.toFixed(1)}s` : '—'}

@@ -303,9 +303,9 @@ export function RebuildEmbeddings() { {status.started_at && (
-

{t.common.created.replace('{time}', new Date(status.started_at).toLocaleString())}

+

{t('common.created').replace('{time}', new Date(status.started_at).toLocaleString())}

{status.completed_at && ( -

{t.notebooks.updated}: {new Date(status.completed_at).toLocaleString()}

+

{t('notebooks.updated')}: {new Date(status.completed_at).toLocaleString()}

)}
)} @@ -315,23 +315,23 @@ export function RebuildEmbeddings() { {/* Help Section */} - {t.advanced.rebuild.whenToRebuild} + {t('advanced.rebuild.whenToRebuild')} -

{t.advanced.rebuild.whenToRebuildAns}

+

{t('advanced.rebuild.whenToRebuildAns')}

- {t.advanced.rebuild.howLong} + {t('advanced.rebuild.howLong')} -

{t.advanced.rebuild.howLongAns}

+

{t('advanced.rebuild.howLongAns')}

- {t.advanced.rebuild.isSafe} + {t('advanced.rebuild.isSafe')} -

{t.advanced.rebuild.isSafeAns}

+

{t('advanced.rebuild.isSafeAns')}

diff --git a/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx b/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx index 2b411d4e..1390822e 100644 --- a/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx +++ b/frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx @@ -34,8 +34,8 @@ export function SystemInfo() { return (
-

{t.advanced.systemInfo}

-
{t.common.loading}
+

{t('advanced.systemInfo')}

+
{t('common.loading')}
) @@ -44,37 +44,37 @@ export function SystemInfo() { return (
-

{t.advanced.systemInfo}

+

{t('advanced.systemInfo')}

{/* Current Version */}
- {t.advanced.currentVersion} - {config?.version || t.advanced.unknown} + {t('advanced.currentVersion')} + {config?.version || t('advanced.unknown')}
{/* Latest Version */} {config?.latestVersion && (
- {t.advanced.latestVersion} + {t('advanced.latestVersion')} {config.latestVersion}
)} {/* Update Status */}
- {t.advanced.status} + {t('advanced.status')} {config?.hasUpdate ? ( - {t.advanced.updateAvailable.replace('{version}', config.latestVersion || '')} + {t('advanced.updateAvailable').replace('{version}', config.latestVersion || '')} ) : config?.latestVersion ? ( - {t.advanced.upToDate} + {t('advanced.upToDate')} ) : ( - {t.advanced.unknown} + {t('advanced.unknown')} )}
@@ -88,7 +88,7 @@ export function SystemInfo() { rel="noopener noreferrer" className="text-sm text-primary hover:underline inline-flex items-center gap-1" > - {t.advanced.viewOnGithub} + {t('advanced.viewOnGithub')} - {t.advanced.updateCheckFailed} + {t('advanced.updateCheckFailed')}
)}
diff --git a/frontend/src/app/(dashboard)/advanced/page.tsx b/frontend/src/app/(dashboard)/advanced/page.tsx index d20331d5..7eb14b22 100644 --- a/frontend/src/app/(dashboard)/advanced/page.tsx +++ b/frontend/src/app/(dashboard)/advanced/page.tsx @@ -13,9 +13,9 @@ export default function AdvancedPage() {
-

{t.advanced.title}

+

{t('advanced.title')}

- {t.advanced.desc} + {t('advanced.desc')}

diff --git a/frontend/src/app/(dashboard)/notebooks/[id]/page.tsx b/frontend/src/app/(dashboard)/notebooks/[id]/page.tsx index 16792a2d..09085fe3 100644 --- a/frontend/src/app/(dashboard)/notebooks/[id]/page.tsx +++ b/frontend/src/app/(dashboard)/notebooks/[id]/page.tsx @@ -119,8 +119,8 @@ export default function NotebookPage() { return (
-

{t.notebooks.notFound}

-

{t.notebooks.notFoundDesc}

+

{t('notebooks.notFound')}

+

{t('notebooks.notFoundDesc')}

) @@ -142,15 +142,15 @@ export default function NotebookPage() { - {t.navigation.sources} + {t('navigation.sources')} - {t.common.notes} + {t('common.notes')} - {t.common.chat} + {t('common.chat')} diff --git a/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx b/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx index 181e6304..befc6f96 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx @@ -83,8 +83,8 @@ export function ChatColumn({ notebookId, contextSelections, sources, sourcesLoad
-

{t.chat.unableToLoadChat}

-

{t.common.refreshPage || 'Please try refreshing the page'}

+

{t('chat.unableToLoadChat')}

+

{t('common.refreshPage') || 'Please try refreshing the page'}

@@ -93,7 +93,7 @@ export function ChatColumn({ notebookId, contextSelections, sources, sourcesLoad return ( - {isEditing ? t.sources.editNote : t.sources.createNote} + {isEditing ? t('sources.editNote') : t('sources.createNote')}
{isEditing && noteLoading ? (
- {t.common.loading} + {t('common.loading')}
) : ( <> @@ -139,8 +139,8 @@ export function NoteEditorDialog({ open, onOpenChange, notebookId, note }: NoteE name="title" value={watchTitle ?? ''} onSave={(value) => setValue('title', value || '')} - placeholder={t.sources.addTitle} - emptyText={t.sources.untitledNote} + placeholder={t('sources.addTitle')} + emptyText={t('sources.untitledNote')} className="text-xl font-semibold" inputClassName="text-xl font-semibold" /> @@ -160,7 +160,7 @@ export function NoteEditorDialog({ open, onOpenChange, notebookId, note }: NoteE value={field.value} onChange={field.onChange} height={420} - placeholder={t.sources.writeNotePlaceholder} + placeholder={t('sources.writeNotePlaceholder')} className={cn( "w-full h-full min-h-[420px] max-h-[500px] overflow-hidden [&_.w-md-editor]:!static [&_.w-md-editor]:!w-full [&_.w-md-editor]:!h-full [&_.w-md-editor-content]:overflow-y-auto", !isEditorFullscreen && "rounded-md border" @@ -177,17 +177,17 @@ export function NoteEditorDialog({ open, onOpenChange, notebookId, note }: NoteE
diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx index aa25e2cd..a8937aeb 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx @@ -55,7 +55,7 @@ export function NotebookCard({ notebook }: NotebookCardProps) { {notebook.archived && ( - {t.notebooks.archived} + {t('notebooks.archived')} )}
@@ -76,12 +76,12 @@ export function NotebookCard({ notebook }: NotebookCardProps) { {notebook.archived ? ( <> - {t.notebooks.unarchive} + {t('notebooks.unarchive')} ) : ( <> - {t.notebooks.archive} + {t('notebooks.archive')} )} @@ -93,7 +93,7 @@ export function NotebookCard({ notebook }: NotebookCardProps) { className="text-red-600" > - {t.common.delete} + {t('common.delete')} @@ -102,11 +102,11 @@ export function NotebookCard({ notebook }: NotebookCardProps) { - {notebook.description || t.chat.noDescription} + {notebook.description || t('chat.noDescription')}
- {t.common.updated.replace('{time}', formatDistanceToNow(new Date(notebook.updated), { + {t('common.updated').replace('{time}', formatDistanceToNow(new Date(notebook.updated), { addSuffix: true, locale: getDateLocale(language) }))} diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookDeleteDialog.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookDeleteDialog.tsx index 202f7ede..c99ca362 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookDeleteDialog.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookDeleteDialog.tsx @@ -69,9 +69,9 @@ export function NotebookDeleteDialog({ - {t.notebooks.deleteNotebook} + {t('notebooks.deleteNotebook')} - {t.notebooks.deleteNotebookDesc.replace('{name}', notebookName)} + {t('notebooks.deleteNotebookDesc').replace('{name}', notebookName)} @@ -79,11 +79,11 @@ export function NotebookDeleteDialog({ {isLoadingPreview ? (
- {t.notebooks.deleteNotebookLoading} + {t('notebooks.deleteNotebookLoading')}
) : previewError ? (
- {t.common.error}: {previewError.message || 'Failed to load preview'} + {t('common.error')}: {previewError.message || 'Failed to load preview'}
) : preview ? ( <> @@ -91,13 +91,13 @@ export function NotebookDeleteDialog({
{preview.note_count > 0 ? (

- {t.notebooks.deleteNotebookNotes.replace( + {t('notebooks.deleteNotebookNotes').replace( '{count}', String(preview.note_count) )}

) : ( -

{t.notebooks.deleteNotebookNoNotes}

+

{t('notebooks.deleteNotebookNoNotes')}

)}
@@ -105,7 +105,7 @@ export function NotebookDeleteDialog({ {preview.shared_source_count > 0 && (

- {t.notebooks.deleteNotebookSharedSources.replace( + {t('notebooks.deleteNotebookSharedSources').replace( '{count}', String(preview.shared_source_count) )} @@ -116,7 +116,7 @@ export function NotebookDeleteDialog({ {/* No sources message */} {preview.exclusive_source_count === 0 && preview.shared_source_count === 0 && (

-

{t.notebooks.deleteNotebookNoSources}

+

{t('notebooks.deleteNotebookNoSources')}

)} @@ -124,7 +124,7 @@ export function NotebookDeleteDialog({ {preview.exclusive_source_count > 0 && (

- {t.notebooks.deleteNotebookExclusiveSources.replace( + {t('notebooks.deleteNotebookExclusiveSources').replace( '{count}', String(preview.exclusive_source_count) )} @@ -137,13 +137,13 @@ export function NotebookDeleteDialog({

@@ -154,7 +154,7 @@ export function NotebookDeleteDialog({
- {t.common.cancel} + {t('common.cancel')} - {t.common.deleting} + {t('common.deleting')} ) : ( - t.common.delete + t('common.delete') )} diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx index 76e36e60..43d452cd 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx @@ -61,10 +61,10 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { onSave={handleUpdateName} className="text-2xl font-bold" inputClassName="text-2xl font-bold" - placeholder={t.notebooks.namePlaceholder} + placeholder={t('notebooks.namePlaceholder')} /> {notebook.archived && ( - {t.notebooks.archived} + {t('notebooks.archived')} )}
@@ -76,12 +76,12 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { {notebook.archived ? ( <> - {t.notebooks.unarchive} + {t('notebooks.unarchive')} ) : ( <> - {t.notebooks.archive} + {t('notebooks.archive')} )} @@ -92,7 +92,7 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { className="text-red-600 hover:text-red-700" > - {t.common.delete} + {t('common.delete')}
@@ -104,14 +104,14 @@ export function NotebookHeader({ notebook }: NotebookHeaderProps) { onSave={handleUpdateDescription} className="text-muted-foreground" inputClassName="text-muted-foreground" - placeholder={t.notebooks.addDescription} + placeholder={t('notebooks.addDescription')} multiline - emptyText={t.notebooks.addDescription} + emptyText={t('notebooks.addDescription')} />
- {t.common.created.replace('{time}', formatDistanceToNow(new Date(notebook.created), { addSuffix: true, locale: dfLocale }))} • - {t.common.updated.replace('{time}', formatDistanceToNow(new Date(notebook.updated), { addSuffix: true, locale: dfLocale }))} + {t('common.created').replace('{time}', formatDistanceToNow(new Date(notebook.created), { addSuffix: true, locale: dfLocale }))} • + {t('common.updated').replace('{time}', formatDistanceToNow(new Date(notebook.updated), { addSuffix: true, locale: dfLocale }))}
diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx index a09efa82..c51ba4ed 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx @@ -45,8 +45,8 @@ export function NotebookList({ return ( diff --git a/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx b/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx index bb915f27..cda02b39 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx @@ -51,8 +51,8 @@ export function NotesColumn({ // Collapsible column state const { notesCollapsed, toggleNotes } = useNotebookColumnsStore() const collapseButton = useMemo( - () => createCollapseButton(toggleNotes, t.common.notes), - [toggleNotes, t.common.notes] + () => createCollapseButton(toggleNotes, t('common.notes')), + [toggleNotes, t('common.notes')] ) const handleDeleteClick = (noteId: string) => { @@ -78,12 +78,12 @@ export function NotesColumn({ isCollapsed={notesCollapsed} onToggle={toggleNotes} collapsedIcon={StickyNote} - collapsedLabel={t.common.notes} + collapsedLabel={t('common.notes')} >
- {t.common.notes} + {t('common.notes')}
{collapseButton}
@@ -108,8 +108,8 @@ export function NotesColumn({ ) : !notes || notes.length === 0 ? ( ) : (
@@ -127,7 +127,7 @@ export function NotesColumn({ )} - {note.note_type === 'ai' ? t.common.aiGenerated : t.common.human} + {note.note_type === 'ai' ? t('common.aiGenerated') : t('common.human')}
@@ -171,7 +171,7 @@ export function NotesColumn({ className="text-red-600 focus:text-red-600" > - {t.notebooks.deleteNote} + {t('notebooks.deleteNote')} @@ -212,9 +212,9 @@ export function NotesColumn({ createCollapseButton(toggleSources, t.navigation.sources), - [toggleSources, t.navigation.sources] + () => createCollapseButton(toggleSources, t('navigation.sources')), + [toggleSources, t('navigation.sources')] ) // Scroll container ref for infinite scroll @@ -151,29 +151,29 @@ export function SourcesColumn({ isCollapsed={sourcesCollapsed} onToggle={toggleSources} collapsedIcon={FileText} - collapsedLabel={t.navigation.sources} + collapsedLabel={t('navigation.sources')} >
- {t.navigation.sources} + {t('navigation.sources')}
{ setDropdownOpen(false); setAddDialogOpen(true); }}> - {t.sources.addSource} + {t('sources.addSource')} { setDropdownOpen(false); setAddExistingDialogOpen(true); }}> - {t.sources.addExistingTitle} + {t('sources.addExistingTitle')} @@ -190,8 +190,8 @@ export function SourcesColumn({ ) : !sources || sources.length === 0 ? ( ) : (
@@ -240,9 +240,9 @@ export function SourcesColumn({
-

{t.notebooks.title}

+

{t('notebooks.title')}

@@ -64,14 +64,14 @@ export default function NotebooksPage() { name="notebook-search" value={searchTerm} onChange={(event) => setSearchTerm(event.target.value)} - placeholder={t.notebooks.searchPlaceholder} + placeholder={t('notebooks.searchPlaceholder')} autoComplete="off" - aria-label={t.common.accessibility?.searchNotebooks || "Search notebooks"} + aria-label={t('common.accessibility.searchNotebooks') || "Search notebooks"} className="w-full sm:w-64" />
@@ -80,21 +80,21 @@ export default function NotebooksPage() { setCreateDialogOpen(true) : undefined} - actionLabel={!isSearching ? t.notebooks.newNotebook : undefined} + actionLabel={!isSearching ? t('notebooks.newNotebook') : undefined} /> {hasArchived && ( )}
diff --git a/frontend/src/app/(dashboard)/podcasts/page.tsx b/frontend/src/app/(dashboard)/podcasts/page.tsx index 8616464b..2dddabc7 100644 --- a/frontend/src/app/(dashboard)/podcasts/page.tsx +++ b/frontend/src/app/(dashboard)/podcasts/page.tsx @@ -29,18 +29,18 @@ export default function PodcastsPage() {
-

{t.podcasts.listTitle}

+

{t('podcasts.listTitle')}

- {t.podcasts.listDesc} + {t('podcasts.listDesc')}

{hasUnconfiguredProfiles ? ( - {t.podcasts.setupRequired} + {t('podcasts.setupRequired')} - {t.podcasts.setupRequiredDesc} + {t('podcasts.setupRequiredDesc')} ) : null} @@ -51,15 +51,15 @@ export default function PodcastsPage() { className="space-y-6" >
-

{t.podcasts.chooseAView}

- +

{t('podcasts.chooseAView')}

+ - {t.podcasts.episodesTab} + {t('podcasts.episodesTab')} - {t.podcasts.templatesTab} + {t('podcasts.templatesTab')}
diff --git a/frontend/src/app/(dashboard)/search/page.tsx b/frontend/src/app/(dashboard)/search/page.tsx index ffd3fd31..8c864299 100644 --- a/frontend/src/app/(dashboard)/search/page.tsx +++ b/frontend/src/app/(dashboard)/search/page.tsx @@ -72,7 +72,7 @@ export default function SearchPage() { }, [availableModels]) const resolveModelName = (id?: string | null) => { - if (!id) return t.searchPage.notSet + if (!id) return t('searchPage.notSet') return modelNameById.get(id) ?? id } @@ -159,19 +159,19 @@ export default function SearchPage() { return (
-

{t.searchPage.askAndSearch}

+

{t('searchPage.askAndSearch')}

setActiveTab(v as 'ask' | 'search')} className="w-full space-y-6">
-

{t.searchPage.chooseAMode}

- +

{t('searchPage.chooseAMode')}

+ - {t.searchPage.askBeta} + {t('searchPage.askBeta')} - {t.searchPage.search} + {t('searchPage.search')}
@@ -179,19 +179,19 @@ export default function SearchPage() { - {t.searchPage.askYourKb} + {t('searchPage.askYourKb')}

- {t.searchPage.askYourKbDesc} + {t('searchPage.askYourKbDesc')}

{/* Question Input */}
- +