The 0.5.0 prompt mis-attributed the API: pull_start lives on
SYNO.Docker.Image, not SYNO.Docker.Registry (live DSM capture).
search and tags ARE correctly on Registry; only pull_start belongs
to Image. Registry/pull_start returns "Method does not exist".
Parameters are unchanged (repository + tag both JSON-encoded), and
the Image/list polling for completion detection is untouched.
Tests updated to assert SYNO.Docker.Image/pull_start. CLAUDE.md
DSM-quirks section consolidates the Image vs. Registry split so
this trap is documented for future surface additions. References
#3 (already closed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three new SYNO.Docker.Registry tools, reverse-engineered from a live
DSM API capture (n4s4 reference disagrees on param names and methods).
- search_registry (#5): SYNO.Docker.Registry/search v1 with JSON-encoded
q, plus offset/limit/page_size. Renders stars, downloads, official
flag, truncated description, and total match count. Read-only.
- list_image_tags: SYNO.Docker.Registry/tags v1 with JSON-encoded repo
(not name — DSM live capture diverges from n4s4). Response shape is
unusual: tag list comes back as the envelope's data field directly.
Output capped by limit (default 50); accepts both list and dict
response shapes defensively. Read-only.
- pull_image (#3): SYNO.Docker.Registry/pull_start v1 with both
repository and tag JSON-encoded. Async pull — no pull_status method
confirmed on this DSM, so completion is detected by polling
SYNO.Docker.Image/list (2–10 s backoff, 240 s budget under the
Claude Desktop ~4 min tool-call ceiling). Timeout returns a
non-fatal "still running" hint. Short-circuits when the image is
already present locally. Confirmation gate required.
Tool count: 31 → 34. CLAUDE.md confirmation list updated. New DSM
quirks documented for pull_start (no pull_status) and tags (repo
param name, top-level data array).
Closes#3Closes#5
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three live-NAS observations that the 0.4.2 implementation got wrong:
1. Header showed "?:?" — DSM Image/get returns image="" and tag=""
for name:tag lookups; the response fields are unreliable. The
header now echoes the user-supplied image_id, falling back to the
sha256 id only when image_id itself is empty.
2. Ports printed as raw Python dicts ({'port':'22','protocol':'tcp'}).
The ports array is a list of {port, protocol} objects — each entry
now renders as "22/tcp".
3. Environment printed as raw Python dicts ({'key':'PATH','value':...}).
The env array is a list of {key, value} objects — each entry now
renders as "PATH=/usr/local/...".
cmd, entrypoint, and volumes (plain string arrays) were already
correct and are untouched. Tests updated to match the live shape;
two new tests guard the header fallback. References #4 (already
closed by 0.4.2 — no need to reopen for a rendering polish).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live DSM API capture revealed the actual SYNO.Docker.Image/get
contract: a single JSON-encoded parameter named `identity` that
accepts both `name:tag` and `sha256:<hash>` forms. The 0.4.0 code
passed `name` + `tag` + `id` and was rejected by DSM with error 114.
Response shape also corrected — the endpoint returns flat top-level
fields (image, tag, id, digest, size, virtual_size, author,
docker_version, cmd, entrypoint, env, ports, volumes), NOT the
Docker-engine inspect shape with details.Config.* + RootFS.Layers
that the previous implementation assumed. Layer rendering removed;
digest / author / docker_version / volumes are now displayed.
Pre-resolution against list_images is no longer needed — the user
input goes straight into `identity` JSON-encoded.
Closes#4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live test on this DSM firmware: SYNO.Docker.Container has no pause/
unpause method ("Method does not exist"). The Container Manager GUI
action menu only exposes Start / Stop / Force-Stop / Restart / Reset —
pause/resume simply isn't a feature here.
The two tools were briefly shipped in 0.4.0 (implemented by symmetry
with the verified stop call) and have now been removed rather than
left as a broken surface. The remaining lifecycle tools
(start_container, stop_container, restart_container) are unaffected.
Tool count: 33 → 31. Closes#7 (won't fix — DSM limitation).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes#1, #4, #6, #7.
Container lifecycle (#1, #7):
- start_container, stop_container, restart_container, pause_container,
unpause_container — all via SYNO.Docker.Container with JSON-encoded
name parameter, routed through _resolve_container_name for hash-
prefix resolution. stop is live-verified; the other four are
implemented by symmetry on the same API surface.
inspect_image (#4):
- Returns full image detail (layers, env, ports, entrypoint/cmd,
labels) via SYNO.Docker.Image/get. Accepts name:tag, registry-
prefixed names, and bare hashes. Defensive response parsing
handles both wrapped (details.*) and flat envelopes.
system_overview (#6):
- Aggregates CPU %, RAM, network and block I/O across all running
containers plus running/stopped counts. No new DSM endpoint —
composed from list + stats, reusing the container_stats CPU
formula. Per-source errors are non-fatal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug 1 — delete_container (DSM error 114):
SYNO.Docker.Container/delete requires three parameters: name
(JSON-encoded), force=false, and preserve_profile=false. Previously
only a bare `name` string was sent, causing DSM to reject the call
with error 114. Added the two missing fields and JSON-encode name to
match the DSM convention. The connector-side running-container guard
is unchanged; force stays hard-coded to false.
Bug 2 — delete_project orphan containers:
Production test revealed that DSM does NOT reject Project/delete on a
running project — it silently removes the registration and leaves the
containers running without any project context. The previous
implementation tried to handle this via a caught SynologyError that
never actually fires. Fix: check the project status from _find_project
connector-side before issuing any DSM call; if RUNNING, return an
error pointing at stop_project. The delete request is never sent for
a running project.
The corresponding unit test (test_delete_project_running_returns_stop_hint)
was a false positive — it mocked a DSM rejection that real DSM never
produces. Replaced with test_delete_project_running_blocked_connector_side
which asserts that client.request("delete") is never called when the
project is RUNNING.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the project lifecycle (create → start/stop/redeploy → delete).
The tool calls SYNO.Docker.Project/delete with the UUID JSON-encoded
as the `id` parameter (per DSM convention) and removes only the
Container Manager registration — the project folder and compose
file remain on the NAS. This mirrors DSM's own "Delete project"
behaviour, not a bug; the success message states the folder was
preserved so the user is not surprised.
Safety:
- Project-name validation runs before any I/O.
- A `_find_project` pre-flight returns "not found" with a clear
message rather than letting DSM reject an unknown UUID.
- No automatic stop. If the project is RUNNING and DSM rejects
the delete, the response tells the user to `stop_project` first
rather than silently halting containers under the guise of a
"delete" call.
- Requires confirmed=True; preview shows name, UUID, status, full
path, and share path so the user can verify before deleting.
Tests cover preview-only, not-found, invalid-name, happy path
(verifies the UUID is JSON-encoded in the delete call), and the
running-project rejection path that surfaces the stop_project hint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>