@synopackageland/cli 0.15.0 → 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.
- package/CHANGELOG.md +10 -1
- package/README.md +7 -2
- package/dist/contract/unsupported-types.js +1 -1
- package/dist/contract/validate.js +2 -2
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +10 -6
- package/dist/host/share-permission-lookup.d.ts +1 -1
- package/dist/host/share-permission.d.ts +1 -1
- package/dist/host/share-permission.js +1 -1
- package/dist/runtime/executor-digests.json +11 -0
- package/package.json +2 -2
- package/skills/SKILL.md +1 -1
- package/skills/reference/compose.md +6 -6
- package/skills/reference/create.md +3 -3
- package/skills/reference/files.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.1 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
- Require explicit `DSM_E2E_SSH` for e2e tunnels and use `dsm` as the default saved-profile target; set `DSM_E2E_TARGET` to reuse an earlier target name. Anonymize internal NAS fixtures and documentation.
|
|
6
|
+
- Pin templates and SDK fixtures/examples to 0.15.1. Add the 0.15.1 executor/lifecycle digest entry and retain the published 0.15.0 pins unchanged.
|
|
7
|
+
- Cover package-named control.sh refusal logs and rejected `.executor` / `.policy.json` staging cleanup.
|
|
8
|
+
- Executor task definitions change: Apps rebuilt with SDK 0.15.1 require one administrator reapproval after upgrade.
|
|
9
|
+
|
|
10
|
+
繁中摘要:e2e tunnel 必須明確設定 `DSM_E2E_SSH`,profile 預設 target 改為 `dsm`(沿用舊 profile 時設定 `DSM_E2E_TARGET`);清理內部 NAS 範例資訊。templates/SDK pins 同步 0.15.1;新增同版三架構 executor/lifecycle digest,保留 0.15.0 pins。驗證 control.sh 拒絕 log 的套件名與 runtime copies 清除。**task 定義改變:App 使用新 SDK 重建升級後,管理員需重新核准一次。**
|
|
11
|
+
|
|
3
12
|
## 0.15.0 (2026-10-18)
|
|
4
13
|
|
|
5
14
|
- Add `synopkgland ctl` host/session management, authenticated API v1 discovery, explicit command trust, typed invocation and fixed job/pagination/NDJSON/upload flows. Commands keep App backend authorization; writes require confirmation.
|
|
@@ -329,7 +338,7 @@ before its timeout.
|
|
|
329
338
|
- The window approval of an upgrade (outdated tasks) makes 11 scheduler calls
|
|
330
339
|
instead of 12: the outdated boot task is deleted by the approval's own first
|
|
331
340
|
EventScheduler get, made moments before the password, instead of a second
|
|
332
|
-
get (2.9 s on
|
|
341
|
+
get (2.9 s on the test NAS in t-0484). The control task is still read again by id
|
|
333
342
|
before its delete (spec/scheduler/v1). A fresh approval stays at 8.
|
|
334
343
|
繁中:升級核准的排程呼叫由 12 降為 11(boot 任務不再於刪除前重讀)。
|
|
335
344
|
- A scope approval reads the executor's selection back every second at most
|
package/README.md
CHANGED
|
@@ -150,12 +150,17 @@ await dismissNotifications(); // when a card shows later
|
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
`runWithTunnel` (the `tunnel --` command) opens an SSH tunnel to the NAS
|
|
153
|
-
(`DSM_E2E_SSH`,
|
|
153
|
+
(`DSM_E2E_SSH`, required, e.g. `admin@nas.example`) on a free loopback port and runs the
|
|
154
154
|
command with `DSM_E2E_URL` set. It masks DSM's session cookies (`id`, `did`,
|
|
155
155
|
`_SSID`), `X-SYNO-TOKEN`, `SynoToken` and `_sid` as `***` in the command's
|
|
156
156
|
output and in the files under `DSM_E2E_OUTPUT_DIR` (default `.e2e`), and makes
|
|
157
157
|
that directory private (700, files 600); saved sessions keep their cookies.
|
|
158
|
-
|
|
158
|
+
Set `DSM_E2E_SSH` to your NAS SSH destination in the environment or a local
|
|
159
|
+
untracked `.env`; an unset or blank value fails before SSH or the command starts.
|
|
160
|
+
`nas.example` is a fixture hostname; replace it with your own host. Accounts come
|
|
161
|
+
from mode 600 files named by `DSM_E2E_ACCOUNT_<USER>`. Saved profiles use
|
|
162
|
+
`DSM_E2E_TARGET` (default `dsm`) and the user as their names; set the target to
|
|
163
|
+
an existing profile's target name to keep reusing it.
|
|
159
164
|
`signIn` and `openDesktop` also register those values as e2e secrets in the
|
|
160
165
|
test's worker, so e2e masks them in its own report. A test that forwards
|
|
161
166
|
requests with `context.route` and `route.fetch` should end with
|
|
@@ -3,7 +3,7 @@ import { collectInstallParameters } from "./load.js";
|
|
|
3
3
|
* Install parameter types that the schema still accepts but that have no working
|
|
4
4
|
* install path on the live package-only route.
|
|
5
5
|
*
|
|
6
|
-
* Measured on
|
|
6
|
+
* Measured on the test NAS 2026-08-27: both end in DSM broken/150 with no Container
|
|
7
7
|
* Manager project, while a no-parameter control package installed fine on the
|
|
8
8
|
* same NAS. `secret` renders its password field and fails; `homes` renders no
|
|
9
9
|
* input at all. `check` reports these as warnings so build keeps working
|
|
@@ -33,7 +33,7 @@ export function validateContract(parsed, contractFile) {
|
|
|
33
33
|
}
|
|
34
34
|
// DSM 7.4 Package Center refuses an SPK whose package identity exceeds 32
|
|
35
35
|
// characters -- "Invalid file format", before the install starts and with
|
|
36
|
-
// nothing written to synopkg.log. Measured on
|
|
36
|
+
// nothing written to synopkg.log. Measured on a DSM 7.4 test NAS: 32 installs, 33
|
|
37
37
|
// does not.
|
|
38
38
|
if (typeof contract.package === "string" && contract.package.length > DSM_PACKAGE_IDENTITY_MAX) {
|
|
39
39
|
const pos = getValuePosition(doc, ["package"]) ?? { line: 1, column: 1 };
|
|
@@ -164,7 +164,7 @@ function validateInstallParameters(parsed, contractFile) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
// A45/A46: both types are in the schema enum but neither has a working
|
|
167
|
-
// install path on the live package-only route -- measured on
|
|
167
|
+
// install path on the live package-only route -- measured on the test NAS as DSM
|
|
168
168
|
// broken/150 with no Container Manager project, against a no-parameter
|
|
169
169
|
// control that installed fine. Refusing here is what makes the failure
|
|
170
170
|
// honest: `homes` used to validate, build a valid SPK, and only break at
|
package/dist/e2e/dsm.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ type Page = ReturnType<NonNullable<ReturnType<typeof surfaceOf>>["page"]>;
|
|
|
17
17
|
type Locator = ReturnType<Page["locator"]>;
|
|
18
18
|
/** The DSM URL the browser opens: DSM_E2E_URL, else the local end of the SSH tunnel. */
|
|
19
19
|
export declare function dsmUrl(env?: Env): string;
|
|
20
|
-
/** ssh arguments forwarding
|
|
20
|
+
/** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
|
|
21
21
|
export declare function tunnelArgs(env?: Env): string[];
|
|
22
22
|
/** A loopback port nothing listens on now, for a tunnel that names none. */
|
|
23
23
|
export declare function freePort(): Promise<number>;
|
|
@@ -122,7 +122,7 @@ export declare function waitForDesktop(page?: Page): Promise<void>;
|
|
|
122
122
|
/**
|
|
123
123
|
* Closes every DSM notification card; returns how many were closed. DSM shows
|
|
124
124
|
* them a moment after the desktop (the admin's "Admin account security" card
|
|
125
|
-
* about 2 s later on
|
|
125
|
+
* about 2 s later on a DSM 7.4 test NAS), so it first waits up to
|
|
126
126
|
* DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
|
|
127
127
|
*/
|
|
128
128
|
export declare function dismissNotifications(page?: Page, env?: Env): Promise<number>;
|
package/dist/e2e/dsm.js
CHANGED
|
@@ -41,11 +41,14 @@ function forwards(env) {
|
|
|
41
41
|
...extra(env.DSM_E2E_TUNNEL_HTTPS_PORT, env.DSM_E2E_REMOTE_HTTPS_PORT, "443"),
|
|
42
42
|
];
|
|
43
43
|
}
|
|
44
|
-
/** ssh arguments forwarding
|
|
44
|
+
/** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
|
|
45
45
|
export function tunnelArgs(env = process.env) {
|
|
46
|
+
const target = env.DSM_E2E_SSH?.trim();
|
|
47
|
+
if (!target)
|
|
48
|
+
throw new Error("DSM_E2E_SSH is required to open a NAS tunnel; set it to user@nas.example (using your NAS host).");
|
|
46
49
|
return ["-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes",
|
|
47
50
|
...forwards(env).flatMap(([local, remote]) => ["-L", `127.0.0.1:${local}:127.0.0.1:${remote}`]),
|
|
48
|
-
|
|
51
|
+
target];
|
|
49
52
|
}
|
|
50
53
|
/** A loopback port nothing listens on now, for a tunnel that names none. */
|
|
51
54
|
export function freePort() {
|
|
@@ -80,12 +83,13 @@ export async function runWithTunnel(command, outer = process.env) {
|
|
|
80
83
|
throw new Error("runWithTunnel needs a command to run.");
|
|
81
84
|
const port = Number(outer.DSM_E2E_TUNNEL_PORT || await freePort());
|
|
82
85
|
const env = { ...outer, DSM_E2E_TUNNEL_PORT: String(port) };
|
|
86
|
+
const args = tunnelArgs(env);
|
|
83
87
|
const ports = forwards(env).map(([local]) => Number(local));
|
|
84
88
|
for (const used of ports) {
|
|
85
89
|
if (await portOpen(used))
|
|
86
90
|
throw new Error(`Port ${used} is already in use; name another DSM_E2E_TUNNEL_* port or close it before opening the tunnel.`);
|
|
87
91
|
}
|
|
88
|
-
const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh",
|
|
92
|
+
const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh", args, { stdio: ["ignore", "ignore", "inherit"] });
|
|
89
93
|
let sshExited = false;
|
|
90
94
|
ssh.once("exit", () => { sshExited = true; });
|
|
91
95
|
try {
|
|
@@ -93,7 +97,7 @@ export async function runWithTunnel(command, outer = process.env) {
|
|
|
93
97
|
for (const waiting of ports) {
|
|
94
98
|
while (!(await portOpen(waiting))) {
|
|
95
99
|
if (sshExited || Date.now() > deadline)
|
|
96
|
-
throw new Error(`The SSH tunnel to ${
|
|
100
|
+
throw new Error(`The SSH tunnel to ${args.at(-1)} did not open on port ${waiting}.`);
|
|
97
101
|
await new Promise(accept => setTimeout(accept, 100));
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -364,7 +368,7 @@ const notificationCloseSelector = ".v-notification-container .v-notification-pan
|
|
|
364
368
|
/**
|
|
365
369
|
* Closes every DSM notification card; returns how many were closed. DSM shows
|
|
366
370
|
* them a moment after the desktop (the admin's "Admin account security" card
|
|
367
|
-
* about 2 s later on
|
|
371
|
+
* about 2 s later on a DSM 7.4 test NAS), so it first waits up to
|
|
368
372
|
* DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
|
|
369
373
|
*/
|
|
370
374
|
export async function dismissNotifications(page = dsmPage(), env = process.env) {
|
|
@@ -454,7 +458,7 @@ async function restoredSessionLive(page) {
|
|
|
454
458
|
*/
|
|
455
459
|
export async function signIn(app, user = "admin", env = process.env) {
|
|
456
460
|
const url = dsmUrl(env);
|
|
457
|
-
const file = profilePath(env.DSM_E2E_TARGET || "
|
|
461
|
+
const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
|
|
458
462
|
const context = surface().context();
|
|
459
463
|
const profile = loadProfile(file, url);
|
|
460
464
|
if (profile)
|
|
@@ -8,7 +8,7 @@ import type { SharePermissionRow } from "./share-permission.js";
|
|
|
8
8
|
* the install proceed, while an empty list would read as a real denial and
|
|
9
9
|
* refuse the install on a transport error.
|
|
10
10
|
*
|
|
11
|
-
* The APIs used were confirmed present on DSM 7.4 (
|
|
11
|
+
* The APIs used were confirmed present on DSM 7.4 (the test NAS):
|
|
12
12
|
* `SYNO.Core.Share.Permission`, `SYNO.Core.Group`, `SYNO.Core.Group.Member`.
|
|
13
13
|
* `SYNO.Core.User.Group` also exists on the NAS but has no generated
|
|
14
14
|
* synoagentcli operation, which is why membership is derived group-by-group.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* extra_values directly, and share permission can change after install.
|
|
11
11
|
*
|
|
12
12
|
* The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
|
|
13
|
-
* before changing the predicate: on
|
|
13
|
+
* before changing the predicate: on the test NAS, `newadmin` has is_writable=false on
|
|
14
14
|
* its own row for `pkgland-p0b` and still writes there, because the
|
|
15
15
|
* `administrators` group row carries the grant. A user-row-only check rejects
|
|
16
16
|
* the only configuration with live proof behind it.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* extra_values directly, and share permission can change after install.
|
|
11
11
|
*
|
|
12
12
|
* The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
|
|
13
|
-
* before changing the predicate: on
|
|
13
|
+
* before changing the predicate: on the test NAS, `newadmin` has is_writable=false on
|
|
14
14
|
* its own row for `pkgland-p0b` and still writes there, because the
|
|
15
15
|
* `administrators` group row carries the grant. A user-row-only check rejects
|
|
16
16
|
* the only configuration with live proof behind it.
|
|
@@ -9,5 +9,16 @@
|
|
|
9
9
|
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
10
10
|
},
|
|
11
11
|
"goToolchain": "go1.26.9"
|
|
12
|
+
},
|
|
13
|
+
"0.15.1": {
|
|
14
|
+
"x86_64": "4f47fb43f6e60dbad064afb0bbec96dd009fa046670e2c4b590b128a855cb404",
|
|
15
|
+
"aarch64": "c903b09ad657a838f0c62b6688e65e9094572b22b969770fca92fcc3640a5364",
|
|
16
|
+
"armv7": "8178d90de7ea720530bb9496e50ed118086b0e57382d84675aadc68f482d219f",
|
|
17
|
+
"lifecycle": {
|
|
18
|
+
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
19
|
+
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
20
|
+
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
21
|
+
},
|
|
22
|
+
"goToolchain": "go1.26.9"
|
|
12
23
|
}
|
|
13
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@napi-rs/keyring": "^2.1.1",
|
|
29
|
-
"@synopackageland/templates": "0.15.
|
|
29
|
+
"@synopackageland/templates": "0.15.1",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"commander": "^13.1.0",
|
|
32
32
|
"json-canonicalize": "^3.0.1",
|
package/skills/SKILL.md
CHANGED
|
@@ -129,7 +129,7 @@ sweep forward the cancel instead of admitting it.
|
|
|
129
129
|
`ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
|
|
130
130
|
repeats the original request on repeated IDs. Review `example.go` to
|
|
131
131
|
adapt a real producer; show Saved only from a protected committed receipt.
|
|
132
|
-
- Wizard support is measured on
|
|
132
|
+
- Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
|
|
133
133
|
forward. This is not proof of other DSM releases or full writer/reboot behavior.
|
|
134
134
|
|
|
135
135
|
## 開啟方式
|
|
@@ -23,7 +23,7 @@ compose:
|
|
|
23
23
|
network: {egress: isolated}
|
|
24
24
|
# Private package binds are rejected by DSM build; keep compatible upstream named volumes.
|
|
25
25
|
identity_gateway:
|
|
26
|
-
sdk: 0.15.
|
|
26
|
+
sdk: 0.15.1
|
|
27
27
|
mode: none
|
|
28
28
|
access: dsm-users
|
|
29
29
|
listen: {http: 28000}
|
|
@@ -151,7 +151,7 @@ declared availability readiness, LAN isolation/egress, protected grant lifecycle
|
|
|
151
151
|
ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
|
|
152
152
|
calling the package supported on a NAS. No NAS is accessed by the build gate.
|
|
153
153
|
|
|
154
|
-
The unified Compose contract requires SDK, CLI and templates 0.15.
|
|
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
155
|
|
|
156
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
157
|
|
|
@@ -198,7 +198,7 @@ resources and does not migrate or delete retained data.
|
|
|
198
198
|
Bridge web ingress uses a dedicated `sdk_ingress` internal bridge. The web service
|
|
199
199
|
receives a fixed IPv4 address, and the package-account gateway connects directly
|
|
200
200
|
to its declared container port. Internal bridges were reachable by package-account
|
|
201
|
-
hooks in
|
|
201
|
+
hooks in feasibility probes on an SA3200D and another test NAS. No HAProxy helper, published
|
|
202
202
|
container port, Docker socket or host root is required. Explicit internet egress
|
|
203
203
|
still adds the existing outbound bridge; other App services retain their internal networks.
|
|
204
204
|
|
|
@@ -231,7 +231,7 @@ Legacy `.sc` residue is not purged by this topology. Migrating an already publis
|
|
|
231
231
|
installation requires a separately authorized cleanup or an official worker fix;
|
|
232
232
|
a no-publish rebuild cannot claim to remove historical service registrations.
|
|
233
233
|
|
|
234
|
-
On
|
|
234
|
+
On the test NAS DSM 7.4/Docker 24.0.2, internal networks blocked tested external IPv4
|
|
235
235
|
and external DNS, but allowed NAS host services through the bridge gateway and
|
|
236
236
|
NAS LAN address. Therefore `isolated` is not a host-service firewall. IPv6 had
|
|
237
237
|
no usable default route on this fixture, which is not proof of an IPv6 firewall.
|
|
@@ -245,7 +245,7 @@ Set-Cookie. Other App sessions/native cookies retain their normal semantics.
|
|
|
245
245
|
Cookies are shared across ports on the same hostname, so filtering only this
|
|
246
246
|
package's SDK cookie would leak another App's SDK session to an upstream.
|
|
247
247
|
|
|
248
|
-
Historical publish-topology
|
|
248
|
+
Historical publish-topology validation on the test NAS was release-blocked: the Container Manager
|
|
249
249
|
24.0.2-1706 docker-project worker can return profile error 1202 on update,
|
|
250
250
|
which Package Center reports as postinst-worker failure 277. The owned test
|
|
251
251
|
project and a real Vaultwarden package reproduced this; switching Compose
|
|
@@ -328,7 +328,7 @@ The [docker-project contract](https://help.synology.com/developer-guide/resource
|
|
|
328
328
|
documents project deletion on PREUNINST and build flags, without a separate
|
|
329
329
|
package-account profile-release option.
|
|
330
330
|
|
|
331
|
-
The original isolated Header fixture on
|
|
331
|
+
The original isolated Header fixture on the test NAS installed and started from a
|
|
332
332
|
clean configuration, then its upgrade still returned port conflict → 1202 →
|
|
333
333
|
277. Historical `.sc` residue alone is therefore insufficient to explain or
|
|
334
334
|
resolve that environment's upgrade failure. Preserve the timestamp/PID chain;
|
|
@@ -40,8 +40,8 @@ rootOps 與 delegate 已共用管理員 PasswordConfirm 核准的 protected exec
|
|
|
40
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。
|
|
41
41
|
|
|
42
42
|
```yaml
|
|
43
|
-
backend: {runtime: python, dir: app, entry: main.py, sdk: 0.15.
|
|
44
|
-
backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.15.
|
|
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}}
|
|
45
45
|
backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -213,7 +213,7 @@ sweep forward the cancel instead of admitting it.
|
|
|
213
213
|
`ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
|
|
214
214
|
repeats the original request on repeated IDs. Review `example.go` to
|
|
215
215
|
adapt a real producer; show Saved only from a protected committed receipt.
|
|
216
|
-
- Wizard support is measured on
|
|
216
|
+
- Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
|
|
217
217
|
forward. This is not proof of other DSM releases or full writer/reboot behavior.
|
|
218
218
|
|
|
219
219
|
## 開啟方式
|
|
@@ -118,7 +118,7 @@ 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
|
|
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.
|