Adds `create_project` for registering a new Container Manager project
from a compose YAML string. Three-step flow that mirrors the DSM
"Create Project" wizard:
1. SYNO.FileStation.CreateFolder with force_parent=true (idempotent
— does not fail if the folder already exists, and creates missing
intermediate directories). Without this step, Docker.Project/create
fails with DSM error 2100.
2. SYNO.Docker.Project/create (form-encoded POST; JSON-encoded string
parameters per DSM convention) returns the new project UUID.
3. trigger_build_stream + _wait_for_project_running, reusing the
existing image-pull / start / poll machinery (including the
BUILD_FAILED early-exit from welle 2).
Safety:
- Project-name validation (Welle-1 regex) runs before any I/O.
- Compose content is YAML-parsed and must contain a top-level
`services` key before any side effects.
- A pre-flight list_projects check rejects duplicate names with a
clear message rather than leaving an orphaned folder on the NAS.
- share_path defaults to compose_base_path + project_name (e.g.
/volume1/docker + myapp → /docker/myapp); a caller-supplied value
overrides it.
- Requires confirmed=True; the preview shows the resolved share path
and the service count parsed from the compose content.
- DSM error 2100 surfaces as "target folder issue" with the attempted
path. A build_stream failure after a successful Project/create tells
the user the project is registered-but-not-started and points at
redeploy_project for recovery.
Tests cover preview-only, already-exists, happy path (with parameter
JSON-encoding assertions), explicit share_path, malformed YAML,
missing services key, invalid project name, error 2100, and
build_stream failure after registration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three resilience and honesty fixes from the v0.2.8 review. Minor
version bump because redeploy_project and system_prune return
different strings.
M-4: trigger_build_stream now converts every non-ReadTimeout
httpx.HTTPError (ConnectError, ConnectTimeout, WriteError,
RemoteProtocolError, ...) into a SynologyError with a clear
message. Previously only ReadTimeout was handled; everything else
propagated as a raw httpx exception. redeploy_project now tracks
whether stop was actually issued and, when build_stream fails after
a successful stop, tells the user the project is in STOPPED state
and recommends start_project / retry rather than the misleading
"use stop + start separately" workaround.
M-5: _wait_for_project_running exits early on BUILD_FAILED / ERROR
(new _TERMINAL_FAILURE_STATUSES frozenset). DSM signals these
statuses within seconds of a failed image pull; the old polling
loop kept waiting up to 5 minutes for RUNNING. redeploy_project
now surfaces the terminal status with a BUILD_FAILED-specific hint
to update_image_tag.
M-6: system_prune preview now enumerates user-created networks
that have no containers attached (excluding the three built-in
networks bridge/host/none, which Docker never prunes). Previously
the preview noted "Unused networks: (not counted)" even though
SYNO.Docker.Utils/prune does delete them — users could lose
networks they had not been warned about.
Tests:
- 2 new dsm_client tests: ConnectError and RemoteProtocolError
both raise SynologyError, not raw httpx exceptions.
- 2 new project tests: recovery hint after stop+build_stream
failure (RUNNING case); old workaround retained for the
STOPPED case where no stop was issued.
- 3 new polling tests: BUILD_FAILED and ERROR each trigger early
exit; redeploy_project surfaces BUILD_FAILED with update_image_tag
hint.
- 2 new system_prune preview tests: counts unused networks
correctly, excludes built-ins; network-fetch failure is non-fatal.
245 tests pass. ruff check + ruff format clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
C-1: __version__ now derived from package metadata via
importlib.metadata.version() so pyproject.toml is the single source of
truth. Previously stuck at "0.1.0" since the initial release.
C-2: Backfill CHANGELOG entries for 0.2.7 and 0.2.8 (both releases had
shipped without changelog updates) and add a 0.2.9 entry covering this
welle.
M-3: Reject project names containing path separators or other unsafe
characters before they reach _find_compose_path. Previously a name like
"../../etc" could traverse out of compose_base_path when the project was
not yet registered with Container Manager. Adds _validate_project_name
(regex ^[a-zA-Z0-9_-]+$, applied in read_compose, update_compose,
update_image_tag, update_env_var) plus parametrized tests for valid and
unsafe names and one rejection test per tool. 236 tests pass.
Also: ruff format autofix on three pre-existing files (cli.py,
config.py, test_config.py) — cosmetic only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cache failed `_ensure_initialized` outcomes for 60 seconds so that
repeated tool calls during a credential outage (wrong password,
IP-blocked 407, DNS failure) don't keep hammering DSM. Each caller
gets the same exception raised from the cache until the cooldown
window expires, after which a fresh attempt is made.
- Adds INIT_ERROR_COOLDOWN module constant (60.0 s).
- Adds self._init_error / self._init_error_until state on DsmClient.
- Re-raises cached error inside the init lock, emits warning log on
cache entry.
Addresses M4 from the 0.2.7 review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FastMCP generates outputSchema for every tool with a return annotation,
roughly doubling the tools/list payload size. Multi-line docstrings with
Args/Returns sections add further bulk that Claude Desktop must parse.
- Strip -> str from all 23 @mcp.tool() functions
- Trim every tool docstring to a single descriptive line (≤100 chars)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude Desktop times out tool calls after ~4 minutes. The previous
implementation read the first SSE chunk before returning, which could
block for the entire image-pull duration.
Now: send the GET request, wait for HTTP response headers (status
check only), close the connection immediately — never read SSE events.
DSM starts the build on receipt of the request and continues
server-side. ReadTimeout on headers is caught and ignored (request
already sent). Removes the _json import added in 0.2.5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the delete-before-start workaround with the real mechanism:
SYNO.Docker.Project/build_stream is what the DSM "Erstellen" button calls
(confirmed via DevTools). It pulls updated images and starts the project.
DsmClient.trigger_build_stream(project_id): fires a streaming GET to
build_stream, reads the first SSE chunk to confirm DSM accepted the
request, then closes. ReadTimeout is swallowed (build running server-side).
Immediate JSON error responses are parsed and raised as SynologyError.
redeploy_project simplified from 4 steps to 3:
1. Stop (skip for STOPPED, suppress for BUILD_FAILED)
2. trigger_build_stream — DSM pulls images + starts project
3. Poll for RUNNING (timeout raised from 30s → 5min for large pulls)
build_stream errors are now fatal (abort with clear message).
Removes _read_compose_images_for_project, _try_delete_image and their
json/yaml/re imports — no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
redeploy_project: replace broken SYNO.Docker.Image/pull with a unified
4-step delete-before-start flow for all project states (RUNNING, STOPPED,
BUILD_FAILED). Reads image tags from the project's compose.yaml via
FileStation before stopping, deletes each cached image (non-fatal), then
starts the project so DSM auto-pulls the latest version. Polls for RUNNING
as before.
update_image_tag: auto-update env vars whose value equals the numeric
version prefix of the old tag when the new tag shares the same
<digits>-<suffix> pattern (e.g. JENKINS_VERSION=2.558 → 2.560 when tag
changes 2.558-jdk21 → 2.560-jdk21). Preview mode lists the pending
auto-updates. Only triggers when the var exists and the pattern matches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove hostnames, concrete container names, image tags, personal notes,
and the completed task backlog. Replace with generic DSM quirks reference,
implementation rules, and tool inventory — suitable for a public connector.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove contextlib.suppress from the image pull step in the BUILD_FAILED
redeploy path. A failed pull (e.g. non-existent tag) now immediately
returns an actionable error pointing to update_image_tag instead of
silently continuing and starting the project with stale/missing image.
Also bumps version 0.2.1 → 0.2.2 and adds CHANGELOG entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DSM starts containers asynchronously - start_project returns immediately
while containers are still initialising. Adds _wait_for_project_running:
polls SYNO.Docker.Project/list every 2s up to 30s after issuing start.
Reports RUNNING on success; emits a warning instead of failure on timeout
so callers can still verify with get_project_status.
Applies to all three redeploy paths (RUNNING, STOPPED, BUILD_FAILED).
Also bumps version 0.2.0 → 0.2.1 and adds CHANGELOG entry.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- list_images: lists all local Docker images sorted by size desc,
shows size (human-readable), creation date, in-use marker, and
update-available marker; gracefully handles container list failure
- delete_image: accepts name:tag or image hash, blocks deletion when
image is in use by a container, requires confirmed=True to execute;
default shows a dry-run preview
- 16 unit tests covering all paths (mock DSM client)
- ruff format + check clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>