@synopackageland/cli 0.17.7 → 0.18.0

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.0 (2026-10-20)
4
+
5
+ - Support the SDK 0.18.x minor line and pin templates 0.18.0 with SDK/App 0.18.0 and UI 0.12.5.
6
+ - Record SDK 0.18.0 executor and lifecycle digests and refresh executor setup fixtures.
7
+ - Add the native OIDC browser bridge and App launch resume flow.
8
+ - Default upstream forks to no ctl integration and document the fixed ctl coverage checklist.
9
+ - Declare optional native-client passthrough capabilities.
10
+
11
+ ## 0.17.8 (2026-10-20)
12
+
13
+ - Update SDK 0.17.7 executor and lifecycle digests for the administrator-role fix.
14
+
3
15
  ## 0.17.7 (2026-10-20)
4
16
 
5
17
  - Inspect Compose Apps with custom icons and localized titles using their archived build inputs, preserving every icon size and strict launcher validation. Preserve declared daemon uninstall hooks when checking fixed lifecycle scripts.
@@ -29,6 +29,7 @@ export function writeForkScaffold(packaging, template, options) {
29
29
  : { kind: 'native-source' };
30
30
  writeFileSync(join(packaging, 'upstream-recipe.json'), JSON.stringify(recipe, null, 2) + '\n');
31
31
  doc.set('entry', 'admin-only');
32
+ doc.delete('ctl');
32
33
  if (official) {
33
34
  doc.setIn(['identity_gateway', 'access'], 'dsm-admins');
34
35
  if (!options.from) {
@@ -54,6 +55,8 @@ export function writeForkScaffold(packaging, template, options) {
54
55
 
55
56
  Keep the upstream fork/history and remote upstream. All wrapper-owned files belong in synology/; never edit upstream files or treat the App as an SDK component. Read synopkgland skill show upstream-fork.
56
57
 
58
+ Leave ctl absent from the manifest by default. Ask whether the user wants AI control; add ctl only when explicitly requested. If requested, read synopkgland skill show ctl and produce ctl-plan covering existing UI actions and public API endpoints, including common reads and writes. Status-only commands do not meet completion. Wrap existing native APIs without changing upstream code or inventing features. Long operations must return through existing asynchronous APIs without waiting for completion; report synchronous-only long APIs as blockers for discussion.
59
+
57
60
  Commit the scaffold, then run bash synology/upgrade.sh <upstream-tag> <build-number>. It fetches the exact tag, verifies ancestry/untouched upstream files, merges, derives version/provenance, checks and builds. Commit synology/ before upgrading again. It does not publish or touch a NAS.
58
61
 
59
62
  ${official ? 'Default: use the upstream official image. upgrade.sh updates its release tag and resolves an OCI index/manifest digest through synopkgland images pin, writing only synology/compose.image.yaml. Container Manager downloads that pinned official image at install. Configure upstream-recipe.json repository/service and imageTag: tag (exact upstream tag) or version (strip v only when upstream image naming requires it). Review multi-architecture support and upstream source/license correspondence. The SELECT_UPSTREAM_TAG placeholder must be replaced by a successful upgrade before check/build.' : 'Native App: implement build-source.sh as the native payload hook; it deliberately fails until implemented. Stage official release binaries pinned by SHA-256 (the OpenList route), or compile the merged source with a source build hook. Verify the release digest before staging architecture-specific daemon.targets; keep the SDK management shell separate.'}
@@ -1,56 +1,12 @@
1
1
  {
2
- "0.17.0": {
3
- "x86_64": "68f372684997258adc61312f2c353e1a286a6b5aaa556f716b4e6ba6f33893d0",
4
- "aarch64": "f6950301e201a4ebc14577a4554fa1b671f90d0f354d1e8809f4d1dafb57c723",
5
- "armv7": "e77c9b6ae22ecfa85d9834af3350192e445987265a7ca6c02ca9b01d88a1b4f7",
6
- "lifecycle": {
7
- "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
8
- "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
9
- "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
10
- },
11
- "goToolchain": "go1.26.9"
12
- },
13
- "0.17.1": {
14
- "x86_64": "68f372684997258adc61312f2c353e1a286a6b5aaa556f716b4e6ba6f33893d0",
15
- "aarch64": "f6950301e201a4ebc14577a4554fa1b671f90d0f354d1e8809f4d1dafb57c723",
16
- "armv7": "e77c9b6ae22ecfa85d9834af3350192e445987265a7ca6c02ca9b01d88a1b4f7",
17
- "lifecycle": {
18
- "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
19
- "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
20
- "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
21
- },
22
- "goToolchain": "go1.26.9"
23
- },
24
- "0.17.3": {
25
- "x86_64": "68f372684997258adc61312f2c353e1a286a6b5aaa556f716b4e6ba6f33893d0",
26
- "aarch64": "f6950301e201a4ebc14577a4554fa1b671f90d0f354d1e8809f4d1dafb57c723",
27
- "armv7": "e77c9b6ae22ecfa85d9834af3350192e445987265a7ca6c02ca9b01d88a1b4f7",
28
- "lifecycle": {
29
- "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
30
- "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
31
- "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
32
- },
33
- "goToolchain": "go1.26.9"
34
- },
35
- "0.17.4": {
36
- "x86_64": "68f372684997258adc61312f2c353e1a286a6b5aaa556f716b4e6ba6f33893d0",
37
- "aarch64": "f6950301e201a4ebc14577a4554fa1b671f90d0f354d1e8809f4d1dafb57c723",
38
- "armv7": "e77c9b6ae22ecfa85d9834af3350192e445987265a7ca6c02ca9b01d88a1b4f7",
39
- "lifecycle": {
40
- "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
41
- "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
42
- "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
43
- },
44
- "goToolchain": "go1.26.9"
45
- },
46
- "0.17.5": {
47
- "x86_64": "af37ecd6bb5ab3e954b5f5bb3db2499b3cf86f0502afdcea18e8dc0614a46e5d",
48
- "aarch64": "87436df3f591e0126d88866922b032968d0dbd01a109de53ab751ef779257baf",
49
- "armv7": "de354d6e582ad1fe3124c3f30eb7f3ce8af456b0fcfad8f23c10421d3a3f3d42",
50
- "lifecycle": {
51
- "x86_64": "f3b368c86677741b48b059699a86c935157ea797634a6116131678e8bbaf33ed",
52
- "aarch64": "b6ccd1365844e6eee9db4ce5833b655f4f1e138bf34317cdbd0e2342d3342a9e",
53
- "armv7": "57b6350c2812783cb536384f8b363c26d894af6589b5f19464d71d8d0a8035d5"
2
+ "0.18.0": {
3
+ "x86_64": "6d9d5a12ac4c990639b31c555560a1b6e49cbe1c267c626a23b91833dcc4452e",
4
+ "aarch64": "efbed582ce1249fed09de72b7bbe553510a55553e44ccf76fa93c684864e8bb8",
5
+ "armv7": "670f83c3349497263c96b0244ec142153c094d5e223dae091c9c8614c0ea04b6",
6
+ "lifecycle": {
7
+ "x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972",
8
+ "aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb",
9
+ "armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681"
54
10
  },
55
11
  "goToolchain": "go1.26.9"
56
12
  }
@@ -1,3 +1,4 @@
1
+ import { oidcResumeRuntime } from "./oidc-resume.js";
1
2
  import { gatewayEntryPath } from './native-gateway.js';
2
3
  import { resolvedSdkLaunch, dsmLauncherType } from "../contract/app-launch.js";
3
4
  import { REOPEN_EVENTS_RUNTIME } from "./reopen-events.js";
@@ -378,6 +379,8 @@ ${ns}.prepareFullPage = function (title) {
378
379
  favicon.href = ${ns}.appIcon;
379
380
  };
380
381
 
382
+ ${manifest.identityGateway?.oidc?.browser ? `${ns}.resumeOIDC = ${oidcResumeRuntime(pkg, Object.values(manifest.identityGateway.listen).filter((p) => typeof p === "number"))};` : ""}
383
+
381
384
  // DSM reopens a window at the size it was last left at, even one saved on a
382
385
  // larger screen, and only moves a window that does not fit: keep it inside.
383
386
  ${ns}.fitDesktop = function (size) {
@@ -405,6 +408,7 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
405
408
  },
406
409
  onOpen: function (param) {
407
410
  this.callParent(arguments);
411
+ ${manifest.identityGateway?.oidc?.browser ? `if (${ns}.resumeOIDC(param)) return;` : ""}
408
412
  var me = this;
409
413
  if (me.mounted) {
410
414
  me.reopenEvents.emit();
@@ -467,6 +471,7 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
467
471
  // with that launch's param. Before the App mounts, it mounts with the latest.
468
472
  onRequest: function (param) {
469
473
  this.callParent(arguments);
474
+ ${manifest.identityGateway?.oidc?.browser ? `if (${ns}.resumeOIDC(param)) return;` : ""}
470
475
  var params = ${ns}.launchParams(param);
471
476
  if (!this.mounted) {
472
477
  if (this.mountPending && Object.keys(params).length) this.launchParams = params;
@@ -24,6 +24,12 @@ export interface GatewayClient {
24
24
  require_pkce: boolean;
25
25
  roles?: GatewayRoles;
26
26
  }
27
+ export interface GatewayBrowser {
28
+ start: string;
29
+ login_paths: string[];
30
+ callback_paths: string[];
31
+ completion_query?: string;
32
+ }
27
33
  /** `identity_gateway`: signs DSM users in to the App's own web UI. */
28
34
  export interface IdentityGateway {
29
35
  /** The SDK release the gateway is built from. */
@@ -40,6 +46,9 @@ export interface IdentityGateway {
40
46
  https?: number;
41
47
  };
42
48
  passthrough: string[];
49
+ optional_passthrough?: Record<string, {
50
+ prefixes: string[];
51
+ }>;
43
52
  redirects: Record<string, string>;
44
53
  email_domain?: string;
45
54
  session_ttl?: number;
@@ -47,6 +56,7 @@ export interface IdentityGateway {
47
56
  headers?: Partial<Record<"user" | "email" | "name" | "groups" | "uid", string>>;
48
57
  oidc?: {
49
58
  issuer_port: number;
59
+ browser?: GatewayBrowser;
50
60
  clients: GatewayClient[];
51
61
  rewrite_json: {
52
62
  method: string;
@@ -133,6 +133,7 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
133
133
  "upstream",
134
134
  "listen",
135
135
  "passthrough",
136
+ "optional_passthrough",
136
137
  "redirects",
137
138
  "email_domain",
138
139
  "session_ttl",
@@ -193,6 +194,28 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
193
194
  const passthrough = v.passthrough ?? [];
194
195
  if (!strings(passthrough, (p) => localPath(p) && p !== "/" && !/^\/_sso(\/|$)/.test(p)))
195
196
  return invalid("passthrough must be path prefixes such as /share/, outside /_sso.");
197
+ const optional = v.optional_passthrough;
198
+ if (optional !== undefined) {
199
+ const [major, minor] = v.sdk.split(".").map(Number);
200
+ if (!(major > 0 || minor >= 18))
201
+ return invalid("optional_passthrough requires the next minor SDK (0.18.0 or later).");
202
+ if (!record(optional) || Object.keys(optional).length > 16)
203
+ return invalid("optional_passthrough is a map of at most 16 fixed entries.");
204
+ const prefixes = [...passthrough];
205
+ const contains = (p, base) => p === base.replace(/\/$/, "") || p.startsWith(base.replace(/\/$/, "") + "/");
206
+ for (const [id, rule] of Object.entries(optional)) {
207
+ if (!name(id) || !record(rule) || !only(rule, ["prefixes"]) ||
208
+ !strings(rule.prefixes, p => localPath(p) && p !== "/" &&
209
+ !p.includes("//") && !p.includes("%") && !/^\/_sso(\/|$)/.test(p), 16) ||
210
+ !rule.prefixes.length)
211
+ return invalid("optional_passthrough entries need a valid id and 1–16 fixed prefixes; they always default off.");
212
+ for (const prefix of rule.prefixes) {
213
+ if (prefixes.some(p => contains(prefix, p) || contains(p, prefix)))
214
+ return invalid("optional and permanent passthrough prefixes must not overlap.");
215
+ prefixes.push(prefix);
216
+ }
217
+ }
218
+ }
196
219
  const redirects = v.redirects ?? {};
197
220
  if (!record(redirects) ||
198
221
  Object.entries(redirects).some(([from, to]) => !localPath(from) || !localPath(to)))
@@ -237,6 +260,7 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
237
260
  ...(listen.https ? { https: listen.https } : {}),
238
261
  },
239
262
  passthrough: passthrough,
263
+ ...(optional !== undefined ? { optional_passthrough: optional } : {}),
240
264
  redirects: redirects,
241
265
  ...(v.email_domain ? { email_domain: v.email_domain } : {}),
242
266
  ...(v.session_ttl ? { session_ttl: v.session_ttl } : {}),
@@ -246,7 +270,7 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
246
270
  if (v.mode === "oidc") {
247
271
  const o = v.oidc;
248
272
  if (!record(o) ||
249
- !only(o, ["issuer_port", "clients", "rewrite_json"]) ||
273
+ !only(o, ["issuer_port", "clients", "rewrite_json", "browser"]) ||
250
274
  !port(o.issuer_port) ||
251
275
  !Array.isArray(o.clients) ||
252
276
  !o.clients.length ||
@@ -266,8 +290,30 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
266
290
  name(r.field))) {
267
291
  return invalid("oidc.rewrite_json entries are { method, path, field }.");
268
292
  }
293
+ const b = o.browser;
294
+ if (b !== undefined) {
295
+ const [major, minor] = v.sdk.split(".").map(Number);
296
+ if (!(major > 0 || minor >= 18))
297
+ return invalid("oidc.browser requires SDK 0.18.0 or later.");
298
+ const path = (p) => localPath(p) && !p.includes("%") && !p.startsWith("/_sso");
299
+ let start;
300
+ if (record(b) && typeof b.start === "string") {
301
+ try {
302
+ start = new URL(b.start, "https://app.invalid");
303
+ }
304
+ catch { /* invalid below */ }
305
+ }
306
+ if (!record(b) || !only(b, ["start", "login_paths", "callback_paths", "completion_query"]) ||
307
+ typeof b.start !== "string" || b.start.length > 1024 || !b.start.startsWith("/") || b.start.startsWith("//") ||
308
+ /[%\\\s#]/.test(b.start) || !start || start.origin !== "https://app.invalid" || !path(b.start.split("?")[0]) ||
309
+ !strings(b.login_paths, path, 16) || !b.login_paths.length ||
310
+ !strings(b.callback_paths, path, 16) || !b.callback_paths.length ||
311
+ (b.completion_query !== undefined && !name(b.completion_query)))
312
+ return invalid("oidc.browser needs fixed local start, login_paths and callback_paths outside /_sso.");
313
+ }
269
314
  gateway.oidc = {
270
315
  issuer_port: o.issuer_port,
316
+ ...(b !== undefined ? { browser: b } : {}),
271
317
  clients: clients,
272
318
  rewrite_json: rewrite,
273
319
  };
@@ -336,6 +382,7 @@ export function identityGatewayConfig(m) {
336
382
  authorize_cgi: `/webman/3rdparty/${m.package}/sso/authorize.cgi`,
337
383
  state_dir: `/var/packages/${m.package}/var/sso`,
338
384
  passthrough: g.passthrough,
385
+ ...(g.optional_passthrough ? { optional_passthrough: g.optional_passthrough } : {}),
339
386
  redirects: g.redirects,
340
387
  ...(g.email_domain ? { email_domain: g.email_domain } : {}),
341
388
  ...(g.session_ttl ? { session_ttl_seconds: g.session_ttl } : {}),
@@ -0,0 +1,2 @@
1
+ /** No caller-provided URL reaches location: only a transaction and a declared port. */
2
+ export declare function oidcResumeRuntime(pkg: string, ports: number[]): string;
@@ -0,0 +1,16 @@
1
+ /** No caller-provided URL reaches location: only a transaction and a declared port. */
2
+ export function oidcResumeRuntime(pkg, ports) {
3
+ return `(function(param) {
4
+ var value = param && param.ssoResume;
5
+ if (typeof value !== 'string') return false;
6
+ var parts = /^([A-Za-z0-9_-]{1,128}):([0-9]{1,5})$/.exec(value);
7
+ if (!parts || ${JSON.stringify(ports)}.indexOf(Number(parts[2])) < 0) return false;
8
+ var target = new URL(${JSON.stringify(`/webman/3rdparty/${pkg}/sso/authorize.cgi`)},location.origin);
9
+ var app = new URL(location.origin); app.port = parts[2]; app.pathname = '/_sso/dsm-return';
10
+ target.searchParams.set('return',app.href);
11
+ target.searchParams.set('txn',parts[1]);
12
+ target.searchParams.set('login_attempt','1');
13
+ location.replace(target.href);
14
+ return true;
15
+ })`;
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.17.7",
3
+ "version": "0.18.0",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@napi-rs/keyring": "^2.1.1",
30
- "@synopackageland/templates": "0.17.8",
30
+ "@synopackageland/templates": "0.18.0",
31
31
  "ajv": "^8.17.1",
32
32
  "commander": "^13.1.0",
33
33
  "json-canonicalize": "^3.0.1",
package/skills/SKILL.md CHANGED
@@ -9,6 +9,8 @@ Use this guide to initialize, build and inspect own applications or upstream OSS
9
9
 
10
10
  For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.17.3 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
11
11
 
12
+ For upstream OSS packaging, leave `ctl` absent from the manifest by default. Ask whether the user wants AI control; implement it only when explicitly requested. Read [reference/ctl.md](reference/ctl.md) for the required `ctl-plan`, UI/API coverage checklist and asynchronous-operation rules. Own Apps retain the native template's ctl defaults, with commands covering their main features; status-only coverage is incomplete.
13
+
12
14
  Start with `synopkgland init -t native-go`, `synopkgland init -t binary`, or `synopkgland init -t compose`. Read reference/create.md or reference/wrap.md, then reference/deploy.md. Native CGI session checks belong to the package. For wrapping upstream OSS with DSM accounts, read [reference/identity-gateway.md](reference/identity-gateway.md) first. For all Compose work, read [reference/compose.md](reference/compose.md): schema-2 upstream files, generated override, SDK planner, fixed package identity and current DSM blockers. Containers do not inherit a signed-in user's File Station ACL. Existing Shared Folder mounts are blocked until official protected ACL/project control is verified.
13
15
 
14
16
  Never install, uninstall, publish or modify NAS data without task authorization. Prefer check, inspect and deploy --dry-run for reviewable artifacts.
@@ -1,5 +1,11 @@
1
1
  # 建立(Create)
2
2
 
3
+ For upstream OSS Apps, ask whether AI control is needed and omit `ctl` from the
4
+ manifest unless explicitly requested. Own Apps retain the template's ctl defaults.
5
+ When designing commands for either kind, follow [ctl-plan and the coverage
6
+ checklist](ctl.md#plan-command-coverage): cover existing main features, including
7
+ common reads and writes, rather than stopping at status commands.
8
+
3
9
  先選擇 `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
10
 
5
11
  ```sh
@@ -14,18 +14,119 @@ Use `ctl host show nas` to inspect approvals/pins, `ctl host revoke nas PackageI
14
14
 
15
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
16
 
17
+ ## Plan command coverage
18
+
19
+ For packaging an upstream open-source App (including forks with `synology/`),
20
+ ask whether the user needs AI control unless it was already explicitly requested.
21
+ Without that opt-in, omit the manifest's `ctl` block and do not build ctl adapters.
22
+ Own Apps retain their native template ctl defaults and the command guidance below.
23
+ For both kinds, commands must cover the App's existing main features, including
24
+ common reads and writes; `status` / `service status` alone are incomplete.
25
+
26
+ Before implementing commands, create `ctl-plan` in the task's requested artifact
27
+ location (otherwise `ctl-plan.md` in the App's documentation; upstream wrappers
28
+ keep it under `synology/`). Record each operation, existing UI action/API endpoint,
29
+ proposed CLI path, effect, caller permission, upstream identity, risk/confirmation,
30
+ coverage or exclusion reason, and validation evidence. Use these steps every time:
31
+
32
+ 1. List every main UI page and its primary buttons: list, details, create, update,
33
+ delete and enable/disable where present. Trace each action to its existing API.
34
+ 2. Inventory every externally exposed App CGI/API endpoint and every upstream
35
+ native API endpoint for the selected version, including non-REST transports.
36
+ Cross-reference each endpoint with the UI inventory and proposed commands.
37
+ Record internal-only, duplicate, sensitive or unsupported endpoints with a
38
+ specific exclusion reason; do not infer ctl coverage from route registration.
39
+ 3. Check each category below against the UI/API inventory. If present, cover its
40
+ common operations; if absent, record `N/A` with evidence and a reason. Record
41
+ unsupported existing capabilities as blockers, not as absent features.
42
+
43
+ | Category | Required inventory |
44
+ | --- | --- |
45
+ | Main UI tasks | Every main page/button, including reads and available mutations |
46
+ | API endpoints | Every external App CGI/API and upstream native endpoint, with a command mapping or exclusion reason |
47
+ | Settings | Read and modify the existing main settings |
48
+ | Diagnostics | Existing status, health, version and log-summary operations |
49
+ | Search | Existing search, filters and bounded result traversal |
50
+ | Long operations | Existing asynchronous start, progress/status, result, cancel and list capabilities, only where provided |
51
+ | Import/export | Existing formats, limits and sensitive output |
52
+ | Permissions | Existing access, grant and authorization-status queries |
53
+
54
+ Completion requires every primary UI action and every public API endpoint to have
55
+ a command mapping or a documented exclusion reason. All applicable categories
56
+ must be accounted for, with common reads and writes validated. A blocked main
57
+ task means coverage is incomplete even if its exclusion is documented. Review the
58
+ plan against the actual selected source/UI, not a guessed list of useful features.
59
+ Do not add an App capability solely to fill a checklist cell. Mark unsupported
60
+ transports and missing APIs for discussion with the user.
61
+
62
+ Validate mapped commands through their real handlers and the CLI schemas. Every
63
+ write/destructive operation needs a non-mutating `--dry-run` plan and explicit
64
+ confirmation before execution, plus a readback of the actual outcome. A CLI plan
65
+ does not simulate upstream effects or replace backend authorization.
66
+
67
+ Use existing native APIs without upstream source patches, database edits, root
68
+ App processes or a second control service. Keep DSM admission, upstream API
69
+ authentication and resource ownership distinct. An idgateway browser login,
70
+ including OIDC, does not automatically grant headless API access; document the
71
+ actual upstream token/header/API-password mapping and shared-account semantics.
72
+
17
73
  ## Designing commands for AI
18
74
 
19
75
  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
76
 
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.
77
+ Wrap existing high-level backend methods 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. Map its existing download/task/search APIs with stable responses. search.stream uses NDJSON; use the fixed ndjson flow, not the JSON method adapter. Use the existing high-level torrent contract; if one is missing, record the gap for discussion rather than inventing a CLI-specific sequence.
22
78
 
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.
79
+ Map Reclaim's existing scan start/status/result APIs with their documented identifiers and states. Never automatically run cleanup.plan then cleanup.apply: the plan identity, freshness and permanent-deletion confirmation belong in the App backend; mark a cleanup-apply command destructive.
24
80
 
25
81
  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
82
 
27
83
  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
84
 
85
+ ## Long operations and AI polling
86
+
87
+ ctl must not synchronously wait for operations that may take more than about
88
+ 30 seconds, such as scans, batch processing or large downloads. Wrap the App's
89
+ existing asynchronous API and return its accepted ID immediately. Expose progress,
90
+ status, result, cancellation and listing only when that API actually provides
91
+ them. Preserve existing IDs, states,
92
+ progress/phase/ETA, pagination and single-operation behavior. Report unavailable
93
+ fields as unknown or record their absence in `ctl-plan`; never fabricate an ETA,
94
+ stop method or "return existing ID" behavior. If the API already returns an active
95
+ ID, retain it so the client can reconnect to that operation.
96
+
97
+ If the only existing API is a synchronous long call, stop that command's
98
+ implementation and report the blocker for discussion. Do not create a job layer,
99
+ new cancellation mechanism or background worker just to expose it through ctl.
100
+
101
+ Reuse the fixed `job` flow for existing compatible start/poll/result APIs. It
102
+ waits by default: AI consumers must pass `--no-wait` for long operations, then
103
+ call the discovered status/result commands by the returned ID. Native Apps
104
+ already using the SDK `background` module and `bg.jobs.*` routes can map those
105
+ existing submit/list/get/result/cancel/pause/resume capabilities. Check the
106
+ installed SDK/template contract and enabled capability first: these routes are
107
+ not automatic ctl commands, a generic ETA/paged-result service, or proof that
108
+ every App supports cancellation or singleton deduplication.
109
+
110
+ For AI consumers:
111
+
112
+ 1. Discover the real command schema and current jobs if listing is supported.
113
+ Dry-run mutations, obtain authorization, then submit once with `--yes` and
114
+ `--no-wait` when using a job flow. Keep host, App and original operation ID.
115
+ 2. Poll its existing status API using the server's retry interval; otherwise use
116
+ 2 seconds, then 5, 10 and at most 30 seconds between polls. Do not invent a
117
+ polling method if the API has none. Reconnect by the same ID after a lost
118
+ connection; never repeat start merely because a reply was lost.
119
+ 3. Report acceptance promptly, then report meaningful phase/progress changes or
120
+ about every 30–60 seconds while observing. Use a bounded observation window
121
+ (default two minutes per turn); when it expires, return the ID, latest state
122
+ and exact follow-up command. Do not wait indefinitely or call a timeout a
123
+ failed/canceled job. A completion notification is available only if the App
124
+ already supplies it.
125
+ 4. On a terminal state, fetch the existing result with bounded pagination where
126
+ supported. Preserve partial/error states; preparation is not download or save
127
+ completion. If cancellation exists and is authorized, dry-run and confirm
128
+ before calling it; read status back until the existing API proves it stopped.
129
+
29
130
  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
131
 
31
132
  ## Sensitive parameters
@@ -42,3 +42,82 @@ Containers use a fixed package identity and do not inherit the signed-in user's
42
42
  For native Apps, retain the existing daemon, identity_gateway and account.on_delete declarations. Use fixed native argv adapters and package-private paths for deletion; the gateway never writes an upstream database. Test rename, demotion, two-user visibility, stale credentials and account deletion against the actual upstream image.
43
43
 
44
44
  Bridge readiness is availability only: declared App health + loopback TCP + the current-version SDK lifecycle acquire generation. It cannot attest actual image/project/generation: another process already executing on the NAS can occupy the port. This local-code-execution threat is accepted; restore official full readback when available. Never use root, Docker socket access or borrowed admin credentials. See compose.md.
45
+
46
+
47
+ ## Optional native-client prefixes (next minor SDK)
48
+
49
+ For user-controlled mobile API or WebDAV access, declare fixed prefixes:
50
+
51
+ ```yaml
52
+ identity_gateway:
53
+ # Add to the existing complete identity_gateway declaration.
54
+ sdk: 0.18.0
55
+ optional_passthrough:
56
+ mobile:
57
+ prefixes: [/dav, /api/mobile]
58
+ ```
59
+
60
+ This unreleased capability requires the next minor SDK (0.18.0+); do not adopt
61
+ it in an App until an approved release exists. There are at most 16 entries,
62
+ each with 1–16 prefixes and no default-enabled option. Root, `/_sso`, encoded or
63
+ ambiguous prefixes and overlaps with optional/permanent passthrough are invalid.
64
+ Generated `gateway.json` carries the same declaration. Other paths still use the
65
+ App's DSM admission policy; existing permanent passthrough stays permanent.
66
+
67
+ Register the loaded SDK gateway Config's `OptionalPassthroughRoutes()` with the
68
+ App's existing authenticated API. `gateway.passthrough.status` and
69
+ `gateway.passthrough.set` are reserved administrator-only SDK methods. The latter
70
+ takes only `{"id":"mobile","enabled":true}`. Show the switch only to admins and
71
+ warn before enabling: it opens an entry without DSM sign-in, protected only by
72
+ the App's own credentials, allowing clients to read/write authorized files.
73
+ Mention the App account's required WebDAV/API permission. Keep native Basic Auth;
74
+ never inject a shared admin credential. Provide a disable switch at all times.
75
+
76
+ State defaults off and persists in private package `sso/passthrough-<id>.json`
77
+ files. The SDK writes atomically then signals the same-UID gateway with SIGHUP;
78
+ no root, port, Unix control socket or restart is needed. Disable returns 403
79
+ before the DSM gate on the next request and reload cancels active optional
80
+ requests. Any switch cancels streams across optional entries. Invalid state or a
81
+ failed signal fails closed. Control responses describe configured state and a
82
+ requested reload, not an acknowledgement. Test actual Basic Auth, streams,
83
+ closing with a valid admin session, traversal/encoded paths and WebDAV COPY/MOVE
84
+ Destination headers. Upstream App authentication and permissions remain the
85
+ App's responsibility.
86
+
87
+ ### Native OIDC browser entry (SDK 0.18.0)
88
+
89
+ An upstream with its own OIDC login can opt into a generic browser bridge:
90
+
91
+ ```yaml
92
+ identity_gateway:
93
+ sdk: 0.18.0
94
+ mode: oidc
95
+ access: dsm-admins
96
+ upstream: 8000
97
+ listen: {https: 18443}
98
+ oidc:
99
+ issuer_port: 18090
100
+ clients:
101
+ - id: native-app
102
+ redirect_paths: [/auth/callback]
103
+ browser:
104
+ start: /auth/oidc?method=login
105
+ login_paths: [/login]
106
+ callback_paths: [/auth/callback]
107
+ completion_query: token # optional, presence only for callback on a login page
108
+ ```
109
+
110
+ `start` is a fixed same-origin native SSO URI. Paths are exact, unencoded and
111
+ outside `/_sso`. The upstream still owns all credential delivery and storage.
112
+ The bridge starts native OIDC on document entry, resumes a bounded local deep
113
+ link, and watches SPA history navigation to a declared login page after logout.
114
+ It stores no upstream token/password. Storage-disabled browsers retain native
115
+ login. Failed/expired flows do not continuously retry. HTML needs a `<head>` and
116
+ CSP that permits the same-origin `/_sso/browser.js`; the SDK never weakens CSP.
117
+ Downloads and non-HTML bodies are not rewritten. The gateway verifies DSM
118
+ admission before the native callback, including an existing session for OIDC.
119
+ A cold DSM login uses the CLI's fixed App-launch transaction resume; no supplied
120
+ URL is accepted as the return destination. The transaction expires in five minutes.
121
+
122
+ This declares navigation only; upstream account provisioning/role mapping must
123
+ use that upstream's supported configuration APIs and be tested independently.
@@ -6,6 +6,18 @@ Never edit upstream README, Dockerfile, source, config, workflows or ignore file
6
6
  Do not rewrite the App or treat it as an SDK component. Use `.git/info/exclude`
7
7
  for local output when needed.
8
8
 
9
+ ## Decide whether to expose AI control
10
+
11
+ Ask whether the user needs AI control unless they have already explicitly requested it.
12
+ Leave the manifest without a `ctl` block when they have not opted in; packaging an
13
+ upstream App does not itself request ctl. If requested, follow
14
+ [the ctl coverage checklist](ctl.md#plan-command-coverage) and produce `ctl-plan`.
15
+ Cover common reads and writes through existing native APIs, including the main
16
+ UI tasks; `status` and `service status` alone are incomplete. Keep upstream code
17
+ unchanged and run adapters as the package account. Report unsupported transports
18
+ or synchronous-only long operations for discussion; do not invent App features
19
+ or bypass the current Compose ctl restriction.
20
+
9
21
  ## Container: official image and a matching source tag
10
22
 
11
23
  Use the upstream **official image**, pinned as `repository:tag@sha256:digest`.
@@ -1,5 +1,11 @@
1
1
  # 封裝上游 Compose
2
2
 
3
+ Ask whether the user needs AI control. Keep `ctl` absent unless explicitly
4
+ requested, then use [ctl-plan and the coverage checklist](ctl.md#plan-command-coverage)
5
+ to map existing upstream UI/API operations. Compose does not support automatic
6
+ ctl descriptions; report this blocker without adding a fake backend or inventing
7
+ features to work around it.
8
+
3
9
  只使用 schema-2 `dsm-package.yaml`。先讀 [compose.md](compose.md) 的宣告、支援表及 DSM 阻擋條件,再讀 [identity-gateway.md](identity-gateway.md) 決定 none/header/token/OIDC。不架 SSO Server,也不需要假 backend 或 wrapper container。
4
10
 
5
11
  ```sh