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

@@ -256,27 +256,38 @@ def _kv(pairs):
def main(argv):
ap = argparse.ArgumentParser(prog="syno", description="Synology DSM Web API client")
ap.add_argument("--vault", default=DEFAULT_VAULT, help="SOPS vault entry for the NAS")
ap.add_argument("--confirm", action="store_true", help="authorize a mutating call")
# --vault / --confirm live on a shared parent so they are accepted BOTH before the
# subcommand and after it (the SKILL docs put --confirm at the end, e.g.
# `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.add_parser("test", help="login and report DSM identity")
pa = sub.add_parser("apis", help="list the device's API map (what you can control)")
def add(name, **kw):
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")
sub.add_parser("sysinfo", help="model/serial/RAM/temp/uptime (SYNO.Core.System)")
sub.add_parser("util", help="live CPU/mem/disk/net (SYNO.Core.System.Utilization)")
sub.add_parser("storage", help="volumes/disks/RAID/usage (SYNO.Storage.CGI.Storage)")
sub.add_parser("shares", help="shared folders (SYNO.Core.Share)")
sub.add_parser("users", help="local users (SYNO.Core.User)")
sub.add_parser("groups", help="local groups (SYNO.Core.Group)")
sub.add_parser("packages", help="installed packages (SYNO.Core.Package)")
sub.add_parser("services", help="services (SYNO.Core.Service)")
sub.add_parser("connections", help="current connections (SYNO.Core.CurrentConnection)")
pl = sub.add_parser("ls", help="FileStation list (no path = shares)")
add("sysinfo", help="model/serial/RAM/temp/uptime (SYNO.Core.System)")
add("util", help="live CPU/mem/disk/net (SYNO.Core.System.Utilization)")
add("storage", help="volumes/disks/RAID/usage (SYNO.Storage.CGI.Storage)")
add("shares", help="shared folders (SYNO.Core.Share)")
add("users", help="local users (SYNO.Core.User)")
add("groups", help="local groups (SYNO.Core.Group)")
add("packages", help="installed packages (SYNO.Core.Package)")
add("services", help="services (SYNO.Core.Service)")
add("connections", help="current connections (SYNO.Core.CurrentConnection)")
pl = add("ls", help="FileStation list (no path = shares)")
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("--version", type=int)
pc.add_argument("--post", action="store_true")
@@ -285,19 +296,21 @@ def main(argv):
# gated convenience writes
for name, helptxt in (("pkg-start", "start 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")
sub.add_parser("reboot", help="reboot the NAS (needs --confirm)")
sub.add_parser("shutdown", help="shut down the NAS (needs --confirm)")
add("reboot", help="reboot the NAS (needs --confirm)")
add("shutdown", help="shut down the NAS (needs --confirm)")
args = ap.parse_args(argv)
vault = getattr(args, "vault", DEFAULT_VAULT)
confirm = getattr(args, "confirm", False)
def guard(method):
if not args.confirm:
if not confirm:
raise SynoError(f"'{method}' mutates the device -- re-run with --confirm")
try:
c = SynoClient(vault=args.vault)
c = SynoClient(vault=vault)
if args.cmd == "test":
c.login()
d = c.call("SYNO.Core.System", "info")