Compare commits

..
Author SHA1 Message Date
shamoon 7b3daa313a Fix: fix allowed_hosts default 2026-11-10 07:53:53 -08:00
GitHub Actions 936b1b3bfa Auto translate strings 2026-11-09 06:53:56 +00:00
shamoon e0c33b3d39 Tweak: chat reference tweaks (#14723) 2026-11-08 22:52:34 -08:00
12 changed files with 141 additions and 127 deletions

No files matched your search

@@ -1,23 +1,5 @@
title: "[Feature Request] "
body:
- type: markdown
attributes:
value: |
#### Use of AI tools
If this request was generated, in whole or in part, by an AI tool or agent you **must** say so using the field below. Failing to clearly disclose this is a violation of our [Code of Conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md).
The project prefers thoughtful, well-described ideas to AI-generated content. Please describe the problem, your use case, the desired behavior, and who would benefit.
- type: dropdown
id: ai-usage
attributes:
label: AI usage
description: Was any part of this request written by an AI tool or agent?
options:
- No AI tools were used
- Yes, AI tools helped write this request
validations:
required: true
- type: textarea
id: description
attributes:
-16
View File
@@ -1,21 +1,5 @@
title: "[Support] "
body:
- type: markdown
attributes:
value: |
#### Use of AI tools
If this post was generated, in whole or in part, by an AI tool or agent you **must** say so using the field below. Failing to clearly disclose this is a violation of our [Code of Conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md).
- type: dropdown
id: ai-usage
attributes:
label: AI usage
description: Was any part of this post written by an AI tool or agent?
options:
- No AI tools were used
- Yes, AI tools helped write this post
validations:
required: true
- type: textarea
id: description
attributes:
-44
View File
@@ -1,44 +0,0 @@
# Guidance for AI agents
Read [CONTRIBUTING.md](CONTRIBUTING.md) before working on this repository, especially [Use of AI Tools](CONTRIBUTING.md#use-of-ai-tools). These contribution rules apply to all agent-assisted work.
## Contribution boundaries
- Do not open or submit pull requests, including "on behalf of" a human. The project does not accept PRs from AI agents. Prepare local changes for a human to review, verify, and submit personally.
- The project does not accept PRs that are entirely or mostly AI-derived. Human submission and disclosure do not override this rule.
- AI-generated content must be clearly disclosed as described in the contributing guidelines. When handing work back to a human, identify what was generated or changed with AI assistance so they can disclose it accurately.
- Trust matters. Encourage first-time contributors to discuss their proposed change before preparing a PR, especially for large changes or changes involving AI-generated code. New features and enhancements should almost always target an existing feature request with community interest and discussion.
- The project prefers thoughtful, well-described ideas to AI-generated pull requests. Help contributors describe their problem, use case, desired behavior, and who would benefit. An implementation is not required for a valuable contribution, and a flawed implementation can obscure a useful idea before it receives proper discussion.
- AI tools have lowered the effort needed to produce a PR, but reviewing and maintaining it still requires substantial work. Encourage discussion of the idea before investing in implementation and review.
- Issue reports must describe observed behavior, reproduction steps, and relevant logs. Do not add speculative root causes, code analysis, or proposed fixes to agent-generated reports.
## Security reports
- AI-assisted security reports must meet the legitimacy and verification requirements in [SECURITY.md](SECURITY.md#ai-assisted-submissions). A human must verify a real Paperless-ngx code path, concrete reproduction steps, affected versions, and realistic security impact before submission.
- Account for existing permission checks, configuration requirements, and trust boundaries. Speculation, scanner output, or concerns about third-party software or deployment choices without a demonstrated Paperless-ngx vulnerability do not meet this threshold and may be closed without extended discussion.
- Disclose AI assistance and use the private vulnerability reporting channel specified in [SECURITY.md](SECURITY.md#reporting-a-vulnerability).
## Working on changes
- Read the relevant code, tests, and documentation before editing. Paperless-ngx is complex: trace interactions between affected components rather than assuming a change is isolated.
- Keep changes focused on the requested work and follow existing patterns. Avoid unrelated refactoring, formatting, dependency updates, or generated-file changes.
- Consider effects on permissions, document processing, background tasks, storage, and the API when those areas are involved.
- Add or update meaningful tests for behavior changes and regressions. Do not weaken tests or checks to make a change pass.
- Update user or developer documentation when behavior or setup changes.
- Keep credentials and private documents out of commits, fixtures, logs, and examples.
## Repository and validation
- `src/`: Django back end.
- `src-ui/`: Angular front end, managed with `pnpm`.
- `docs/`: Documentation.
- `docker/` and `scripts/`: Deployment and development tooling.
- Base functional changes on `dev`; documentation-only changes may target `main`.
Follow [the development documentation](docs/development.md) for environment setup and testing. Common checks after setup are:
- Back end: run `uv run pytest` from `src/` (use a test path to focus on the affected area).
- Front end: run `pnpm test` and `pnpm lint` from `src-ui/`.
- Formatting and other pre-commit checks: run `uv run prek run --files <changed-files>` from the repository root.
When handing work back, summarize the changes, the checks actually run and their results, and any remaining uncertainty. Clearly state when a check could not be run; do not claim verification that did not happen. The human contributor must understand and independently verify the contribution before submitting it.
+3 -16
View File
@@ -4,18 +4,6 @@ If you feel like contributing to the project, please do! Bug fixes and improveme
⚠️ Please note: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Pull requests that are opened without meeting this requirement may not be merged.
## Trust and collaboration
Paperless-ngx places a high value on trust between contributors and maintainers. If you have not contributed before, consider starting with a discussion before preparing a pull request, especially for a large contribution or one involving AI-generated code.
Paperless-ngx is complex, and understanding how its components interact takes time. A change that appears correct in isolation can have consequences elsewhere in the application. Contributors should understand the code they submit, explain how it fits into the project, and work with maintainers through review. Starting with smaller, focused contributions can help build that understanding and trust.
We prefer thoughtful, well-described ideas to AI-generated pull requests. You do not need to supply an implementation for your contribution to be valuable. Explain the problem you have encountered, your use case, the behavior you would like to see, and who would benefit. Concrete examples, alternatives you have considered, and relevant constraints help the community discuss and refine the idea.
AI tools have lowered the effort needed to produce a pull request, but evaluating one still requires substantial maintainer time. Maintainers still need to establish whether the implementation is correct, fits the project, and can be maintained over time.
A flawed implementation can also obscure a good idea: discussion becomes focused on problems with the generated code before the community has considered whether the underlying proposal is useful. Starting with the idea gives it a fair discussion on its own merits and lets contributors and maintainers agree on an approach before investing in implementation and review.
If you want to implement something big:
- As above, please start with a discussion! Maybe something similar is already in development and we can make it happen together.
@@ -73,10 +61,9 @@ This project does not specifically prohibit the use of AI tools or agents _durin
1. Any content — code, pull request descriptions, issue reports or comments — that was generated in whole or in part by an AI tool or agent **must be clearly disclosed as such**. Failing to clearly disclose this is considered a violation of our [Code of Conduct](CODE_OF_CONDUCT.md) and may result in your contributions being closed and further action being taken.
2. AI-generated code must not violate copyright protections.
3. We will not accept PRs opened or submitted by AI agents, including agents acting "on behalf of" a human. A human contributor must personally review, verify, and submit the contribution and take responsibility for it throughout review.
4. We will not accept PRs that are entirely or mostly AI-derived.
5. Issue reports must describe _observed behavior only_: what happened, how to reproduce it and the relevant logs. Reports generated by an AI agent should not include code analysis, speculation about the root cause or suggest fixes.
6. Contributors are responsible for everything they submit, regardless of how it was produced. Please do not open issues or PRs you have not verified yourself.
3. We will not accept PRs that are entirely or mostly AI-derived.
4. Issue reports must describe _observed behavior only_: what happened, how to reproduce it and the relevant logs. Reports generated by an AI agent should not include code analysis, speculation about the root cause or suggest fixes.
5. Contributors are responsible for everything they submit, regardless of how it was produced. Please do not open issues or PRs you have not verified yourself.
# Translating Paperless-ngx
+11 -11
View File
@@ -501,20 +501,20 @@ See the [Django project documentation on the settings](https://docs.djangoprojec
#### [`PAPERLESS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_ALLOWED_HOSTS) {#PAPERLESS_ALLOWED_HOSTS}
: If you're planning on putting Paperless on the open internet, then
you really should set this value to the domain name you're using.
Failing to do so leaves you open to HTTP host header attacks.
: If you expose Paperless to the internet, set `PAPERLESS_URL` to its public URL
or list its public hostname in `PAPERLESS_ALLOWED_HOSTS` to help protect against
HTTP Host header attacks.
You can read more about this in [the Django project's documentation](https://docs.djangoproject.com/en/4.1/topics/security/#host-header-validation)
Just remember that this is a comma-separated list, so
"example.com" is fine, as is "example.com,www.example.com", but
NOT " example.com" or "example.com,"
Separate multiple hostnames with commas, as in `example.com,www.example.com`.
Can also be set using PAPERLESS_URL (see above).
"localhost" is always allowed for docker healthcheck
Defaults to "\*", which is all hosts.
- With neither setting configured, Paperless accepts any host.
- With only `PAPERLESS_URL` set, Paperless accepts the hostname from that URL
and `localhost` (used by the Docker health check).
- `PAPERLESS_ALLOWED_HOSTS` adds hosts to `localhost` and the hostname from
`PAPERLESS_URL`, if set. A blank list adds no hosts.
- With `PAPERLESS_ALLOWED_HOSTS=*`, Paperless accepts any host, even when
`PAPERLESS_URL` is set.
#### [`PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list>`](#PAPERLESS_CORS_ALLOWED_HOSTS) {#PAPERLESS_CORS_ALLOWED_HOSTS}
+15 -8
View File
@@ -4030,14 +4030,21 @@
<source>Sources</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">46,47</context>
<context context-type="linenumber">47,48</context>
</context-group>
</trans-unit>
<trans-unit id="6025275430560167531" datatype="html">
<source>+<x id="INTERPOLATION" equiv-text="{{ hiddenReferenceCount(message) }}"/> more</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">49,50</context>
</context-group>
</trans-unit>
<trans-unit id="8066608938393600549" datatype="html">
<source>Message</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">61,62</context>
<context context-type="linenumber">66,67</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.html</context>
@@ -4048,22 +4055,22 @@
<source>Stop</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">74</context>
<context context-type="linenumber">79</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">74</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="6490688569532630280" datatype="html">
<source>Send</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">78</context>
<context context-type="linenumber">83</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">78</context>
<context context-type="linenumber">83</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
@@ -4090,14 +4097,14 @@
<source>Ask a question about this document...</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.ts</context>
<context context-type="linenumber">61</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit id="224049052163028286" datatype="html">
<source>Ask a question about a document...</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.ts</context>
<context context-type="linenumber">62</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit id="8700121026680200191" datatype="html">
@@ -42,11 +42,16 @@
<span class="text-break">{{ message.content }}</span>
}
@if (message.role === 'assistant' && !message.isStreaming && message.references?.length) {
<div class="chat-references list-group mt-3">
<div class="list-group-item text-muted small py-1" i18n>Sources</div>
@for (reference of message.references; track reference.id) {
<a class="list-group-item list-group-item-action d-flex align-items-center text-primary" [routerLink]="['/documents', reference.id]" [title]="reference.title">
<pngx-bs-icon width="0.9em" height="0.9em" name="file-text" class="me-2 flex-shrink-0"></pngx-bs-icon><span class="text-truncate">{{ reference.title }}</span>
<div class="chat-references mt-2 pt-2 border-top">
<div class="d-flex align-items-center text-muted mb-1">
<span i18n>Sources</span>
@if (hiddenReferenceCount(message) > 0 && !expandedReferences().has(message)) {
<button type="button" class="badge rounded-pill bg-body-secondary text-body-secondary fw-normal border-0 ms-2" (click)="expandReferences(message)" i18n>+{{ hiddenReferenceCount(message) }} more</button>
}
</div>
@for (reference of visibleReferences(message); track reference.id) {
<a class="reference-link d-flex align-items-center lh-sm text-primary text-decoration-none py-1" [routerLink]="['/documents', reference.id]" [title]="reference.title">
<pngx-bs-icon width="0.8em" height="0.8em" name="file-text" class="me-2 flex-shrink-0"></pngx-bs-icon><span class="text-truncate">{{ reference.title }}</span>
</a>
}
</div>
@@ -12,6 +12,10 @@
display: none;
}
.message .flex-grow-1 {
min-width: 0;
}
.dropdown-item {
white-space: initial;
}
@@ -14,6 +14,7 @@ import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon
import { MarkdownPipe } from 'src/app/pipes/markdown.pipe'
import {
ChatMessage,
ChatReference,
ChatService,
parseChatResponse,
} from 'src/app/services/chat.service'
@@ -40,8 +41,29 @@ export class ChatComponent implements OnInit {
readonly input = signal('')
readonly documentId = signal<number>(undefined)
readonly slowResponse = signal(false)
readonly expandedReferences = signal<Set<ChatMessage>>(new Set())
public popperOptions = pngxPopperOptions
static readonly COLLAPSED_REFERENCE_COUNT = 2
public visibleReferences(message: ChatMessage): ChatReference[] {
if (this.expandedReferences().has(message)) {
return message.references
}
return message.references.slice(0, ChatComponent.COLLAPSED_REFERENCE_COUNT)
}
public hiddenReferenceCount(message: ChatMessage): number {
return Math.max(
0,
message.references.length - ChatComponent.COLLAPSED_REFERENCE_COUNT
)
}
public expandReferences(message: ChatMessage): void {
this.expandedReferences.update((expanded) => new Set(expanded).add(message))
}
private chatService: ChatService = inject(ChatService)
private router: Router = inject(Router)
+9 -4
View File
@@ -450,7 +450,10 @@ CORS_EXPOSE_HEADERS = [
"Content-Disposition",
]
ALLOWED_HOSTS = get_list_from_env("PAPERLESS_ALLOWED_HOSTS", default=["*"])
if "PAPERLESS_ALLOWED_HOSTS" in os.environ or os.getenv("PAPERLESS_URL"):
ALLOWED_HOSTS = get_list_from_env("PAPERLESS_ALLOWED_HOSTS")
else:
ALLOWED_HOSTS = ["*"]
if ALLOWED_HOSTS != ["*"]:
# always allow localhost. Necessary e.g. for healthcheck in docker.
ALLOWED_HOSTS.append("localhost")
@@ -460,13 +463,15 @@ def _parse_paperless_url():
global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS
url = os.getenv("PAPERLESS_URL")
if url:
hostname = urlparse(url).hostname
if not hostname:
hostname = (urlparse(url).hostname or "").removesuffix(".")
if not hostname or hostname == "*" or hostname.startswith("."):
raise ImproperlyConfigured(
f"PAPERLESS_URL {url!r} has no hostname, expected a URL like https://example.com",
f"PAPERLESS_URL {url!r} has no valid hostname, expected a URL like https://example.com",
)
CSRF_TRUSTED_ORIGINS.append(url)
CORS_ALLOWED_ORIGINS.append(url)
if ":" in hostname and not hostname.startswith("["):
hostname = f"[{hostname}]" # Add brackets around IPv6 addresses
ALLOWED_HOSTS.append(hostname)
return url
+66 -4
View File
@@ -1,4 +1,8 @@
import json
import os
import subprocess
import sys
from pathlib import Path
from unittest import TestCase
from unittest import mock
@@ -168,8 +172,66 @@ class TestPaperlessURLSettings(TestCase):
self.assertIn(url, settings.CORS_ALLOWED_ORIGINS)
@pytest.mark.parametrize("url", ["example.com", "https://", "/just/a/path"])
def test_paperless_url_without_hostname_is_rejected(
@pytest.mark.parametrize(
("url", "allowed_hosts", "expected"),
[
(None, None, ["*"]),
(None, "", ["localhost"]),
("https://example.com:8443", None, ["localhost", "example.com"]),
("https://example.com.", "", ["localhost", "example.com"]),
("http://[::1]:8000", None, ["localhost", "[::1]"]),
("https://example.com", "*", ["*", "example.com"]),
(
"https://example.com",
"lan.example.com",
["lan.example.com", "localhost", "example.com"],
),
],
)
def test_allowed_hosts_at_startup(
tmp_path: Path,
url: str | None,
allowed_hosts: str | None,
expected: list[str],
) -> None:
config = tmp_path / "paperless.conf"
config.touch()
env = {
**os.environ,
"PAPERLESS_CONFIGURATION_PATH": str(config),
"PAPERLESS_LOGGING_DIR": str(tmp_path),
}
for name, value in (
("PAPERLESS_URL", url),
("PAPERLESS_ALLOWED_HOSTS", allowed_hosts),
):
if value is None:
env.pop(name, None)
else:
env[name] = value
result = subprocess.run(
[
sys.executable,
"-c",
"import json; from paperless.settings import ALLOWED_HOSTS; print(json.dumps(ALLOWED_HOSTS))",
],
cwd=Path(__file__).resolve().parents[3],
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == expected
@pytest.mark.parametrize(
"url",
["example.com", "https://", "/just/a/path", "https://*", "https://.example.com"],
)
def test_paperless_url_without_valid_hostname_is_rejected(
monkeypatch: pytest.MonkeyPatch,
url: str,
) -> None:
@@ -179,9 +241,9 @@ def test_paperless_url_without_hostname_is_rejected(
WHEN:
- The URL is parsed
THEN:
- A configuration error is raised instead of adding None to ALLOWED_HOSTS
- A configuration error is raised for missing or wildcard hostnames
"""
monkeypatch.setenv("PAPERLESS_URL", url)
with pytest.raises(ImproperlyConfigured, match="no hostname"):
with pytest.raises(ImproperlyConfigured, match="no valid hostname"):
_parse_paperless_url()
+1 -1
View File
@@ -22,8 +22,8 @@ logger = logging.getLogger("paperless_ai.chat")
CHAT_METADATA_DELIMITER = "\n\n__PAPERLESS_CHAT_METADATA__"
CHAT_ERROR_MESSAGE = "Sorry, something went wrong while generating a response."
CHAT_NO_CONTENT_MESSAGE = "Sorry, I couldn't find any content to answer your question."
MAX_CHAT_REFERENCES = 3
CHAT_RETRIEVER_TOP_K = 5
MAX_CHAT_REFERENCES = CHAT_RETRIEVER_TOP_K
def _build_chat_prompt(output_language: str | None) -> str: