Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

<!-- Checklist the maintainer can walk to verify the change. -->

- [ ] `uv run pytest tests/<module>/test_<file>.py::test_<name>` — passes
- [ ] `uv run ruff check src/ tests/` — clean
- [ ] `uv run ruff format --check src/ tests/` — clean
- [ ] `uv run mypy src/` — clean
- [ ] `uv run pytest tests/modules/<area>/test_<file>.py::test_<name>` — passes
- [ ] `uv run ruff check src/ tests/ scripts/` — clean
- [ ] `uv run ruff format --check src/ tests/ scripts/` — clean
- [ ] `uv run mypy src/ scripts/` — clean
- [ ] Confirm no regression in the affected module

## CHANGELOG
Expand All @@ -28,6 +28,6 @@
Categories: Added / Changed / Fixed.
-->

- [ ] Added a `## Unreleased` entry to `CHANGELOG.md`
- [ ] Added a `## Unreleased` entry to `CHANGELOG.md` under the appropriate Keep-a-Changelog category (Added / Changed / Fixed)

Closes #
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- **Community-health files: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`** (#56) — adds the standard GitHub community health files. `CONTRIBUTING.md` documents the inbound = outbound Apache-2.0 licensing rule, the no-bounties policy, the dev workflow (`uv sync`, `pytest`, `ruff`, `mypy`), the test-mirror file convention, the integration-test setup pointer, and the QA-label flow (`Awaiting CI` → `Ready for QA` → `QA Approved`). `CODE_OF_CONDUCT.md` adopts Contributor Covenant 2.1 and routes private reports through GitHub Private Security Advisories with a `Conduct` title prefix. `SECURITY.md` declares 0.5.x as the supported line and enumerates in-scope areas (auth/credential handling, session-token leakage, File Station path validation, argument injection in the DSM client, permission-tier gating, async background-task cleanup, config-loading invariants, supply-chain/registry-publish integrity, GHA injection patterns) and out-of-scope areas (upstream-dependency CVEs, DSM itself, compromised-host scenarios). Pattern ported from `cmeans/pypi-winnow-downloads#20`. Also aligns the now-merged `.github/PULL_REQUEST_TEMPLATE.md` (from #58) with CI: adds `scripts/` to the ruff and mypy checklist lines, tightens the pytest path placeholder to `tests/modules/<area>/test_<file>.py`, and matches the `## CHANGELOG` checkbox wording with the CONTRIBUTING.md PR-body example.
- **Dependabot PR hygiene: `.github/PULL_REQUEST_TEMPLATE.md` + auto-CHANGELOG workflow** (#58) — addresses the QA gap surfaced by the first Dependabot PR (#55) where the Dependabot-generated body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that `CLAUDE.md` § "Adding a CHANGELOG entry on every PR" requires. Two artifacts: (1) `PULL_REQUEST_TEMPLATE.md` providing a `## Summary` / `## Test plan` / `## CHANGELOG` scaffold for human-authored PRs (Dependabot bypasses templates). (2) `.github/workflows/dependabot-changelog.yml` running on `pull_request_target` for `dependabot[bot]`-authored PRs only — mints a token via `actions/create-github-app-token` (SHA-pinned to v3.1.1) so pushes attribute to `cmeans-claude-dev[bot]` and re-fire the required `pull_request` checks (lint / typecheck / test 3.11/3.12/3.13 / version-sync), enumerates the bump set via `dependabot/fetch-metadata` (SHA-pinned to v2.5.0), prefers the named `dependency-group` output and falls back to `package-ecosystem`, and pushes a follow-up commit (`chore(changelog): record dep bumps from #N`) back to the Dependabot branch. Required repo secrets: `BOT_APP_ID`, `BOT_APP_PRIVATE_KEY`. Loop guard skips when the last commit author is `cmeans-claude-dev[bot]`; idempotency check skips when `(#N)` is already present in `CHANGELOG.md` so `@dependabot recreate`/`rebase` doesn't double-write and a hand-prepended human entry (e.g., the CVE callouts in PR #55) is preserved.

### Fixed
Expand Down
39 changes: 39 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Code of Conduct

This project adopts version 2.1 of the
[Contributor Covenant](https://www.contributor-covenant.org) as its
Code of Conduct. The full text is canonical at:

<https://www.contributor-covenant.org/version/2/1/code_of_conduct/>

All contributors, maintainers, and community participants are expected
to follow it when interacting in any project space, including GitHub
issues, pull requests, discussions, and any communication channels
linked from this repository.

## Scope

This Code applies within all project spaces and also when an
individual is officially representing the project in public spaces.
Examples include using an official project email address, posting via
an official social media account, or acting as an appointed
representative at an online or offline event.

## Reporting a concern

Because mcp-synology is a one-person project with no shared inbox,
reports are routed through the same private channel used for security
issues:

1. Open a **GitHub Private Security Advisory** at
<https://github.com/cmeans/mcp-synology/security/advisories/new>.
2. Put `Conduct` in the title so the maintainer can distinguish it
from a security report.
3. Include a description of what happened, links to any relevant
public threads, and how you'd like the situation handled.

This is a workaround — GitHub does not currently offer a dedicated
private-contact channel for non-security matters. The advisory
mechanism is the only way to open a private thread that the
maintainer can see and respond to without exposing the discussion
publicly. Reports will be reviewed confidentially.
187 changes: 187 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Contributing to mcp-synology

Thanks for your interest. This is a small project with a simple
contribution process. Please read this whole document before opening
your first PR — it's short.

## License of your contribution

**By submitting a pull request, you agree that your contribution is
licensed under the [Apache License 2.0](LICENSE)**, the same license
as the rest of the project.

This is the "inbound = outbound" rule defined in Apache-2.0 § 5:

> Unless You explicitly state otherwise, any Contribution intentionally
> submitted for inclusion in the Work by You to the Licensor shall be
> under the terms and conditions of this License, without any
> additional terms or conditions.

In plain English:

- **You retain copyright on your own code.** You are not transferring
ownership to the maintainer.
- **You grant everyone a perpetual, irrevocable, royalty-free license**
to use, modify, redistribute, and sublicense your contribution under
Apache-2.0.
- **You grant a patent license** covering any patents you hold that
read on your contribution (Apache-2.0 § 3).
- **You cannot attach additional terms** to a contribution. If your
PR body, commit messages, or comments propose extra restrictions —
compensation claims, bounty invoices, attribution beyond what
Apache-2.0 already requires, "please don't use this commercially,"
etc. — those have no legal effect under § 5 and the PR will be
asked to remove them before review.

If you can't agree to those terms, please don't submit a PR.

## No bounties or paid contributions

mcp-synology does not offer bug bounties, paid contributions, or any
kind of reward program. All contributions are voluntary donations
under Apache-2.0.

Attaching a wallet address, invoice, bounty claim, or compensation
request to a PR does not create an expectation of payment. PRs with
such attachments will be asked to remove them before review.

## Before you open a PR

For anything bigger than a one-line fix:

1. **Open an issue first** so we can agree on scope and approach.
Drive-by PRs for non-trivial changes often get closed because they
don't match what the project needs.
2. **One concern per PR.** Don't bundle "fix X" with "refactor Y" and
"add Z." Small focused PRs get reviewed and merged faster.
3. **Check that a similar PR isn't already open.**

## Development setup

```bash
uv sync --extra dev # install runtime + dev dependencies
uv run pytest # run unit + module tests
uv run pytest tests/modules/filestation/test_listing.py # single test file
uv run pytest -k "test_list_shares" # single test by name
uv run ruff check src/ tests/ scripts/ # lint
uv run ruff format --check src/ tests/ scripts/ # format check
uv run mypy src/ scripts/ # typecheck (strict mode)
```

Tests are mocked at the HTTP boundary (`respx`), so they don't reach
a real Synology NAS or require credentials to run.

**Integration tests** (marked `@pytest.mark.integration`) require a
real NAS and `tests/integration_config.yaml` (copy from
`integration_config.yaml.example`). They're excluded from CI by
default — run locally with:

```bash
uv run pytest -m integration -v --log-cli-level=INFO
```

Requires **Python 3.11+**.

Architecture overview, design specs, and module conventions live in
[`CLAUDE.md`](CLAUDE.md) and [`docs/specs/`](docs/specs/) — read the
relevant spec before touching a module.

## PR requirements

Every PR must:

- **Include a test.** If you're fixing a bug, add a regression test
that fails on `main` and passes on your branch. If you're adding a
feature, cover the new code paths. Test files mirror source files
(`src/mcp_synology/modules/filestation/listing.py` →
`tests/modules/filestation/test_listing.py`).
- **Add a CHANGELOG entry** under `## Unreleased` in
[`CHANGELOG.md`](CHANGELOG.md), categorized `### Added` (new
feature, including tests and docs), `### Changed` (behavior change),
or `### Fixed` (bug fix). Reference the PR number and any closed
issue: `- ... (#NN) — closes #MM`.
- **Link the issue** with `Closes #N` in the PR body so merging
auto-closes it.
- **Pass CI locally first** — run `uv run pytest`, `uv run ruff
check src/ tests/ scripts/`, and `uv run mypy src/ scripts/`
and confirm green before pushing.
- **Write a clear commit message.** PRs are squash-merged, so your
PR title becomes the commit subject and your PR body becomes the
commit body. Write both as if someone reading `git log` a year
from now should understand what changed and why.

## PR body format

Two required sections:

## Summary

Two or three sentences on what changed and why.

## Test plan

A checklist the maintainer can walk to verify the change:

- [ ] Run `uv run pytest tests/modules/<area>/test_<module>.py::test_<name>` — passes
- [ ] `uv run ruff check src/ tests/ scripts/` — clean
- [ ] `uv run ruff format --check src/ tests/ scripts/` — clean
- [ ] `uv run mypy src/ scripts/` — clean
- [ ] Confirm no regression in the affected module

## CHANGELOG

- [ ] Added a `## Unreleased` entry to `CHANGELOG.md` under the
appropriate Keep-a-Changelog category (Added / Changed / Fixed)

Closes #N

## How the review process works

1. **CI runs first.** For first-time contributors, the maintainer has
to manually approve the workflow run (GitHub policy for fork PRs).
Your PR will sit with no checks until a maintainer clicks "Approve
and run." This is not a signal that you're being ignored.
2. **Label automation takes over.** After CI passes, the PR
auto-promotes from `Awaiting CI` → `Ready for QA`. You don't need
to do anything.
3. **Maintainer reviews.** If there are issues, the PR gets
`QA Failed` and a review comment. Push your fix; labels reset
automatically.
4. **Final maintainer review and merge.** Once QA is clean, the
maintainer does a final review and merges the PR. All PRs are
squash-merged. Your branch is auto-deleted after merge.

## Code style

- Python 3.11+; full type hints on all functions, parameters, and
return values; `mypy --strict` must pass.
- `dataclass` for internal data structures, `pydantic.BaseModel` for
validated external input (config, API responses).
- Async throughout — all DSM API calls and tool handlers are async.
- Use `httpx` (async) for HTTP, never `requests`. Mock with `respx`.
- Use `yaml.safe_load`, never `yaml.load`.
- Ruff: line length 100, rules `E/F/W/I/N/UP/B/SIM/TCH`.
- All tool output goes through shared formatters in
`core/formatting.py` — never format strings inline in tool handlers.
- Default to no comments. Add a one-line comment only when the
*why* is non-obvious; don't paraphrase the code.

## Reporting bugs or security issues

Issue templates are available — please use the right one:

- **[Bug report](../../issues/new?template=bug_report.yml)** —
something isn't working as documented.
- **[Feature request](../../issues/new?template=feature_request.yml)** —
a new capability or a change to existing behavior.
- **[Platform test report](../../issues/new?template=platform_test_report.yml)** —
results from testing on a specific DSM version, NAS model, or OS.

For **security issues**, see [`SECURITY.md`](SECURITY.md) for private
disclosure instructions. Please don't file public issues for security
problems.

## Contact

File an issue or start a discussion on the repo. This is a one-person
project, so **response times vary** — please be patient.
134 changes: 134 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Security Policy

## Supported versions

mcp-synology is currently on the 0.5.x line. Fixes for security issues
are applied to the latest published version only. Users of earlier
versions should upgrade.

| Version | Supported |
| ------- | ----------------- |
| 0.5.x | ✅ security fixes |
| < 0.5 | ❌ upgrade |

## Reporting an issue

**Please do not file a public GitHub issue for security problems.**

The only supported channel is a **GitHub Private Security Advisory**.
To open one:

1. Go to <https://github.com/cmeans/mcp-synology/security/advisories/new>.
2. Fill in a description, steps to reproduce, and the affected
version.
3. Submit as a draft advisory. Only the maintainer will see it.

This creates a private thread where the report, any proof-of-concept,
the fix, and disclosure timing can be discussed without exposing the
issue publicly. The private vulnerability reporting feature is
enabled on this repository.

If you cannot use GitHub Private Security Advisories for some reason,
please open a **public** issue titled simply "Security contact
request" — no details — and the maintainer will reach out to arrange
a private channel.

## Please include

- A description of the issue and its impact.
- Steps to reproduce (or a proof-of-concept).
- The version of mcp-synology affected (output of `mcp-synology
--version`).
- Your operating system, Python version, DSM version, and NAS model
(auth and File Station behavior is DSM-version-dependent — DSM
6.x, 7.0, 7.1, and 7.2 each have known quirks).
- Whether the issue is reproducible against a clean
`pip install mcp-synology`, against the vdsm test harness in
`tests/vdsm/`, or only against a specific NAS configuration.

## What to expect

- **Acknowledgment** after the maintainer sees the report. Response
times vary — this is a one-person project.
- **Coordinated fix timeline.** mcp-synology is maintained by one
person, not a security team. Please be patient.
- **Credit in the release notes** if you'd like it. Anonymous
disclosure is also fine.
- **No monetary reward.** mcp-synology does not operate a bug bounty
program. Reports are voluntary contributions to project safety.

## Scope

**In scope**

- Credential-handling issues in the auth manager
(`src/mcp_synology/core/auth.py`) — OS keyring storage, env-var
fallback, plaintext-config-file last-resort path, the
`MCPSynology_{instance_id}_{unique_id}` session-name format, and
the lazy-keepalive design.
- Session-error retry logic in the DSM client
(`src/mcp_synology/core/client.py` + `src/mcp_synology/core/errors.py`)
— DSM error codes 106 / 107 / 119 trigger transparent re-auth and
exactly-one retry. The codes and the `is_session_error` helper live
in `errors.py`; the retry path lives in `client.py`
(`_SESSION_ERROR_CODES` set plus the call sites that consult it).
Bypasses, missed retry sites, leaked credentials during re-auth, or
re-auth-on-105 (permission-denied) regressions would be in scope.
- DSM session token leakage — passwords are masked in DEBUG logs;
regressions where a session token, password, or 2FA OTP appears in
log output, error messages, or persisted state would be in scope.
- Path-traversal or share-validation issues in File Station tool
handlers (`src/mcp_synology/modules/filestation/`) — paths are
normalized and the first component is validated against the cached
share list; bypasses that let a tool reach outside a share would
be in scope.
- Argument-injection or unsafe parameter encoding in
`src/mcp_synology/core/client.py` — comma/backslash escaping in
multi-path params, query-string construction for DSM API calls.
- MCP tool exposure issues — tools that should be gated by the
permission tier (READ vs WRITE) but are unconditionally registered,
or write tools registered without explicit user opt-in.
- Background-task lifecycle bugs in the four async DSM tasks (Search,
DirSize, CopyMove, Delete) that could leak session resources or
leave orphan tasks consuming CPU on the NAS — `try/finally` cleanup
must always call stop/clean.
- Config-loading issues — strict top-level validation, lenient
module-settings validation, env-var override precedence, and the
read-only-from-server-perspective invariant. Anything that lets a
malformed config escalate beyond a clean error message is in scope.
- Supply-chain or packaging issues affecting published wheels or
sdists on PyPI (trusted publishing, sdist contents, lockfile
integrity).
- MCP-registry publish workflow integrity (`.github/workflows/publish.yml`
→ `publish-registry` job) — OIDC-only auth, idempotent re-publish
behavior on duplicate-version errors.
- GitHub Actions workflow injection — particularly any new use of
`${{ github.event.* }}` expressions inside `run:` blocks where a
contributor-controlled value (branch name, PR title, comment body)
could become directly-executed shell. The pattern documented in
`.github/workflows/pr-labels-ci.yml` (route through step-level
`env:` and reference as `$VAR`) is the project standard.

**Out of scope**

- Vulnerabilities in dependencies (`mcp`, `httpx`, `keyring`,
`pydantic`, `PyYAML`, `click`) — please report those upstream to
the affected project.
- Vulnerabilities in DSM itself — report those to Synology PSIRT at
<https://www.synology.com/security/advisory>. mcp-synology is a
client; we patch around DSM bugs but don't fix DSM.
- Attacks that require an adversary to already have shell access on
the host where mcp-synology is running, write access to the user's
config file, write access to the OS keyring, or NAS admin
credentials — that's a compromised host or a compromised NAS, not
a project-specific issue.
- DSM API rate-limit / lockout surprises from misconfigured polling
intervals — that's documentation territory, not a security issue.
- Issues with Claude Desktop, Claude Code, or any other MCP host —
please report to the affected host project.

## Historical issues

Security-relevant findings are tracked in the GitHub issue tracker
under the `security` label. See also the [`LICENSE`](LICENSE) file
for Apache-2.0 warranty disclaimers.
Loading