@synopackageland/cli 0.19.1 → 0.20.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 +7 -0
- package/dist/deploy/native-install-values.d.ts +3 -3
- package/dist/deploy/native-install-values.js +21 -7
- package/dist/runtime/executor-digests.json +1 -24
- package/dist/standalone/compose-errors.js +3 -3
- package/dist/standalone/compose.d.ts +6 -3
- package/dist/standalone/compose.js +6 -4
- package/dist/standalone/inspect-spk.js +12 -4
- package/dist/standalone/lifecycle.js +2 -1
- package/dist/standalone/manifest.js +4 -3
- package/package.json +2 -2
- package/skills/reference/compose.md +56 -3
- package/skills/reference/deploy.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 (2026-10-21)
|
|
4
|
+
|
|
5
|
+
- Align CLI/templates and the supported SDK minor line at 0.20; retain published UI 0.12.5.
|
|
6
|
+
- Compare upgrade approval requirements with `build/inspect --baseline`.
|
|
7
|
+
- Package native Compose shared-folder mounts with `select: install` and pass host health readiness to the daemon.
|
|
8
|
+
- Pin the 0.20.0 executor/lifecycle digest inventory and refresh canonical approval fixtures. Rebuilt executor Apps require administrator reapproval after upgrade.
|
|
9
|
+
|
|
3
10
|
## 0.19.1 (2026-10-20)
|
|
4
11
|
|
|
5
12
|
- Record SDK 0.19.1 executor and lifecycle digests for the executor fix that accepts `deadline_ms` on delegated `webapi.call`; SDK 0.19.0 digests remain. Executor Apps rebuilt with SDK 0.19.1 require one administrator reapproval after upgrading.
|
|
@@ -8,9 +8,9 @@ export type NativeInstallValuesResult = {
|
|
|
8
8
|
message: string;
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* Install wizard values for
|
|
12
|
-
* delegate asks the wizard for
|
|
13
|
-
*
|
|
11
|
+
* Install wizard values for native and Compose deployment. An install-selected
|
|
12
|
+
* delegate or install.sharedFolder asks the wizard for a selection. Reject
|
|
13
|
+
* missing first-install answers before building or uploading.
|
|
14
14
|
*/
|
|
15
15
|
export declare function resolveNativeInstallValues(manifest: StandaloneManifest, provided: Record<string, string>, state: {
|
|
16
16
|
installed: boolean;
|
|
@@ -6,20 +6,34 @@ function validShareName(name) {
|
|
|
6
6
|
!/^homes?$/i.test(name) && name.trim() === name && name.length <= 256);
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Install wizard values for
|
|
10
|
-
* delegate asks the wizard for
|
|
11
|
-
*
|
|
9
|
+
* Install wizard values for native and Compose deployment. An install-selected
|
|
10
|
+
* delegate or install.sharedFolder asks the wizard for a selection. Reject
|
|
11
|
+
* missing first-install answers before building or uploading.
|
|
12
12
|
*/
|
|
13
13
|
export function resolveNativeInstallValues(manifest, provided, state) {
|
|
14
14
|
const fail = (code, message) => ({ ok: false, code, message });
|
|
15
15
|
const selected = manifest.capabilities.delegate?.scope === "install-selected";
|
|
16
|
+
const folder = manifest.sharedFolder?.mode === "select" ? manifest.sharedFolder : undefined;
|
|
17
|
+
const key = folder?.key;
|
|
18
|
+
const changeKey = key ? `CHANGE_${key}` : undefined;
|
|
16
19
|
const names = Object.keys(provided);
|
|
17
|
-
if (!selected && names.length)
|
|
20
|
+
if (!selected && !folder && names.length)
|
|
18
21
|
return fail("DEPLOY_PARAM_UNDECLARED", `${manifest.package} has no install wizard values; remove --param ${names[0]}.`);
|
|
19
|
-
const unknown = names.find((name) => name
|
|
22
|
+
const unknown = names.find((name) => !(selected && (name === "delegate_share" || name === "delegate_home")) && name !== key && name !== changeKey);
|
|
20
23
|
if (unknown)
|
|
21
|
-
return fail("DEPLOY_PARAM_UNDECLARED", `--param ${unknown} is not an install wizard value. Use: ${USAGE}.`);
|
|
22
|
-
|
|
24
|
+
return fail("DEPLOY_PARAM_UNDECLARED", `--param ${unknown} is not an install wizard value. Use: ${folder ? `--param ${folder.key}=<shared folder>` : USAGE}.`);
|
|
25
|
+
if (key && changeKey) {
|
|
26
|
+
const name = provided[key], change = provided[changeKey];
|
|
27
|
+
if (change !== undefined && change !== "true" && change !== "false")
|
|
28
|
+
return fail("DEPLOY_PARAM_TYPE_INVALID", `--param ${changeKey} must be true or false.`);
|
|
29
|
+
if (name !== undefined && (name === "" || /[/\\"\x00-\x1f\x7f]/.test(name) || name.includes("..") || /^[.@]/.test(name)))
|
|
30
|
+
return fail("DEPLOY_PARAM_TYPE_INVALID", `--param ${key} must be one DSM Shared Folder name, never a path.`);
|
|
31
|
+
if ((!state.installed || change === "true") && name === undefined)
|
|
32
|
+
return fail("DEPLOY_PARAM_REQUIRED", `Select the native Shared Folder with --param ${key}=<shared folder>.`);
|
|
33
|
+
if (state.installed && name !== undefined && change !== "true")
|
|
34
|
+
return fail("DEPLOY_PARAM_REQUIRED", `Changing the selected folder requires --param ${changeKey}=true and --param ${key}=<shared folder>.`);
|
|
35
|
+
}
|
|
36
|
+
const share = selected ? provided.delegate_share : undefined, home = selected ? provided.delegate_home : undefined;
|
|
23
37
|
if (home !== undefined && home !== "true" && home !== "false")
|
|
24
38
|
return fail("DEPLOY_PARAM_TYPE_INVALID", "--param delegate_home must be true or false.");
|
|
25
39
|
if (share !== undefined && !validShareName(share))
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"0.19.0": {
|
|
3
|
-
"x86_64": "3b23252a4d2ad188e8920aee3c022d054a85f31797dfb9f34a576449f8b08f41",
|
|
4
|
-
"aarch64": "024a8a01977324645b475330371afb8b8c8387eac6a11ec3d9d30a4a0e445174",
|
|
5
|
-
"armv7": "fae3266f65863815f1417332a3c73d12507941029077e0ddc118da8461e8cff3",
|
|
6
|
-
"lifecycle": {
|
|
7
|
-
"x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972",
|
|
8
|
-
"aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb",
|
|
9
|
-
"armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681"
|
|
10
|
-
},
|
|
11
|
-
"goToolchain": "go1.26.9"
|
|
12
|
-
},
|
|
13
|
-
"0.19.1": {
|
|
14
|
-
"x86_64": "e27cfeb85fed221b06f9c58166f02c0f701a724308cd17c96a43a843a0b5c4aa",
|
|
15
|
-
"aarch64": "908db00cbd37d072f6ffc339e7bca24da5efb8cddd4befcbad02c7398beb7191",
|
|
16
|
-
"armv7": "a0f35d62ec80a1bf3fb49f5e2d1a5f7576eda569a6fd0695084cec6cb8e100bc",
|
|
17
|
-
"lifecycle": {
|
|
18
|
-
"x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972",
|
|
19
|
-
"aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb",
|
|
20
|
-
"armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681"
|
|
21
|
-
},
|
|
22
|
-
"goToolchain": "go1.26.9"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{ "0.20.0": { "x86_64": "e27cfeb85fed221b06f9c58166f02c0f701a724308cd17c96a43a843a0b5c4aa", "aarch64": "908db00cbd37d072f6ffc339e7bca24da5efb8cddd4befcbad02c7398beb7191", "armv7": "a0f35d62ec80a1bf3fb49f5e2d1a5f7576eda569a6fd0695084cec6cb8e100bc", "lifecycle": { "x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972", "aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb", "armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681" }, "goToolchain": "go1.26.9" } }
|
|
@@ -212,13 +212,13 @@ export const COMPOSE_ERRORS = {
|
|
|
212
212
|
},
|
|
213
213
|
COMPOSE_SHARE_INVALID: {
|
|
214
214
|
code: "COMPOSE_SHARE_INVALID",
|
|
215
|
-
description: "
|
|
216
|
-
suggestion: "
|
|
215
|
+
description: "Shared Folder selection, permission or fixed mount declaration is invalid.",
|
|
216
|
+
suggestion: "For select: install, match install.sharedFolder mode: select, key and permission; choose an existing unambiguous folder. Dynamic admin-after-install remains blocked. Never use a destination grant or host root workaround.",
|
|
217
217
|
},
|
|
218
218
|
COMPOSE_SHARE_NEEDS_APPROVAL: {
|
|
219
219
|
code: "COMPOSE_SHARE_NEEDS_APPROVAL",
|
|
220
220
|
description: "Declare capabilities.composeMounts: true; user destination grants cannot authorize package mounts.",
|
|
221
|
-
suggestion: "
|
|
221
|
+
suggestion: "Declare the package-wide composeMounts opt-in. Native install selection requires DSM package-user permission; dynamic admin-after-install remains blocked.",
|
|
222
222
|
},
|
|
223
223
|
COMPOSE_SOURCE_UNSAFE: {
|
|
224
224
|
code: "COMPOSE_SOURCE_UNSAFE",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ComposeAppData } from "../runtime/compose-data.js";
|
|
2
2
|
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
3
3
|
import { type Daemon } from "./daemon.js";
|
|
4
|
+
import type { SharedFolder } from "./shared-folder.js";
|
|
4
5
|
import type { StandaloneError, StandaloneManifest } from "./manifest.js";
|
|
5
6
|
export interface ComposePlan {
|
|
6
7
|
app_data?: ComposeAppData;
|
|
@@ -77,6 +78,7 @@ export declare function readComposeApp(value: unknown, root: string, arches: Nat
|
|
|
77
78
|
sdk?: string;
|
|
78
79
|
mode?: string;
|
|
79
80
|
composeMounts?: boolean;
|
|
81
|
+
sharedFolder?: SharedFolder;
|
|
80
82
|
resolveImages?: boolean;
|
|
81
83
|
architecture?: NativeArchFamily;
|
|
82
84
|
imageProofs?: unknown;
|
|
@@ -85,12 +87,12 @@ export declare function composeDaemon(m: Pick<StandaloneManifest, "compose" | "l
|
|
|
85
87
|
export declare function composeNetwork(m: Pick<StandaloneManifest, "compose" | "identityGateway" | "spkVersion">): {
|
|
86
88
|
public_ports: number[];
|
|
87
89
|
readiness?: {
|
|
88
|
-
version: string;
|
|
89
|
-
plan_sha256: string;
|
|
90
90
|
health: {
|
|
91
91
|
path: string;
|
|
92
92
|
status: number;
|
|
93
93
|
} | undefined;
|
|
94
|
+
version?: string | undefined;
|
|
95
|
+
plan_sha256?: string | undefined;
|
|
94
96
|
} | undefined;
|
|
95
97
|
ingress?: {
|
|
96
98
|
candidate: number;
|
|
@@ -101,7 +103,7 @@ export declare function composeNetwork(m: Pick<StandaloneManifest, "compose" | "
|
|
|
101
103
|
export declare function composeSource(c: ComposeApp): string;
|
|
102
104
|
/** Retain upstream bytes, the true override and a lock without secret values. */
|
|
103
105
|
export declare function writeComposePayload(c: ComposeApp, root: string, target: string, gateway?: unknown): void;
|
|
104
|
-
/**
|
|
106
|
+
/** Materialize fixed package identity and native wizard share names in the incoming tree. */
|
|
105
107
|
export declare function renderComposeInstall(pkg: string): string;
|
|
106
108
|
/** The same SDK entry point supplies the package-account install materializer. */
|
|
107
109
|
export declare function buildComposeControl(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: import("./build.js").CommandRunner): Promise<string | undefined>;
|
|
@@ -118,3 +120,4 @@ export declare function dsmComposeProblem(c: ComposeApp): {
|
|
|
118
120
|
message: string;
|
|
119
121
|
} | undefined;
|
|
120
122
|
export declare const COMPOSE_READINESS_LIMITATION = "Availability only: App health + fixed endpoint TCP + package lifecycle acquire generation matching the installed version. This does not attest the actual Docker image, project or generation. Another process already executing locally on the NAS may impersonate the fixed endpoint. Accepted for this local-code-execution threat model; replace with full official readback when available.";
|
|
123
|
+
export declare const COMPOSE_HOST_READINESS_LIMITATION = "Host availability only: package UID and loopback listener bindings, plus declared App HTTP health when configured. This does not attest the actual Docker image, project or generation.";
|
|
@@ -93,6 +93,7 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
|
93
93
|
package: context.package ?? "ComposeApp",
|
|
94
94
|
identity_mode: context.mode ?? "none",
|
|
95
95
|
compose_mounts: context.composeMounts === true,
|
|
96
|
+
...(context.sharedFolder?.mode === "select" ? { install_shared_folder: context.sharedFolder } : {}),
|
|
96
97
|
resolve_images: context.resolveImages === true,
|
|
97
98
|
registry_mirrors: context.resolveImages &&
|
|
98
99
|
process.env.SYNOPKGLAND_COMPOSE_REGISTRY_MIRRORS
|
|
@@ -171,11 +172,10 @@ export function composeNetwork(m) {
|
|
|
171
172
|
...(c.plan.ingress
|
|
172
173
|
? { ingress: { candidate: c.plan.ingress.candidate } }
|
|
173
174
|
: {}),
|
|
174
|
-
...(!host
|
|
175
|
+
...(c.web && (!host || c.web.health) && !c.plan.mounts_blocked
|
|
175
176
|
? {
|
|
176
177
|
readiness: {
|
|
177
|
-
version: m.spkVersion,
|
|
178
|
-
plan_sha256: c.plan.sha256,
|
|
178
|
+
...(!host ? { version: m.spkVersion, plan_sha256: c.plan.sha256 } : {}),
|
|
179
179
|
health: c.web.health,
|
|
180
180
|
},
|
|
181
181
|
}
|
|
@@ -207,7 +207,7 @@ export function writeComposePayload(c, root, target, gateway) {
|
|
|
207
207
|
}))
|
|
208
208
|
writeFileSync(join(target, name), content, { mode: 0o600 });
|
|
209
209
|
}
|
|
210
|
-
/**
|
|
210
|
+
/** Materialize fixed package identity and native wizard share names in the incoming tree. */
|
|
211
211
|
export function renderComposeInstall(pkg) {
|
|
212
212
|
return `#!/bin/sh
|
|
213
213
|
set -eu
|
|
@@ -268,6 +268,7 @@ export function lockComposeImages(m, arch, resolveImages, imageProofs) {
|
|
|
268
268
|
sdk: m.compose.sdk,
|
|
269
269
|
mode: m.identityGateway?.mode ?? "none",
|
|
270
270
|
composeMounts: m.capabilities.composeMounts,
|
|
271
|
+
sharedFolder: m.sharedFolder,
|
|
271
272
|
resolveImages,
|
|
272
273
|
architecture: arch,
|
|
273
274
|
imageProofs,
|
|
@@ -346,3 +347,4 @@ export function dsmComposeProblem(c) {
|
|
|
346
347
|
};
|
|
347
348
|
}
|
|
348
349
|
export const COMPOSE_READINESS_LIMITATION = "Availability only: App health + fixed endpoint TCP + package lifecycle acquire generation matching the installed version. This does not attest the actual Docker image, project or generation. Another process already executing locally on the NAS may impersonate the fixed endpoint. Accepted for this local-code-execution threat model; replace with full official readback when available.";
|
|
350
|
+
export const COMPOSE_HOST_READINESS_LIMITATION = "Host availability only: package UID and loopback listener bindings, plus declared App HTTP health when configured. This does not attest the actual Docker image, project or generation.";
|
|
@@ -6,7 +6,7 @@ import { inspectApiDescription } from "./api-description-inspect.js";
|
|
|
6
6
|
import { dsmLauncherType } from "../contract/app-launch.js";
|
|
7
7
|
import { isWebLaunchMode, resolvedWebLaunch, trustedAppEntryPath, usesAuthGateway, } from "../contract/web-launch.js";
|
|
8
8
|
import { loadContract } from "../contract/load.js";
|
|
9
|
-
import { readComposeApp, composeSource, dsmComposeProblem, COMPOSE_READINESS_LIMITATION, } from "./compose.js";
|
|
9
|
+
import { readComposeApp, composeSource, dsmComposeProblem, COMPOSE_READINESS_LIMITATION, COMPOSE_HOST_READINESS_LIMITATION, } from "./compose.js";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { inspectNativeLayout } from "./native-layout.js";
|
|
12
12
|
import { checkBridgeApp, checkBridgeExecutor } from "./inspect-bridge.js";
|
|
@@ -575,6 +575,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
575
575
|
requiresAdminApproval: true,
|
|
576
576
|
})),
|
|
577
577
|
};
|
|
578
|
+
const appScripts = unwrapAppScripts(join(dir, "scripts"), findings);
|
|
578
579
|
const root = join(dir, "unified-compose-inspection");
|
|
579
580
|
mkdirSync(root);
|
|
580
581
|
await tar.x({ file: join(dir, "package.tgz"), cwd: root });
|
|
@@ -596,6 +597,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
596
597
|
sdk: saved.sdk,
|
|
597
598
|
mode: cfg?.mode ?? "none",
|
|
598
599
|
composeMounts: metadata.capabilities?.includes("composeMounts"),
|
|
600
|
+
sharedFolder: recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))),
|
|
599
601
|
architecture: arch,
|
|
600
602
|
imageProofs: JSON.parse(readFileSync(join(root, "compose/images.json"), "utf8")),
|
|
601
603
|
});
|
|
@@ -641,6 +643,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
641
643
|
: {}),
|
|
642
644
|
},
|
|
643
645
|
compose: c,
|
|
646
|
+
sharedFolder: recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))),
|
|
644
647
|
...(g ? { identityGateway: g } : {}),
|
|
645
648
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
646
649
|
};
|
|
@@ -650,6 +653,11 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
650
653
|
throw new Error("gateway configuration differs from the declared package contract");
|
|
651
654
|
if (readOptional(join(dir, "conf/privilege")) !== renderNativePrivilege())
|
|
652
655
|
throw new Error("Compose hooks must run as the package account");
|
|
656
|
+
if (manifest.sharedFolder) {
|
|
657
|
+
for (const [name, text] of Object.entries(expectedWizardFiles(info.package, manifest.sharedFolder)))
|
|
658
|
+
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
659
|
+
throw new Error(`unexpected WIZARD_UIFILES/${name}; native share selection must match the declaration`);
|
|
660
|
+
}
|
|
653
661
|
const resource = renderStandaloneResource(manifest);
|
|
654
662
|
if (readOptional(join(dir, "conf/resource")) !== resource)
|
|
655
663
|
throw new Error("unexpected worker resource; unapproved Shares must never auto-acquire");
|
|
@@ -671,7 +679,6 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
671
679
|
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
672
680
|
throw new Error(`unexpected WIZARD_UIFILES/${name}; retained-volume guidance must match the plan`);
|
|
673
681
|
}
|
|
674
|
-
const appScripts = unwrapAppScripts(join(dir, "scripts"), findings);
|
|
675
682
|
manifest.appScripts = undefined;
|
|
676
683
|
for (const hook of [
|
|
677
684
|
"preinst",
|
|
@@ -737,9 +744,10 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
737
744
|
? "none"
|
|
738
745
|
: c.plan.services.find((s) => s.service === c.web?.service)
|
|
739
746
|
?.network === "host"
|
|
740
|
-
? "host-uid-listener"
|
|
747
|
+
? c.web.health ? "host-uid-listener-health" : "host-uid-listener"
|
|
741
748
|
: "health-fixed-ip-lifecycle",
|
|
742
|
-
limitation:
|
|
749
|
+
limitation: c.plan.services.find((s) => s.service === c.web?.service)?.network === "host"
|
|
750
|
+
? COMPOSE_HOST_READINESS_LIMITATION : COMPOSE_READINESS_LIMITATION,
|
|
743
751
|
},
|
|
744
752
|
};
|
|
745
753
|
}
|
|
@@ -33,7 +33,8 @@ function renderStandaloneScriptInternal(name, manifest) {
|
|
|
33
33
|
const control = `"/var/packages/${manifest.package}/target/daemon/control"`;
|
|
34
34
|
const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}"`;
|
|
35
35
|
const preflight = name === "start-stop-status"
|
|
36
|
-
? `case "$1" in prestart|start|restart)\n "\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/compose/control" preflight || exit $? ;;\nesac\n`
|
|
36
|
+
? `case "$1" in prestart|start|restart)\n "\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/compose/control" preflight || exit $? ;;\nesac\n` +
|
|
37
|
+
(!manifest.identityGateway ? `case "$1" in prestart|prestop) exit 0 ;; esac\n` : "")
|
|
37
38
|
: "";
|
|
38
39
|
const remember = name === "postinst" &&
|
|
39
40
|
manifest.compose.plan.ingress &&
|
|
@@ -240,12 +240,16 @@ function readManifest(raw, c) {
|
|
|
240
240
|
c.fail("CTL_UNSUPPORTED_BACKEND", "Compose does not support automatic ctl descriptions; disable ctl.");
|
|
241
241
|
if (isRecord(raw.compose) && isRecord(raw.compose.web) && !isRecord(raw.compose.web.health))
|
|
242
242
|
c.fail("COMPOSE_HEALTH_REQUIRED", "Select compose.web.health.path and expected 2xx status for the actual App. Readiness does not follow redirects; check is static and does not probe HTTP.");
|
|
243
|
+
const sharedFolder = raw.install === undefined
|
|
244
|
+
? undefined
|
|
245
|
+
: readSharedFolder(raw.install, (e) => c.fail(e.code, e.message));
|
|
243
246
|
const compose = arches &&
|
|
244
247
|
readComposeApp(raw.compose, c.root, arches, (e) => c.fail(e.code, e.message), {
|
|
245
248
|
package: packageId,
|
|
246
249
|
sdk: typeof gatewayRaw.sdk === "string" ? gatewayRaw.sdk : undefined,
|
|
247
250
|
mode: typeof gatewayRaw.mode === "string" ? gatewayRaw.mode : "none",
|
|
248
251
|
composeMounts: capabilities.composeMounts,
|
|
252
|
+
sharedFolder,
|
|
249
253
|
});
|
|
250
254
|
if (raw.compose !== undefined &&
|
|
251
255
|
((raw.daemon !== undefined && !timeoutOnly) ||
|
|
@@ -306,9 +310,6 @@ function readManifest(raw, c) {
|
|
|
306
310
|
if (permissions)
|
|
307
311
|
checkBridgeMethods(backend, permissions, c);
|
|
308
312
|
}
|
|
309
|
-
const sharedFolder = raw.install === undefined
|
|
310
|
-
? undefined
|
|
311
|
-
: readSharedFolder(raw.install, (e) => c.fail(e.code, e.message));
|
|
312
313
|
const entry = raw.entry === undefined
|
|
313
314
|
? undefined
|
|
314
315
|
: raw.compose !== undefined &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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.
|
|
30
|
+
"@synopackageland/templates": "0.20.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
|
@@ -98,7 +98,7 @@ a build operator trust decision. `check`/`inspect` remain offline.
|
|
|
98
98
|
| 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 |
|
|
99
99
|
| Image VOLUME | Build inventories verified image config and assigns a stable named volume |
|
|
100
100
|
| Existing external volume | Rejected without trusted admin ownership/driver readback |
|
|
101
|
-
| Existing Shared Folder |
|
|
101
|
+
| Existing Shared Folder | Native install selection plus DSM package-user ACL; dynamic admin-after-install authority stays blocked |
|
|
102
102
|
| 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 |
|
|
103
103
|
| Host | Explicit verified OIDC web consumer with address_env forced to 127.0.0.1; no container-root; internet access is visible |
|
|
104
104
|
| Egress | isolated internal networks, or explicit internet outbound bridge; fine allowlist declarations fail |
|
|
@@ -122,7 +122,48 @@ modification and deletion. DSM identity/account bridge and file authority are
|
|
|
122
122
|
separate features. Per-DSM-user filesystem access requires a File Station session
|
|
123
123
|
or Broker Adapter; it is outside this change and cannot be obtained through a mount.
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
For one existing Shared Folder, reuse the native install/upgrade wizard:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
capabilities: {composeMounts: true}
|
|
129
|
+
install:
|
|
130
|
+
sharedFolder: {mode: select, key: media, permission: ro}
|
|
131
|
+
compose:
|
|
132
|
+
shares:
|
|
133
|
+
media:
|
|
134
|
+
select: install
|
|
135
|
+
# Omitted permission defaults to ro. For rw, set rw in both declarations.
|
|
136
|
+
mounts: [{service: web, target: /media}]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The share key and permission must match `install.sharedFolder` exactly. This
|
|
140
|
+
route supports `mode: select`; `create` resource ordering is not supported.
|
|
141
|
+
The package-account preinst resolves the chosen name on `/volumeN` and writes
|
|
142
|
+
only the fixed source placeholder into the worker's private `.env`. Missing,
|
|
143
|
+
ambiguous or symlinked sources fail installation. Host paths are never wizard
|
|
144
|
+
answers. Every mounted service uses the fixed package UID:GID, drops all Linux
|
|
145
|
+
capabilities and has no-new-privileges. Container-root cannot mount Shares.
|
|
146
|
+
|
|
147
|
+
**Selecting an existing share does not grant permission.** An administrator must
|
|
148
|
+
grant the package account access in **Control Panel > Shared Folder > Edit >
|
|
149
|
+
Permissions > System internal user**. Until then, container file operations fail
|
|
150
|
+
under the kernel's ACL checks. No root helper, ACL writer or Docker socket is
|
|
151
|
+
given to the App. Grant directly to the package account; supplementary DSM groups
|
|
152
|
+
are not added to the container. See the SDK Shared Folder permission guide.
|
|
153
|
+
|
|
154
|
+
The upgrade wizard keeps the saved name unless its change checkbox is selected.
|
|
155
|
+
CLI deployment uses `--param media=Media` on first install, and
|
|
156
|
+
`--param CHANGE_media=true --param media=OtherMedia` to change it on upgrade.
|
|
157
|
+
Stop/disable and uninstall use DSM's official project worker; source data and
|
|
158
|
+
administrator-managed ACL entries are retained. A moved share needs a new
|
|
159
|
+
upgrade to resolve its volume path; start does not rematerialize the bind.
|
|
160
|
+
|
|
161
|
+
ACL changes affect subsequent filesystem operations. Already-open file
|
|
162
|
+
descriptors and data already read are not recalled. To stop existing streams,
|
|
163
|
+
stop the package before removing its access. This native route does not promise
|
|
164
|
+
automatic container termination or recreation on ACL changes.
|
|
165
|
+
|
|
166
|
+
For dynamic selection after install, declare the separate, blocked authority:
|
|
126
167
|
|
|
127
168
|
```yaml
|
|
128
169
|
compose:
|
|
@@ -140,13 +181,25 @@ upstream docker-project is acquired. The independent authority purpose is
|
|
|
140
181
|
`compose-mount`; a user destination grant from Download Hub/delegate never
|
|
141
182
|
permits a package-wide mount.
|
|
142
183
|
|
|
143
|
-
The
|
|
184
|
+
The dynamic implementation does **not** issue a mount grant, invoke ACL tools,
|
|
144
185
|
install a protected executor for Compose, or activate/revoke an upstream project.
|
|
145
186
|
Confirmation returns `COMPOSE_PROJECT_CONTROL_UNAVAILABLE`. It must remain
|
|
146
187
|
unsupported until an official ACL plus fixed project-control adapter can use
|
|
147
188
|
protected policy/digest/generation authority, stop/recreate/read back and revoke
|
|
148
189
|
without reviving stale grants. No app runs as host root to bypass this boundary.
|
|
149
190
|
|
|
191
|
+
| Acceptance blocker | Native install selection |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `DSM_DOCKER_PROJECT_MODEL` | Uses the existing official docker-project worker; device acceptance still applies. |
|
|
194
|
+
| `DSM_STORAGE_OWNERSHIP_TIMING` | Existing shares need no package-created directory or chown; private package binds remain unsupported. |
|
|
195
|
+
| `DSM_EGRESS_BOUNDARY` | Unchanged independent network acceptance condition. |
|
|
196
|
+
| `COMPOSE_PROJECT_CONTROL_UNAVAILABLE` | Bypassed through native install/upgrade/stop/uninstall, without a dynamic controller. |
|
|
197
|
+
| `DSM_MOUNT_ACL_REVOKE` | Native kernel ACL semantics above; immediate stream cancellation remains outside this route. |
|
|
198
|
+
|
|
199
|
+
The first three general device acceptance labels remain in the plan. Dynamic
|
|
200
|
+
`admin-after-install` and `install-draft` keep all five blockers and
|
|
201
|
+
`mounts_blocked: true`; mixing dynamic authority with native selection is refused.
|
|
202
|
+
|
|
150
203
|
## Gateway transport
|
|
151
204
|
|
|
152
205
|
`none` keeps native app login; `header`, `token` and embedded `oidc` use supported
|
|
@@ -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. Compose uses schema-2 differences and fixed package identity.
|
|
8
|
+
Deployment uploads directly to DSM. It does not publish artifacts. Compose uses schema-2 differences and fixed package identity. For `compose.shares` with `select: install`, use matching `install.sharedFolder: {mode: select, key: media, permission: ro}` and pass `--param media=Media`. Upgrade keeps the selection unless `--param CHANGE_media=true --param media=OtherMedia` is provided. An administrator grants the fixed package account access through DSM; selection is not a grant. Dynamic admin-after-install protected ACL/project control remains blocked. A user destination grant never 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
|
|