Skip to content

Commit b4ef062

Browse files
Merge branch 'master' into a11y/announce-streamed-reply
2 parents e73d868 + 29713f2 commit b4ef062

32 files changed

Lines changed: 1204 additions & 40 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ Mintplex Labs & the community maintain a number of deployment methods, scripts,
188188
|----------------------------------------|----|-----|---------------|------------|
189189
| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] |
190190

191-
| Railway | RepoCloud | Elestio | Northflank |
192-
| --------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
193-
| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | [![Deploy on Elestio][elestio-btn]][elestio-deploy] | [![Deploy on Northflank][northflank-btn]][northflank-deploy] |
191+
| Railway | RepoCloud | Elestio | Northflank | Sealos |
192+
| --------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
193+
| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | [![Deploy on Elestio][elestio-btn]][elestio-deploy] | [![Deploy on Northflank][northflank-btn]][northflank-deploy] | [![Deploy on Sealos][sealos-btn]][sealos-deploy] |
194194

195195
[or set up a production AnythingLLM instance without Docker →](./BARE_METAL.md)
196196

@@ -316,3 +316,5 @@ This project is [MIT](./LICENSE) licensed.
316316
[elestio-deploy]: https://elest.io/open-source/anythingllm
317317
[northflank-btn]: https://assets.northflank.com/deploy_to_northflank_smm_36700fb050.svg
318318
[northflank-deploy]: https://northflank.com/stacks/deploy-anythingllm
319+
[sealos-btn]: https://sealos.io/Deploy-on-Sealos.svg
320+
[sealos-deploy]: https://sealos.io/products/app-store/anything-llm

SECURITY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ If your report is about being able to access the system via _bypassing the authe
4646

4747
If your report depends on an attacker knowing or guessing a UUID (e.g., a session ID, embed ID, or any other random identifier) without demonstrating a practical way to obtain it, this is not a valid report. UUIDs (v4) have 122 bits of entropy and are not feasible to brute-force. Unless your report includes a concrete method to leak or enumerate the UUID in question, it will be closed immediately.
4848

49+
### Reports based on Developer API Key access
50+
51+
Developer API keys are **system-level credentials**. They are only creatable by administrators and intentionally grant full, unrestricted access to the entire `/v1/*` API surface — equivalent to admin access. If your report's attack chain starts with "attacker has a developer API key," you are describing an attacker who already has admin-equivalent access. There is no privilege escalation possible from a developer API key because it is already the highest privilege level.
52+
53+
This includes reports about API keys being able to mint auth tokens, access admin endpoints, or perform actions on behalf of other users. All of these are intended capabilities of the API key system. If you believe a developer API key should have scoped or limited permissions, that is a feature request, not a vulnerability.
54+
4955
### Reports about admin-enabled agent tools doing "too much"
5056

5157
Several agent tools (e.g., `sql-agent`, filesystem tools) are disabled by default and require an administrator to explicitly enable and configure them. If your report is that an admin-enabled tool can perform actions beyond what you think it should (writes via SQL, broad file access, etc.), this is not a valid report. The admin who enables the tool and configures its access (connection strings, paths, credentials) is making an intentional decision about what the agent can do. We provide UI-level warnings but intentionally do not restrict functionality — the user should be able to do whatever they want with their own systems.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* eslint-env jest, node */
2+
const OCRLoader = require("../../../utils/OCRLoader");
3+
4+
describe("toSharpRawInput", () => {
5+
test("expands packed 1-bit DeviceGray so Sharp gets integer channels", () => {
6+
const packed = Buffer.from([0b10101010]);
7+
const raw = OCRLoader.toSharpRawInput({
8+
width: 8,
9+
height: 1,
10+
data: packed,
11+
kind: 1,
12+
});
13+
expect(raw.channels).toBe(1);
14+
expect(raw.data).toEqual(Buffer.from([255, 0, 255, 0, 255, 0, 255, 0]));
15+
});
16+
17+
test("handles widths that are not a multiple of 8 (row padding bits)", () => {
18+
// width 10 -> rowBytes = 2, last 6 bits of each row are padding.
19+
// Row 0: 1111111111 (padding 000000), row 1: 0000000000 (padding 111111)
20+
const packed = Buffer.from([0b11111111, 0b11000000, 0b00000000, 0b00111111]);
21+
const raw = OCRLoader.toSharpRawInput({
22+
width: 10,
23+
height: 2,
24+
data: packed,
25+
kind: 1,
26+
});
27+
expect(raw.channels).toBe(1);
28+
expect(raw.data.length).toBe(20);
29+
expect(raw.data).toEqual(
30+
Buffer.from([...Array(10).fill(255), ...Array(10).fill(0)])
31+
);
32+
});
33+
34+
test("detects packed 1-bit by buffer length when kind is omitted", () => {
35+
const width = 2480;
36+
const height = 2;
37+
const packed = Buffer.alloc(Math.ceil(width / 8) * height, 0xff);
38+
const raw = OCRLoader.toSharpRawInput({ width, height, data: packed });
39+
expect(raw.channels).toBe(1);
40+
expect(raw.data.length).toBe(width * height);
41+
expect(raw.data[0]).toBe(255);
42+
});
43+
44+
test("passes through 8-bit gray and copies the buffer", () => {
45+
const data = Buffer.from([1, 2, 3, 4]);
46+
const raw = OCRLoader.toSharpRawInput({ width: 2, height: 2, data });
47+
expect(raw.channels).toBe(1);
48+
data[0] = 99;
49+
expect(raw.data[0]).toBe(1);
50+
});
51+
52+
test("passes through RGB", () => {
53+
const data = Buffer.alloc(2 * 2 * 3, 128);
54+
const raw = OCRLoader.toSharpRawInput({ width: 2, height: 2, data });
55+
expect(raw.channels).toBe(3);
56+
expect(raw.data.length).toBe(12);
57+
});
58+
59+
test("returns null for empty or mismatched buffers", () => {
60+
expect(OCRLoader.toSharpRawInput(null)).toBeNull();
61+
expect(
62+
OCRLoader.toSharpRawInput({
63+
width: 8,
64+
height: 1,
65+
data: Buffer.from([1, 2]),
66+
})
67+
).toBeNull();
68+
});
69+
});

collector/utils/OCRLoader/index.js

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,67 @@ class OCRLoader {
274274
}
275275
}
276276

277+
/**
278+
* pdf.js ImageKind.GRAYSCALE_1BPP. Packed 1-bit rows are ceil(width/8) bytes.
279+
* @type {number}
280+
*/
281+
const PDFJS_GRAYSCALE_1BPP = 1;
282+
283+
/**
284+
* Unpack pdf.js packed 1-bit DeviceGray into 8-bit grayscale Sharp can ingest.
285+
* Bit 1 is white, bit 0 is black (PDF DeviceGray).
286+
*
287+
* @param {Buffer|Uint8Array} packed
288+
* @param {number} width
289+
* @param {number} height
290+
* @returns {Buffer|null}
291+
*/
292+
function unpackPackedGray1Bpp(packed, width, height) {
293+
if (!packed || width <= 0 || height <= 0) return null;
294+
const rowBytes = Math.ceil(width / 8);
295+
if (packed.length < rowBytes * height) return null;
296+
297+
const out = Buffer.alloc(width * height);
298+
for (let y = 0; y < height; y++) {
299+
const rowStart = y * rowBytes;
300+
for (let x = 0; x < width; x++) {
301+
const byte = packed[rowStart + (x >> 3)];
302+
const bit = (byte >> (7 - (x & 7))) & 1;
303+
out[y * width + x] = bit ? 255 : 0;
304+
}
305+
}
306+
return out;
307+
}
308+
309+
/**
310+
* Build a Sharp `raw` input from a pdf.js image object.
311+
* Copies the source buffer so concurrent OCR workers cannot share backing memory.
312+
* 1-bit CCITT/bitonal scans (issue #6118) are expanded to 8-bit gray.
313+
*
314+
* @param {{width?: number, height?: number, data?: ArrayLike<number>, kind?: number}} img
315+
* @returns {{data: Buffer, width: number, height: number, channels: number}|null}
316+
*/
317+
function toSharpRawInput(img) {
318+
const width = img?.width;
319+
const height = img?.height;
320+
if (!width || !height || !img.data) return null;
321+
322+
const src = Buffer.from(img.data);
323+
const isPacked1Bpp =
324+
img.kind === PDFJS_GRAYSCALE_1BPP ||
325+
src.length === Math.ceil(width / 8) * height;
326+
327+
if (isPacked1Bpp) {
328+
const unpacked = unpackPackedGray1Bpp(src, width, height);
329+
if (!unpacked) return null;
330+
return { data: unpacked, width, height, channels: 1 };
331+
}
332+
333+
const channels = src.length / width / height;
334+
if (![1, 2, 3, 4].includes(channels)) return null;
335+
return { data: src, width, height, channels };
336+
}
337+
277338
/**
278339
* Converts a PDF page to a buffer using Sharp.
279340
* @param {Object} options - The options for the Sharp PDF page object.
@@ -313,14 +374,14 @@ class PDFSharp {
313374

314375
const name = ops.argsArray[i][0];
315376
const img = await page.objs.get(name);
316-
const { width, height } = img;
317-
const size = img.data.length;
318-
const channels = size / width / height;
377+
const raw = toSharpRawInput(img);
378+
if (!raw) continue;
379+
const { data, width, height, channels } = raw;
319380
const targetDPI = 70;
320381
const targetWidth = Math.floor(width * (targetDPI / 72));
321382
const targetHeight = Math.floor(height * (targetDPI / 72));
322383

323-
const image = this.sharp(img.data, {
384+
const image = this.sharp(data, {
324385
raw: { width, height, channels },
325386
density: targetDPI,
326387
})
@@ -352,4 +413,6 @@ class PDFSharp {
352413
}
353414
}
354415

416+
OCRLoader.toSharpRawInput = toSharpRawInput;
417+
OCRLoader.unpackPackedGray1Bpp = unpackPackedGray1Bpp;
355418
module.exports = OCRLoader;
6.75 KB
Loading
8.4 KB
Loading

frontend/src/components/EmbeddingSelection/GeminiOptions/index.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const DEFAULT_MODELS = [
66
id: "gemini-embedding-001",
77
name: "Gemini Embedding 001",
88
},
9+
{
10+
id: "gemini-embedding-2",
11+
name: "Gemini Embedding 2",
12+
},
913
];
1014

1115
export default function GeminiOptions({ settings }) {

frontend/src/components/LLMSelection/AwsBedrockLLMOptions/index.jsx

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,35 +128,45 @@ export default function AwsBedrockLLMOptions({ settings }) {
128128
);
129129
}
130130

131+
const MANUAL_MODEL_ENTRY = "-- Enter model ID manually --";
132+
131133
function BedrockModelSelection({ settings, apiKey, region }) {
132134
const [groupedModels, setGroupedModels] = useState({});
133135
const [loading, setLoading] = useState(true);
136+
const [manualEntry, setManualEntry] = useState(false);
134137

135138
useEffect(() => {
136139
async function findCustomModels() {
137140
setLoading(true);
138-
const { models } = await System.customModels(
141+
const { models = [] } = await System.customModels(
139142
"bedrock",
140143
apiKey,
141144
null,
142145
null,
143146
{ region }
144147
);
145-
if (models?.length > 0) {
146-
const modelsByOrganization = models.reduce((acc, model) => {
147-
const org = model.organization || "AWS Bedrock";
148-
acc[org] = acc[org] || [];
149-
acc[org].push(model);
150-
return acc;
151-
}, {});
152-
setGroupedModels(modelsByOrganization);
153-
}
148+
const modelsByOrganization = models.reduce((acc, model) => {
149+
const org = model.organization || "AWS Bedrock";
150+
acc[org] = acc[org] || [];
151+
acc[org].push(model);
152+
return acc;
153+
}, {});
154+
setGroupedModels(modelsByOrganization);
155+
156+
// Saved models not present in the fetched list (eg: cross-region
157+
// inference profile IDs the Mantle listing omits) can only render
158+
// via manual entry - same for an empty list.
159+
const savedModel = settings?.AwsBedrockLLMModel;
160+
const savedModelInList = models.some((model) => model.id === savedModel);
161+
setManualEntry(
162+
models.length === 0 || (!!savedModel && !savedModelInList)
163+
);
154164
setLoading(false);
155165
}
156166
findCustomModels();
157167
}, [apiKey, region]);
158168

159-
if (loading || Object.keys(groupedModels).length === 0) {
169+
if (loading) {
160170
return (
161171
<div className="flex flex-col w-60">
162172
<label className="text-white text-sm font-semibold block mb-3">
@@ -175,6 +185,35 @@ function BedrockModelSelection({ settings, apiKey, region }) {
175185
);
176186
}
177187

188+
if (manualEntry) {
189+
return (
190+
<div className="flex flex-col w-60">
191+
<label className="text-white text-sm font-semibold block mb-3">
192+
Chat Model Selection
193+
</label>
194+
<input
195+
type="text"
196+
name="AwsBedrockLLMModel"
197+
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
198+
placeholder="eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
199+
defaultValue={settings?.AwsBedrockLLMModel}
200+
required={true}
201+
autoComplete="off"
202+
spellCheck={false}
203+
/>
204+
{Object.keys(groupedModels).length > 0 && (
205+
<button
206+
type="button"
207+
onClick={() => setManualEntry(false)}
208+
className="text-white/60 hover:text-white text-xs text-left mt-1.5 underline w-fit"
209+
>
210+
Select from available models
211+
</button>
212+
)}
213+
</div>
214+
);
215+
}
216+
178217
return (
179218
<div className="flex flex-col w-60">
180219
<label className="text-white text-sm font-semibold block mb-3">
@@ -183,6 +222,9 @@ function BedrockModelSelection({ settings, apiKey, region }) {
183222
<select
184223
name="AwsBedrockLLMModel"
185224
required={true}
225+
onChange={(e) => {
226+
if (e.target.value === MANUAL_MODEL_ENTRY) setManualEntry(true);
227+
}}
186228
className="border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
187229
>
188230
{Object.keys(groupedModels)
@@ -200,6 +242,8 @@ function BedrockModelSelection({ settings, apiKey, region }) {
200242
))}
201243
</optgroup>
202244
))}
245+
<option disabled={true}>──────────</option>
246+
<option value={MANUAL_MODEL_ENTRY}>{MANUAL_MODEL_ENTRY}</option>
203247
</select>
204248
</div>
205249
);

frontend/src/components/SettingsSidebar/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default function SettingsSidebar() {
140140
<div>
141141
<Link
142142
to={paths.home()}
143-
className="flex shrink-0 max-w-[55%] items-center justify-start mx-[20.5px] my-[18px]"
143+
className="flex shrink-0 items-center justify-start mx-[20.5px] my-[18px]"
144144
>
145145
<img
146146
src={logo}
6.75 KB
Loading

0 commit comments

Comments
 (0)