synology: fix --confirm arg position + verify write path live

- Fix argparse: --confirm/--vault were only accepted BEFORE the subcommand, so
  every documented gated-write (e.g. `call X set k=v --confirm`) failed. Moved to
  a shared parent parser (SUPPRESS defaults) -> both flags work in either position.
- Verified the CSRF write path live on cascadesDS: Share create -> verify ->
  delete -> verify gone. Both mutating calls succeeded; device left pristine.
- SKILL.md: write/setter path marked VERIFIED; confirmed share-create signature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 11:56:32 -07:00
parent 73b957d911
commit 21ef1f2570
2 changed files with 43 additions and 26 deletions

View File

@@ -129,16 +129,20 @@ VPN-down connect error surfaced to the user, a method refused for lack of --conf
--confirm`. (`ls <folder>` now catches this and prints the SSH hint.) Windows note: a bare --confirm`. (`ls <folder>` now catches this and prints the SSH hint.) Windows note: a bare
`ls /Public` arg is also rewritten by MSYS path-conversion — prefer `MSYS_NO_PATHCONV=1` or the `ls /Public` arg is also rewritten by MSYS path-conversion — prefer `MSYS_NO_PATHCONV=1` or the
`call ... folder_path=/x` form, but the 407 denial blocks it regardless. `call ... folder_path=/x` form, but the 407 denial blocks it regardless.
- **WRITE/SETTER PATH NOT YET EXERCISED LIVE.** Session-expiry re-login (106/119), the mutating-verb - **WRITE/SETTER PATH VERIFIED 2026-06-25.** Live round-trip on cascadesDS: `SYNO.Core.Share create`
gate, and read-modify-write setters are coded to spec but no mutating call has been made against (hidden throwaway share) -> confirmed in `shares` -> `SYNO.Core.Share delete` -> confirmed gone.
the device yet. Gate is verified only on the read path. First real change will confirm CSRF on a Both mutating calls passed CSRF (synotoken) end-to-end and returned success; device left pristine.
write end-to-end. The mutating-verb gate correctly blocks without `--confirm` and allows with it. (Session-expiry
re-login 106/119 still not forced live, but the auth/CSRF/setter plumbing is now proven.)
Share create signature confirmed: `name=<n> 'shareinfo:={"name":..,"vol_path":"/volume1",..}'`.
- Built originally from a 5-agent scan of the DSM 7 help tree + authoritative API sources (kwent - Built originally from a 5-agent scan of the DSM 7 help tree + authoritative API sources (kwent
`_full.json`, N4S4/synology-api, Synology Web API guides) — see `references/dsm-api.md`. `_full.json`, N4S4/synology-api, Synology Web API guides) — see `references/dsm-api.md`.
- **Live fixes from this run (2026-06-25):** `services` used `SYNO.Core.Service.list` which 103s on - **Live fixes from this run (2026-06-25):** `services` used `SYNO.Core.Service.list` which 103s on
DSM 7.2.1 — corrected to `get` (returns `{"service":[...]}`). `apis | head` raised a DSM 7.2.1 — corrected to `get` (returns `{"service":[...]}`). `apis | head` raised a
`BrokenPipeError` traceback — now caught and exits cleanly. `ls <folder>` now degrades to an SSH `BrokenPipeError` traceback — now caught and exits cleanly. `ls <folder>` now degrades to an SSH
hint on the FileStation 400/407 denial. hint on the FileStation 400/407 denial. **`--confirm`/`--vault` after the subcommand were rejected
by argparse** (every documented gated-write example, e.g. `call X set k=v --confirm`, would have
failed) — moved to a shared parent parser so both flags now work before AND after the subcommand.
- **DSM 7.2.x reboot/shutdown 103:** the Web-API `reboot`/`shutdown` can return error 103 on some - **DSM 7.2.x reboot/shutdown 103:** the Web-API `reboot`/`shutdown` can return error 103 on some
builds. Fallback is the SSH `reboot|shutdown --confirm` recipe (`synoshutdown -r|-s`). builds. Fallback is the SSH `reboot|shutdown --confirm` recipe (`synoshutdown -r|-s`).
- **Param-schema caveat:** method names + versions in the reference are device-authoritative, but many - **Param-schema caveat:** method names + versions in the reference are device-authoritative, but many

View File

@@ -256,27 +256,38 @@ def _kv(pairs):
def main(argv): def main(argv):
ap = argparse.ArgumentParser(prog="syno", description="Synology DSM Web API client") # --vault / --confirm live on a shared parent so they are accepted BOTH before the
ap.add_argument("--vault", default=DEFAULT_VAULT, help="SOPS vault entry for the NAS") # subcommand and after it (the SKILL docs put --confirm at the end, e.g.
ap.add_argument("--confirm", action="store_true", help="authorize a mutating call") # `call X set k=v --confirm`). SUPPRESS defaults stop the subparser copy from
# clobbering a value parsed by the main parser.
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--vault", default=argparse.SUPPRESS, help="SOPS vault entry for the NAS")
common.add_argument("--confirm", action="store_true", default=argparse.SUPPRESS,
help="authorize a mutating call")
ap = argparse.ArgumentParser(prog="syno", parents=[common],
description="Synology DSM Web API client")
sub = ap.add_subparsers(dest="cmd", required=True) sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("test", help="login and report DSM identity") def add(name, **kw):
pa = sub.add_parser("apis", help="list the device's API map (what you can control)") return sub.add_parser(name, parents=[common], **kw)
add("test", help="login and report DSM identity")
pa = add("apis", help="list the device's API map (what you can control)")
pa.add_argument("filter", nargs="?", help="case-insensitive substring filter") pa.add_argument("filter", nargs="?", help="case-insensitive substring filter")
sub.add_parser("sysinfo", help="model/serial/RAM/temp/uptime (SYNO.Core.System)") add("sysinfo", help="model/serial/RAM/temp/uptime (SYNO.Core.System)")
sub.add_parser("util", help="live CPU/mem/disk/net (SYNO.Core.System.Utilization)") add("util", help="live CPU/mem/disk/net (SYNO.Core.System.Utilization)")
sub.add_parser("storage", help="volumes/disks/RAID/usage (SYNO.Storage.CGI.Storage)") add("storage", help="volumes/disks/RAID/usage (SYNO.Storage.CGI.Storage)")
sub.add_parser("shares", help="shared folders (SYNO.Core.Share)") add("shares", help="shared folders (SYNO.Core.Share)")
sub.add_parser("users", help="local users (SYNO.Core.User)") add("users", help="local users (SYNO.Core.User)")
sub.add_parser("groups", help="local groups (SYNO.Core.Group)") add("groups", help="local groups (SYNO.Core.Group)")
sub.add_parser("packages", help="installed packages (SYNO.Core.Package)") add("packages", help="installed packages (SYNO.Core.Package)")
sub.add_parser("services", help="services (SYNO.Core.Service)") add("services", help="services (SYNO.Core.Service)")
sub.add_parser("connections", help="current connections (SYNO.Core.CurrentConnection)") add("connections", help="current connections (SYNO.Core.CurrentConnection)")
pl = sub.add_parser("ls", help="FileStation list (no path = shares)") pl = add("ls", help="FileStation list (no path = shares)")
pl.add_argument("path", nargs="?") pl.add_argument("path", nargs="?")
pc = sub.add_parser("call", help="generic: call ANY API method (the power tool)") pc = add("call", help="generic: call ANY API method (the power tool)")
pc.add_argument("api"); pc.add_argument("method") pc.add_argument("api"); pc.add_argument("method")
pc.add_argument("--version", type=int) pc.add_argument("--version", type=int)
pc.add_argument("--post", action="store_true") pc.add_argument("--post", action="store_true")
@@ -285,19 +296,21 @@ def main(argv):
# gated convenience writes # gated convenience writes
for name, helptxt in (("pkg-start", "start a package by id"), for name, helptxt in (("pkg-start", "start a package by id"),
("pkg-stop", "stop a package by id")): ("pkg-stop", "stop a package by id")):
pp = sub.add_parser(name, help=helptxt + " (needs --confirm)") pp = add(name, help=helptxt + " (needs --confirm)")
pp.add_argument("id") pp.add_argument("id")
sub.add_parser("reboot", help="reboot the NAS (needs --confirm)") add("reboot", help="reboot the NAS (needs --confirm)")
sub.add_parser("shutdown", help="shut down the NAS (needs --confirm)") add("shutdown", help="shut down the NAS (needs --confirm)")
args = ap.parse_args(argv) args = ap.parse_args(argv)
vault = getattr(args, "vault", DEFAULT_VAULT)
confirm = getattr(args, "confirm", False)
def guard(method): def guard(method):
if not args.confirm: if not confirm:
raise SynoError(f"'{method}' mutates the device -- re-run with --confirm") raise SynoError(f"'{method}' mutates the device -- re-run with --confirm")
try: try:
c = SynoClient(vault=args.vault) c = SynoClient(vault=vault)
if args.cmd == "test": if args.cmd == "test":
c.login() c.login()
d = c.call("SYNO.Core.System", "info") d = c.call("SYNO.Core.System", "info")