@synopackageland/cli 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +33 -18
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -846
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/unsupported-types.js +1 -1
  14. package/dist/contract/validate.js +2 -2
  15. package/dist/contract/web-launch.d.ts +1 -12
  16. package/dist/contract/web-launch.js +10 -74
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/e2e/dsm.d.ts +2 -2
  46. package/dist/e2e/dsm.js +10 -6
  47. package/dist/error-registry.js +1020 -834
  48. package/dist/host/share-permission-lookup.d.ts +1 -1
  49. package/dist/host/share-permission.d.ts +1 -1
  50. package/dist/host/share-permission.js +1 -1
  51. package/dist/info.js +120 -28
  52. package/dist/init.js +188 -64
  53. package/dist/inspect.js +21 -6
  54. package/dist/runtime/executor-digests.json +5 -5
  55. package/dist/skill.js +51 -9
  56. package/dist/standalone/api-description-inspect.d.ts +4 -0
  57. package/dist/standalone/api-description-inspect.js +12 -0
  58. package/dist/standalone/api-description.d.ts +7 -0
  59. package/dist/standalone/api-description.js +73 -0
  60. package/dist/standalone/build.d.ts +3 -0
  61. package/dist/standalone/build.js +140 -33
  62. package/dist/standalone/compose-errors.d.ts +3 -0
  63. package/dist/standalone/compose-errors.js +243 -0
  64. package/dist/standalone/compose.d.ts +97 -10
  65. package/dist/standalone/compose.js +306 -75
  66. package/dist/standalone/daemon.js +111 -29
  67. package/dist/standalone/dsm-window.js +39 -8
  68. package/dist/standalone/identity-gateway.d.ts +9 -2
  69. package/dist/standalone/identity-gateway.js +282 -66
  70. package/dist/standalone/inspect-spk.d.ts +12 -0
  71. package/dist/standalone/inspect-spk.js +666 -136
  72. package/dist/standalone/lifecycle.js +124 -42
  73. package/dist/standalone/manifest.d.ts +8 -1
  74. package/dist/standalone/manifest.js +303 -86
  75. package/dist/standalone/resource.js +6 -2
  76. package/dist/standalone/spk.d.ts +4 -1
  77. package/dist/standalone/spk.js +138 -39
  78. package/dist/template-paths.d.ts +0 -2
  79. package/dist/template-paths.js +0 -5
  80. package/dist/test-command.d.ts +1 -1
  81. package/dist/test-command.js +1 -1
  82. package/dist/validate.d.ts +3 -3
  83. package/dist/validate.js +32 -140
  84. package/package.json +5 -3
  85. package/schema/synopkgland-api-v1.schema.json +423 -0
  86. package/skills/SKILL.md +4 -2
  87. package/skills/reference/compose.md +349 -0
  88. package/skills/reference/create.md +9 -18
  89. package/skills/reference/ctl.md +41 -0
  90. package/skills/reference/deploy.md +1 -1
  91. package/skills/reference/files.md +5 -1
  92. package/skills/reference/identity-gateway.md +8 -144
  93. package/skills/reference/wrap.md +10 -39
  94. package/assets/go/daemon/fixture_linux_test.go +0 -15
  95. package/assets/go/daemon/fixture_other_test.go +0 -7
  96. package/assets/go/daemon/go.mod +0 -5
  97. package/assets/go/daemon/main.go +0 -636
  98. package/assets/go/daemon/main_test.go +0 -315
  99. package/assets/go/daemon/network.go +0 -107
  100. package/assets/go/daemon/network_test.go +0 -86
  101. package/assets/go/daemon/process_linux.go +0 -55
  102. package/assets/go/daemon/process_linux_test.go +0 -161
  103. package/assets/go/daemon/process_other.go +0 -14
  104. package/assets/go/daemon/testdata/child.go +0 -44
  105. package/assets/go/gateway/auth.go +0 -101
  106. package/assets/go/gateway/auth_test.go +0 -216
  107. package/assets/go/gateway/cgi.go +0 -133
  108. package/assets/go/gateway/cgi_test.go +0 -156
  109. package/assets/go/gateway/csrf.go +0 -73
  110. package/assets/go/gateway/csrf_test.go +0 -155
  111. package/assets/go/gateway/directory.go +0 -108
  112. package/assets/go/gateway/directory_test.go +0 -202
  113. package/assets/go/gateway/gateway.go +0 -412
  114. package/assets/go/gateway/gateway_test.go +0 -676
  115. package/assets/go/gateway/go.mod +0 -5
  116. package/assets/go/gateway/groups.go +0 -66
  117. package/assets/go/gateway/main.go +0 -45
  118. package/dist/build-spk/gateway.d.ts +0 -31
  119. package/dist/build-spk/gateway.js +0 -60
  120. package/dist/compose/privilege.d.ts +0 -14
  121. package/dist/compose/privilege.js +0 -102
@@ -0,0 +1,349 @@
1
+ # Upstream Compose packages
2
+
3
+ The CLI converts schema-2 declarations to an SPK. The SDK owns Compose planning,
4
+ identity gateway and lifecycle control. There is no separate wrapper container
5
+ or SSO Server. Upstream files stay byte-for-byte unchanged, including comments.
6
+
7
+ ```yaml
8
+ schema: 2
9
+ package: MediaApp
10
+ version: 0.1.0
11
+ build: 1
12
+ displayname: Media App
13
+ description: Upstream application
14
+ maintainer: Example
15
+ os_min_ver: "7.2.1-69057"
16
+ arches: [x86_64, aarch64]
17
+ capabilities: {}
18
+ compose:
19
+ files: [compose.yaml, site.yaml]
20
+ env_files: [.env]
21
+ profiles: []
22
+ web: {service: web, port: 8080, health: {path: /health, status: 200}}
23
+ network: {egress: isolated}
24
+ # Private package binds are rejected by DSM build; keep compatible upstream named volumes.
25
+ identity_gateway:
26
+ sdk: 0.15.1
27
+ mode: none
28
+ access: dsm-users
29
+ listen: {http: 28000}
30
+ ui: {launch: new-page}
31
+ ```
32
+
33
+ No backend, dummy CGI, UI directory or helper payload is required. Omit `web`
34
+ and `identity_gateway` for headless projects. Optional `compose.helpers` names
35
+ architecture-specific native payloads for token/account adapters. All lifecycle
36
+ scripts run as the package account; containers are managed by Container Manager.
37
+
38
+ ## Planning and artifacts
39
+
40
+ The pinned compose-go reference loader merges ordered files, env files and
41
+ profiles. Process environment never supplies interpolation values. Local include,
42
+ extends, env_file and label_file dependencies are bounded, confined and copied
43
+ without symlinks. Remote/interpolated dependencies are rejected. CLI import uses
44
+ the SDK inventory, not a second parser for dependency semantics.
45
+
46
+ `compose/upstream/` preserves input bytes. `generated.override.yaml` is a real
47
+ reference-loader replacement override: lists such as ports and mounts use
48
+ `!override` so original wildcard publications cannot survive list merging.
49
+ `docker-compose.yml` is the final tag-free worker document. `compose.lock.json`
50
+ records input/effective hashes, image/platform inventory, fixed identities,
51
+ mounts, networking and explicit acceptance blockers. `images.json` contains the
52
+ registry manifests/config bytes needed for offline image-volume verification.
53
+ Inspection replays the trusted SDK planner over the packaged inputs and compares
54
+ actual effective, override, lock, launcher, gateway, resources and lifecycle.
55
+
56
+ Build resolves anonymous registry metadata and pins the Linux platform manifest.
57
+ It verifies pinned upstream index/manifest digests and image config hashes, and
58
+ turns image-declared anonymous VOLUMEs into stable package-namespaced volumes.
59
+ Private registry credentials are not inherited. `SYNOPKGLAND_COMPOSE_REGISTRY_MIRRORS`
60
+ is an explicit build-host JSON map of registry names to HTTPS origins; it changes
61
+ metadata transport only, never the repository or runtime reference. Mirrors are
62
+ a build operator trust decision. `check`/`inspect` remain offline.
63
+
64
+ ## Support boundary
65
+
66
+ | Feature | Current behavior |
67
+ | --- | --- |
68
+ | Files in one directory, profiles, anchors, depends_on, healthcheck | Reference-loader semantics retained; DSM worker compatibility needs device acceptance |
69
+ | Named local volumes | Stable package namespace, retained across recreation; inspect inventory |
70
+ | Private bind data | DSM build rejects COMPOSE_PRIVATE_BIND_TIMING_UNSUPPORTED: clean-install preinst precedes pkgvar creation, while worker acquire precedes postinst; no root initialization or automatic volume conversion |
71
+ | Image VOLUME | Build inventories verified image config and assigns a stable named volume |
72
+ | Existing external volume | Rejected without trusted admin ownership/driver readback |
73
+ | Existing Shared Folder | Explicit opt-in intent only; blocked settings page until official protected ACL/project control exists |
74
+ | Bridge | Default no-publish dedicated internal bridge with a package-derived fixed IP; availability requires App health, TCP and the current lifecycle acquire record; no Docker identity attestation |
75
+ | Host | Explicit verified OIDC web consumer with address_env forced to 127.0.0.1; no container-root; internet access is visible |
76
+ | Egress | isolated internal networks, or explicit internet outbound bridge; fine allowlist declarations fail |
77
+ | Public ports | All upstream publishes removed; the host gateway dials the declared web service on its fixed bridge IP; auxiliary ingress is unsupported |
78
+ | Root | Fixed package UID/GID by default; per-service container-root requires reason, bridge and no host binds; only CHOWN/FOWNER/SETUID/SETGID opt-ins |
79
+ | Build, hooks, privileged, host PID/IPC, devices, sockets | Rejected; no host root workaround |
80
+ | Secret/config file resources, plugins, replicas/deploy | Rejected until their bounded delivery/runtime contract exists |
81
+ | Subpath hosting | Unsupported; use the app's dedicated origin at / |
82
+
83
+ Container-root is distinct from host elevation. Named-volume ownership and an
84
+ image's arbitrary-UID compatibility are not proved by a valid plan. Test the
85
+ actual immutable image, initialization, health, UID/capabilities and restart.
86
+ Unknown compatibility is never a certification result.
87
+
88
+ ## Shared Folder authority
89
+
90
+ Containers use a fixed package identity. **They do not inherit the signed-in
91
+ user's File Station ACL.** App library/share settings control which app users
92
+ see mounted data. Read-only prevents source changes; read/write can permit file
93
+ modification and deletion. DSM identity/account bridge and file authority are
94
+ separate features. Per-DSM-user filesystem access requires a File Station session
95
+ or Broker Adapter; it is outside this change and cannot be obtained through a mount.
96
+
97
+ Declare `capabilities: {composeMounts: true}` and, for example:
98
+
99
+ ```yaml
100
+ compose:
101
+ shares:
102
+ media:
103
+ select: admin-after-install
104
+ permission: ro
105
+ mounts: [{service: web, target: /media}]
106
+ ```
107
+
108
+ Selection is after installation by an administrator in the App, not a claim
109
+ that the installation wizard can enumerate existing Shares. `install-draft`
110
+ is only a draft intent. Until authorization, only SDK settings start and no
111
+ upstream docker-project is acquired. The independent authority purpose is
112
+ `compose-mount`; a user destination grant from Download Hub/delegate never
113
+ permits a package-wide mount.
114
+
115
+ The current implementation does **not** issue a mount grant, invoke ACL tools,
116
+ install a protected executor for Compose, or activate/revoke an upstream project.
117
+ Confirmation returns `COMPOSE_PROJECT_CONTROL_UNAVAILABLE`. It must remain
118
+ unsupported until an official ACL plus fixed project-control adapter can use
119
+ protected policy/digest/generation authority, stop/recreate/read back and revoke
120
+ without reviving stale grants. No app runs as host root to bypass this boundary.
121
+
122
+ ## Gateway transport
123
+
124
+ `none` keeps native app login; `header`, `token` and embedded `oidc` use supported
125
+ upstream interfaces plus the DSM account bridge. Access defaults to dsm-users;
126
+ admins/groups revalidate NSS membership per request. Public access is explicit
127
+ and only valid with none. Reserved DSM id/did and this gateway's cookies are
128
+ filtered in both directions; other Cookie order/duplicates and Set-Cookie
129
+ attributes are preserved. Claimed identity and DSM token headers are stripped.
130
+
131
+ Upstream POST does not require SynoToken. Browser mutations and WebSocket upgrades
132
+ require exact scheme/host/port Origin, or Referer when Origin is absent, plus
133
+ SameSite gateway cookies. Missing/null/foreign/duplicate origin fails. Explicit
134
+ app-authenticated passthrough can serve native clients without browser headers;
135
+ when browser evidence is present it is still checked. No identity is injected
136
+ into passthrough. SynoToken remains required at DSM's own authorization CGI.
137
+
138
+ WebSocket frames, SSE flush, streamed uploads, Range/HEAD downloads and redirects
139
+ have transport tests. Uploads default to 16 GiB; max_upload_bytes can reduce it.
140
+ Known oversize bodies are rejected before proxying; streamed oversize errors
141
+ return 413. Dial, TLS and response-header deadlines are separate; there is no
142
+ whole-response streaming timeout. Exact upstream absolute redirects change
143
+ origin while retaining encoded path, query and fragment; unrelated IdP URLs and
144
+ relative redirects keep their semantics. No HTML/JS body regex rewriting.
145
+
146
+ ## Device acceptance remains required
147
+
148
+ A Linux gate is not DSM proof. Confirm final SPK digest and DSM/Container Manager
149
+ versions, worker interpolation/model semantics, storage ownership timing,
150
+ declared availability readiness, LAN isolation/egress, protected grant lifecycle, encrypted Share
151
+ ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
152
+ calling the package supported on a NAS. No NAS is accessed by the build gate.
153
+
154
+ The unified Compose contract requires SDK, CLI and templates 0.15.1. Use the published, matching versions together; the legacy packaging route and bridge publish mode are removed.
155
+
156
+ Primary override files, and each included override set, must share a directory. Cross-directory override bases are rejected before loader I/O. Included implicit .env files are confined, inventoried and preserved; symlinks are rejected.
157
+
158
+ ## DSM bridge availability readiness
159
+
160
+ Declare `compose.web.health: {path: /health, status: 200}` using the App's real
161
+ unauthenticated health endpoint (an origin-relative path and expected 2xx status).
162
+ The SDK follows no redirects, uses no proxy/cookies/DSM credentials, and bounds
163
+ TCP/HTTP timeouts. All three conditions are required before gateway admission:
164
+
165
+ 1. The declared App health endpoint returns the expected status.
166
+ 2. The planner-derived fixed bridge IP and container port accept a TCP connection.
167
+ 3. A private, durably written `pkgvar/daemon/compose-acquire.json` records an active
168
+ lifecycle acquire generation, the current installed SPK version and plan digest.
169
+
170
+ DSM's startup worker returns before the generated start hook records acquire.
171
+ Stop/upgrade/uninstall durably release it before stopping the gateway; a new
172
+ start records a fresh generation. Missing, released, foreign or stale state
173
+ fails closed. This is the package lifecycle's record, not DSM runtime readback.
174
+
175
+ **Limitation:** these checks cannot prove that the Docker image, project or
176
+ generation actually running is the declared one. The threat is another local
177
+ process occupying the same port and answering health. This is accepted because
178
+ that process must already have code execution on the NAS. Do not use readiness
179
+ as an identity or isolation attestation. Restore full authoritative verification
180
+ when an official package-account readback channel becomes available. Never grant
181
+ Docker socket access, borrow an admin session or use root for readiness.
182
+
183
+ `inspect` reports `compose.readiness.profile` and this limitation. The old
184
+ bridge-unverified admission blocker is replaced by `COMPOSE_READINESS_INVALID`,
185
+ `COMPOSE_ACQUIRE_GENERATION_INVALID`, `COMPOSE_TCP_NOT_READY` and
186
+ `COMPOSE_HEALTH_NOT_READY`, according to the actual failing condition.
187
+
188
+ Compose SPKs intentionally omit `install_dep_packages=ContainerManager`: DSM can
189
+ report that dependency not ready even while its systemd unit and Docker daemon
190
+ are active. The official `docker-project` worker checks actual availability;
191
+ this does not authorize starting, modifying or repairing Container Manager.
192
+
193
+ For disposable environments, optional `compose.resource_prefix: t-0661-` prefixes
194
+ all generated project/network/volume names while retaining the package ID as the
195
+ namespace suffix. Keep it stable across upgrades: changing it selects new
196
+ resources and does not migrate or delete retained data.
197
+
198
+ Bridge web ingress uses a dedicated `sdk_ingress` internal bridge. The web service
199
+ receives a fixed IPv4 address, and the package-account gateway connects directly
200
+ to its declared container port. Internal bridges were reachable by package-account
201
+ hooks in feasibility probes on an SA3200D and another test NAS. No HAProxy helper, published
202
+ container port, Docker socket or host root is required. Explicit internet egress
203
+ still adds the existing outbound bridge; other App services retain their internal networks.
204
+
205
+ The subnet is a deterministic /29 candidate derived from package ID and
206
+ `compose.network.ingress_candidate` (integer 0-255, default 0). It comes from
207
+ private 10.128.0.0/9; this pool is not globally reserved. Before acquisition, the
208
+ package-account preparer reads kernel IPv4 routes and all interface prefixes, and rejects overlap with
209
+ `COMPOSE_SUBNET_CONFLICT`. Choose another candidate and rebuild; no silent
210
+ renumbering occurs on upgrade. Choose a different candidate for a clean
211
+ installation; automatic migration of an installed network is not provided. The exact previous package allocation can be
212
+ reused through a private `pkgvar/compose-ingress.json` allocation record written
213
+ in postinst. Preinst reads DSM7's physical `<SYNOPKG_PKGDEST_VOL>/@appdata/<id>`
214
+ when `SYNOPKG_PKGVAR` is absent: DSM removes both the old payload and
215
+ `/var/packages/<id>/var` before replacement;
216
+ it does not attest Docker ownership. Missing or invalid history never exempts a
217
+ conflicting route. Container Manager's atomic IPAM check remains the final arbiter for
218
+ concurrent acquisition and networks without a visible host route; the preparer
219
+ does not claim a complete Docker network inventory or network ownership attestation.
220
+ Unavailable kernel inventory fails with `COMPOSE_SUBNET_INVENTORY_UNAVAILABLE`.
221
+
222
+ `inspect` inventories the address, subnet, service, port and candidate. The gateway
223
+ accepts only its package-derived bridge address; arbitrary private-network URLs,
224
+ DNS targets and user-supplied credentials/query strings are not ingress endpoints.
225
+ The readiness profile is `health-fixed-ip-lifecycle` and retains the existing
226
+ availability-only limitation. Host-network OIDC remains a separate explicit opt-in.
227
+
228
+ Bridge publish mode has been removed. Existing `web.host_port` declarations are
229
+ accepted for compatibility and ignored by bridge transport; new scaffolds omit it.
230
+ Legacy `.sc` residue is not purged by this topology. Migrating an already published
231
+ installation requires a separately authorized cleanup or an official worker fix;
232
+ a no-publish rebuild cannot claim to remove historical service registrations.
233
+
234
+ On the test NAS DSM 7.4/Docker 24.0.2, internal networks blocked tested external IPv4
235
+ and external DNS, but allowed NAS host services through the bridge gateway and
236
+ NAS LAN address. Therefore `isolated` is not a host-service firewall. IPv6 had
237
+ no usable default route on this fixture, which is not proof of an IPv6 firewall.
238
+ The official worker added NET_BIND_SERVICE to the capability bounding set even
239
+ with cap_drop ALL; effective/permitted/ambient capabilities were zero for the
240
+ nonroot fixture. Do not claim an empty bounding set or complete NAS isolation.
241
+
242
+ DSM cookie names `id`, `did`, `_SSID`, `_CrPoSt`, `stay_login` and the complete
243
+ `synopkgland_sso_` namespace are reserved in both request Cookie and response
244
+ Set-Cookie. Other App sessions/native cookies retain their normal semantics.
245
+ Cookies are shared across ports on the same hostname, so filtering only this
246
+ package's SDK cookie would leak another App's SDK session to an upstream.
247
+
248
+ Historical publish-topology validation on the test NAS was release-blocked: the Container Manager
249
+ 24.0.2-1706 docker-project worker can return profile error 1202 on update,
250
+ which Package Center reports as postinst-worker failure 277. The owned test
251
+ project and a real Vaultwarden package reproduced this; switching Compose
252
+ publish syntax did not resolve it. No root App, firewall edit or Container
253
+ Manager modification is used as a workaround. Validate acquisition, upgrade
254
+ and uninstall on a working official worker before claiming DSM support.
255
+
256
+ ### Diagnose official worker failures without changing the NAS
257
+
258
+ Package Center's postinst-worker error is a wrapper. Read only the failed
259
+ package's worker messages and the matching timestamp/PID in DSM logs, removing
260
+ session, token, cookie and password fields before displaying or saving output.
261
+ Distinguish these observed failure chains:
262
+
263
+ | Worker result | Matching lower-level evidence | Interpretation |
264
+ | --- | --- | --- |
265
+ | Profile 1202, Package Center 277 | `service_conf_section_set.c:92 Port conflict with existing ports` followed by `profile.cpp:118 Fail to create port section` | Official service-port registration rejected a conflict; malformed Compose syntax is not established by these codes |
266
+ | Compose-up 2201, Package Center 276 | Registry request fails with `x509: certificate signed by unknown authority` | The Docker daemon could not fetch an image; gateway authentication and readiness have not been tested yet |
267
+
268
+ For the first chain, `servicetool --conf-port-conflict-check --tcp PORT` and
269
+ `servicetool --runtime-port-conflict-check --tcp PORT` are read-only checks.
270
+ Read the matching lower-level error even for wrapper 276: a follow-up failed
271
+ reinstall returned 276 with profile 1202, rather than a registry failure.
272
+ Record the reported service/process owner. A clean readback after uninstall
273
+ does not identify the conflict that existed during the failed operation.
274
+ Do not remove service profiles, disable firewall checks, bypass the official
275
+ worker, change ports automatically or restart Container Manager to turn the
276
+ failure into a pass.
277
+
278
+ Container Manager's status script also requires `synosystemctl` on PATH. A
279
+ status failure from a restricted SSH environment alone does not prove daemon
280
+ failure; correlate the script's actual error, service-unit state and Docker
281
+ server response. Keep administrative diagnostics separate from the App, which
282
+ continues to run as its package account without Docker socket access.
283
+
284
+ An additional DSM 7.3.2 / Container Manager 24.0.2-1606 fixture passed Header
285
+ and Vaultwarden install/start/upgrade. This does not certify the failing DSM
286
+ 7.4 / 24.0.2-1706 environment. Its explicit test configuration used immutable
287
+ mirror image references and declared internet egress for the web service; it
288
+ does not extend the isolated-ingress or original-registry support evidence.
289
+ The same Header SPK also passed install/start/upgrade on a second DSM 7.4 /
290
+ 24.0.2-1706 device. The original failure therefore cannot be classified as
291
+ affecting every device with that Container Manager version.
292
+
293
+ Official uninstall on both additional fixtures removed the packages, projects,
294
+ containers and networks but left their `docker_<container>.sc` service-port
295
+ profiles. Read-only configuration checks still reported those ports as reserved
296
+ after the containers were gone. This is a separate cleanup acceptance failure;
297
+ successful upgrade/reinstall does not clear it. The retained profiles are a
298
+ possible source of later port conflicts, not proof of the original conflict's
299
+ owner. Do not add a root App hook or delete DSM service configuration as a
300
+ workaround; record the owned profiles and resolve the official worker cleanup
301
+ contract before claiming complete uninstall support.
302
+
303
+ ### Official port-profile cleanup boundary
304
+
305
+ Follow-up tests isolated the uninstall defect from SDK-generated ingress. Minimal
306
+ packages containing only the official docker-project resource, package-account
307
+ hooks and a non-root Node container reproduced retained port reservations with
308
+ all three forms: long loopback publish, short loopback publish and wildcard
309
+ publish. Distinct project names and distinct ports allowed two Apps to run
310
+ concurrently, but did not make their uninstall clean. A no-publish control did
311
+ not create a port-profile `.sc`. Changing syntax or exposing the port to the LAN
312
+ is therefore not a validated cleanup fix.
313
+
314
+ Read-only inspection of Container Manager 24.0.2-1606 and 1706 found that
315
+ Project.Clean calls Compose.Down before RemoveProfileForAllContainers; that
316
+ cleanup method obtains IDs through Compose.Ps `-qa`. Once Down has removed the
317
+ containers, that lookup can produce no profiles to unregister. The observed
318
+ files were created during install acquisition, before the package was started.
319
+ This supports an official worker cleanup defect; it does not establish the
320
+ exact internal conflict owner or race during a separate failed upgrade.
321
+
322
+ The documented [port-config worker](https://help.synology.com/developer-guide/resource_acquisition/port_config.html)
323
+ was also tested. Even when its protocol-file basename and section matched the
324
+ CM file, DSM registered it as `<package>.sc`. Container Manager separately
325
+ registered `docker_<container>.sc`; releasing port-config removed only the
326
+ package registration. Declaring that resource did not solve the CM residue.
327
+ The [docker-project contract](https://help.synology.com/developer-guide/resource_acquisition/docker-project.html)
328
+ documents project deletion on PREUNINST and build flags, without a separate
329
+ package-account profile-release option.
330
+
331
+ The original isolated Header fixture on the test NAS installed and started from a
332
+ clean configuration, then its upgrade still returned port conflict → 1202 →
333
+ 277. Historical `.sc` residue alone is therefore insufficient to explain or
334
+ resolve that environment's upgrade failure. Preserve the timestamp/PID chain;
335
+ do not report a clean post-uninstall check as proof that upgrade was fixed.
336
+
337
+ The publish topology did not pass the complete lifecycle. The no-publish
338
+ implementation above requires its own final-SPK install/upgrade/reinstall/uninstall
339
+ and two-App acceptance before release. An administrator may separately authorize exact
340
+ owned-profile cleanup through official servicetool, with pre-removal hashes and
341
+ file/config readback; this is fixture recovery, never an App hook or a supported
342
+ rootless package cleanup mechanism.
343
+
344
+ A native package-account listener remains another rootless option. The new
345
+ no-publish bridge topology above implements the separately authorized ingress design. Existing host-network OIDC support requires a
346
+ verified loopback address setting, cannot remap ports or enforce isolated
347
+ egress, and is not a general replacement for Header/native-cookie bridge Apps.
348
+ Do not silently switch networking, add Docker socket access, run an App hook as
349
+ root or delete DSM configuration to claim compatibility.
@@ -1,6 +1,6 @@
1
1
  # 建立(Create)
2
2
 
3
- 先選擇 `native-go`(Go CGI + DSM 視窗)、`native-python`/`native-node`/`native-rust`(同一個 App,backend 換成 Python、Node.js(TypeScript)或 Rust)、`binary`(預建服務程式)或 `compose`(上游容器)。`compose` 預設示範 Share 與固定 run-as 使用者,`--from <compose.yaml>` 匯入現有 Compose。
3
+ 先選擇 `native-go`(Go CGI + DSM 視窗)、`native-python`/`native-node`/`native-rust`(同一個 App,backend 換成 Python、Node.js(TypeScript)或 Rust)、`binary`(預建服務程式)或 `compose`(上游容器)。`compose` 產生 schema-2 的上游 Compose 範例與 DSM 差異宣告,`--from <compose.yaml>` 匯入現有 Compose。
4
4
 
5
5
  ```sh
6
6
  synopkgland init -t native-go --with files,jobs,login --package MyApp my-app
@@ -17,7 +17,8 @@ synopkgland init -t compose --from ./compose.yaml upstream-app
17
17
 
18
18
  ## 容器存取控制
19
19
 
20
- 有 adminport 的容器 App 預設經 DSM 驗證 gateway(`integration.web.access`:dsm-users 預設、dsm-admins、dsm-groups+groups、public)。Compose published port 必須綁 loopback:`127.0.0.1:<adminport>:<port>`,否則 check 失敗(`COMPOSE_GATEWAY_PORT_EXPOSED`)。gateway 是編譯的 Go,建置需要 Go 與 `--arch`。`public` 才保留匿名 Web Station portal,inspect 會以 `CONTAINER_ANONYMOUS_PORTAL` 提醒。Compose App 沒有 executor,不能宣告 `integration.web.portal`(`CONTRACT_PORTAL_REQUIRES_EXECUTOR`)。
20
+ Compose 只使用 schema-2 `dsm-package.yaml`,上游原樣保留,SDK planner 產生 override 與 worker 單檔。請先讀 [compose.md](compose.md)。預設 none 保留原生登入,另有 identity gateway 的 header/token/OIDC;不架 SSO Server。
21
+ 容器固定使用套件身分,不繼承登入者 File Station ACL。Shared Folder 是安裝後 admin 授權,現階段官方 ACL/project control 未驗證,維持 blocked,不 acquire 上游專案。
21
22
 
22
23
  ## 建置與檢查
23
24
 
@@ -39,8 +40,8 @@ rootOps 與 delegate 已共用管理員 PasswordConfirm 核准的 protected exec
39
40
  backend 寫 `runtime: python|node|rust`(`runtime: go` 等同 `kind: go`)。CGI 是 SDK 的 Go runtime(`bridge/cmd/runtime`),它驗證 DSM session、entry 與 `permissions.json` 後,以套件帳號(不是 root)每個請求啟動一次 App。App 放在 ui 以外的目錄,打包成 `target/app/`,build 寫入 `bridge.json`(spec/bridge/v1)並把它與 `permissions.json` 的 sha256 編進 runtime。
40
41
 
41
42
  ```yaml
42
- backend: {runtime: python, dir: app, entry: main.py, sdk: 0.14.1}
43
- backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.14.1, node: {package: Node.js_v20}}
43
+ backend: {runtime: python, dir: app, entry: main.py, sdk: 0.15.1}
44
+ backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.15.1, node: {package: Node.js_v20}}
44
45
  backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
45
46
  ```
46
47
 
@@ -212,7 +213,7 @@ sweep forward the cancel instead of admitting it.
212
213
  `ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
213
214
  repeats the original request on repeated IDs. Review `example.go` to
214
215
  adapt a real producer; show Saved only from a protected committed receipt.
215
- - Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
216
+ - Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
216
217
  forward. This is not proof of other DSM releases or full writer/reboot behavior.
217
218
 
218
219
  ## 開啟方式
@@ -232,16 +233,6 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
232
233
  `defaultAlias`、Portal 面板/capability 或 Web Station alias;沒有 Portal
233
234
  不是 inspect/info/doctor 問題。明確啟用 `portal` 時才將其 alias 作為 `defaultAlias`。
234
235
 
235
- Docker/web service App 設定 `integration.web.launch`,內建預設是 `new-page`;
236
- 省略開啟方式時直接開新分頁,不宣告 Application Portal。SDK 原生 App
237
- 省略時使用 `dsm-window`;非 UI SDK 原生 App 必須明確寫 `ui.launch: new-page`。
238
- Web service 主選單使用
239
- `type: "url"` 與明確的 `url` 指向既有 DSM gateway。明確 `access: public` 才保留 Web Station
240
- alias;這是服務入口,不是 SDK Application Portal。DSM 會將 standalone URL 改寫成 launchApp 頁面,
241
- 需要 AppInstance/AppWindow JS class;schema 1 web service 沒有該 class,因此使用 url。需要 DSM 視窗時明寫 `dsm-window`。
242
-
243
- Schema 2 的 `dsm-package.yaml` + `identity_gateway` + `compose` 同樣視為
244
- web service:省略 `ui.launch` 時預設 `new-page`,使用 standalone launcher,
245
- 不帶 Application Portal。兩條 Compose 路線共用 `contract/app-launch.ts`
246
- 解析開啟方式與選擇 launcher 型別;明確的 `ui.launch` 可覆寫預設。
247
- 只有沒有 web service 的 SDK 原生 App 省略時才預設 `dsm-window`。
236
+ Compose 在 `dsm-package.yaml` 使用 `ui.launch`,預設 new-page;省略時也不宣告 Application Portal。
237
+
238
+ Compose classless gateway 用 `type: url`,有 AppWindow wrapper 的原生 App 才使用 `standalone`;都沿用 CLI 共用 app-launch 解析。Compose 的差異請見 [compose.md](compose.md)。
@@ -0,0 +1,41 @@
1
+ # Control registered NAS Apps with ctl
2
+
3
+ Use this for operating installed Apps, rather than building/deploying projects.
4
+
5
+ 1. `synopkgland ctl host list --json` checks registered machines and sessions.
6
+ 2. Register with `ctl host add nas --url https://nas.example:5001 --username alice`, or renew with `ctl host login nas`. Credentials are hidden interactive input; `--password-stdin` is available. Do not put passwords/OTP in argv. Sessions and remembered-device tokens use OS secure storage, never a plaintext fallback.
7
+ 3. Run `synopkgland ctl list --host nas --json` for all registered Apps, commands, schemas, effects, permissions and executable examples. If description access needs approval, ask the user to approve the whole candidate batch; do not self-approve. An explicit `--approve-installed` records permission to send the DSM session to the displayed inventory's fixed description endpoints. Use `--app <package>` to narrow the batch. No description endpoint is anonymous.
8
+ 4. Use the discovered alias, CLI path and typed flags with an explicit `--host nas`; inspect `--help` and `--dry-run` before execution. For example, `synopkgland ctl notes list --status pending --host nas --dry-run`. Use the real command/schema from discovery, not this illustrative name.
9
+ 5. Reads need no write confirmation. For authorized write/destructive commands, add `--yes`; without it the CLI returns a plan and confirmation error. A --yes flag does not replace the user's authorization or the App's backend checks.
10
+
11
+ There is no shared current context: with multiple hosts, always specify --host. `--host a,b` and --all-hosts return per-host results, with exit 8 on partial failure. Do not retry a write merely because one host failed; query its state first. --refresh rereads descriptions; unchanged package versions use the host/account/package/version/hash cache. Never assume cached metadata grants resource permission.
12
+
13
+ Use `ctl host show nas` to inspect approvals/pins, `ctl host revoke nas PackageID` to remove an App approval, `ctl host logout nas` to retain registration, and `ctl host remove nas --yes` to delete sessions, device token, approvals and cache. A failed NAS logout is reported independently from successful local removal.
14
+
15
+ Stdout defaults to JSON without a TTY. Success is {ok:true,data}; failure is {ok:false,error:{code,message,hint,allowed?}}. Multi-host output uses {ok,results:[{host,ok,data|error}]}. Follow the concrete hint and complete allowed list; do not guess another method or bypass ctl with raw HTTP. Exit codes: 2 authentication, 3 parameters/names, 4 permissions/trust, 5 App/contract, 6 network, 7 confirmation, 8 partial failure.
16
+
17
+ ## Designing commands for AI
18
+
19
+ Declare explicit `ctl.commands` in dsm-package.yaml; build/inspect compare references with actual route registrations and permissions. Internal bindings are not CLI commands. Each command needs a stable cli path, description, parameter JSON Schema (descriptions/enums/defaults/examples), effect and examples. The build derives permissions; don't duplicate authorization. Missing schema means --input file/- only.
20
+
21
+ Prefer a high-level backend method when sequencing is App-specific. Download Hub's immediate acceptance/background preparation, owner-bound search resultId, destination grants and BT file selection must not become a raw gopeed.request proxy. Offer downloads add/list/pause/resume and search tasks with stable responses. search.stream uses NDJSON; use the fixed ndjson flow, not the JSON method adapter. Torrent import needs a dedicated high-level backend contract, not a CLI-specific sequence.
22
+
23
+ Reclaim scan start/poll/result can use the fixed job flow once backend job identifiers/states have a stable documented shape. Existing job.result can be an explicit read command. Never automatically run cleanup.plan then cleanup.apply: the plan identity, freshness and permanent-deletion confirmation belong in the App backend; mark an eventual cleanup command destructive.
24
+
25
+ Connect status/ports listing can be invoke commands; connecting is accepted, not completed. Use job polling or a high-level method for completion and retain provider/exposure confirmation. Do not hide browser authorization in a supposedly successful CLI call.
26
+
27
+ Only fixed generic flows are supported: invoke, bounded job start/poll/result (--wait/--no-wait), bounded offset pagination, bounded NDJSON collection, and SDK upload. SDK upload currently streams a local file once through nas.files.upload to File Station multipart; there is no generic chunk/start/finish protocol to invent. Metadata cannot specify scripts, arbitrary URLs or expressions; every step is bound to the same package's allowlisted methods and normal server permissions.
28
+
29
+ For AI consumers: execute commands that describe a complete task. Do not manually concatenate hidden API methods, invent job polling, or treat accepted as completed. Let the fixed command adapter handle the declared flow and surface its timeout/partial state.
30
+
31
+ ## Sensitive parameters
32
+
33
+ App authors must mark passkeys, tokens, private URLs, torrent/base64 and upload content with standard JSON Schema `writeOnly: true`, including nested objects, array items and local references. Mark sensitive response fields in resultSchema too. Do not embed real secrets in schema defaults/enums/examples or command examples.
34
+
35
+ CLI plans, dry-run, missing-yes errors, JSON/multi-host output and known input values repeated by the App use the fixed string `<redacted>`. Schema validation errors never echo instance values or paths. This changes presentation only: authorized requests still send the original values. The CLI does not log or cache business parameters. Sensitive description examples/defaults are masked in list/help and disk caches; a cache altered by redaction requires a fresh authenticated description/hash check before execution. Fill placeholders locally; never paste secrets into AI messages or logs.
36
+
37
+ ## Command naming recommendations
38
+
39
+ Prefer `<resource> <verb>`. Common verbs are `list`, `get`, `export` (often reads), `create`, `update`, `import`, `run` (often writes), and `delete` (often destructive). Pick one synonym within an App; avoid mixing show/get/status for the same task. These are recommendations only: Apps may use domain verbs such as `pause` and `resume`.
40
+
41
+ Every command must explicitly declare its required `effect`. CLI/build never infer or overwrite effect from the verb. The existing check that command effect must not be less restrictive than its bindings remains. There is no verb allowlist or new verb rejection rule.
@@ -5,7 +5,7 @@
5
5
  3. Review `synopkgland deploy --dry-run --arch x86_64`, which does not connect to a NAS.
6
6
  4. Only when installation is authorized, run `synopkgland deploy --host <profile>`.
7
7
 
8
- Deployment uploads directly to DSM. It does not publish artifacts. Container install answers use --share, --run-as and --param; omitted upgrade answers are retained by package preinst, which also rejects missing newly required values.
8
+ Deployment uploads directly to DSM. It does not publish artifacts. Compose uses schema-2 differences and fixed package identity. Existing Shared Folder intent requires admin confirmation after install; official protected ACL/project control is currently blocked. No install answer or user destination grant activates a package mount. See compose.md and its device acceptance blockers.
9
9
 
10
10
  For native-go root-ops, approve fixed operations in DSM after installation. Updates that change the task text require fresh approval. Uninstall removes the executor tasks, the App's Application Privileges rules and a stale portal alias itself. Never delete installed packages or data as part of a code cleanup.
11
11
 
@@ -118,7 +118,11 @@ sweep forward the cancel instead of admitting it.
118
118
  `ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
119
119
  repeats the original request on repeated IDs. Review `example.go` to
120
120
  adapt a real producer; show Saved only from a protected committed receipt.
121
- - Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
121
+ - Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
122
122
  forward. This is not proof of other DSM releases or full writer/reboot behavior.
123
123
 
124
124
  The pinned scope-selection task also binds the protected generation observed before confirmation. The executor compares it under the publish lock and refuses stale tasks, so a retained task cannot replay an older selection after another confirmation.
125
+
126
+ ## Container mounts
127
+
128
+ Containers use a fixed package identity and never inherit the signed-in user's File Station ACL. Mount grants are distinct from user destination grants. Per-user file access requires a session/Broker Adapter, outside this change. Read [compose.md](compose.md); official ACL/project control remains blocked.