@synopackageland/cli 0.19.1 → 0.21.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 +12 -0
- package/README.md +17 -0
- package/dist/cli.js +5 -1
- 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/runtime/sdk-source.d.ts +2 -2
- package/dist/runtime/sdk-source.js +5 -3
- package/dist/standalone/compose-errors.js +8 -3
- package/dist/standalone/compose.d.ts +16 -4
- package/dist/standalone/compose.js +9 -5
- package/dist/standalone/inspect-spk.d.ts +1 -0
- package/dist/standalone/inspect-spk.js +16 -5
- package/dist/standalone/lifecycle.js +2 -1
- package/dist/standalone/manifest.js +4 -3
- package/dist/standalone/resource.js +1 -0
- 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,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 (2026-10-21)
|
|
4
|
+
|
|
5
|
+
- Validate and preserve `compose.devices.gpu` in check, build and inspect for optional Intel render-node access and software fallback.
|
|
6
|
+
- Pin SDK/templates 0.21.0 and reuse immutable executor/v0.20.0 with verified unchanged executor/lifecycle digests.
|
|
7
|
+
|
|
8
|
+
## 0.20.0 (2026-10-21)
|
|
9
|
+
|
|
10
|
+
- Align CLI/templates and the supported SDK minor line at 0.20; retain published UI 0.12.5.
|
|
11
|
+
- Compare upgrade approval requirements with `build/inspect --baseline`.
|
|
12
|
+
- Package native Compose shared-folder mounts with `select: install` and pass host health readiness to the daemon.
|
|
13
|
+
- Pin the 0.20.0 executor/lifecycle digest inventory and refresh canonical approval fixtures. Rebuilt executor Apps require administrator reapproval after upgrade.
|
|
14
|
+
|
|
3
15
|
## 0.19.1 (2026-10-20)
|
|
4
16
|
|
|
5
17
|
- 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.
|
package/README.md
CHANGED
|
@@ -367,3 +367,20 @@ unencrypted-HTTP limitations before opening it. Native resource/startup failures
|
|
|
367
367
|
fail closed; they do not silently expose a direct listener. The native runtime
|
|
368
368
|
requires the corresponding SDK release; this CLI change does not publish that
|
|
369
369
|
release or change existing manifest version pins.
|
|
370
|
+
|
|
371
|
+
Compose Apps may opt in to Intel render devices with
|
|
372
|
+
`compose.devices.gpu.services: [media]`. The matching SDK discovers exact devices
|
|
373
|
+
and the native `videodriver` GID at installation; the CLI emits DSM's
|
|
374
|
+
`video-driver` resource. `check` rejects arbitrary paths/GIDs and elevated GPU
|
|
375
|
+
services. `inspect --json` includes `compose.devices.gpu` with the runtime path
|
|
376
|
+
policy, GID source and software fallback, plus outstanding DSM acceptance
|
|
377
|
+
blockers. Keep the package UID and let the App validate its upstream encoder;
|
|
378
|
+
absence of a GPU produces no device mapping. See the matching SDK's Compose GPU
|
|
379
|
+
documentation for the security boundary and hardware validation limits.
|
|
380
|
+
|
|
381
|
+
DSM's native provider adds NET_BIND_SERVICE to the bounding set for Compose
|
|
382
|
+
packages. The SDK still emits `cap_drop: [ALL]` and requests no `cap_add`; the
|
|
383
|
+
nonroot package process with no-new-privileges has zero effective, permitted and
|
|
384
|
+
ambient capabilities on the tested provider. This general platform behavior is
|
|
385
|
+
not a GPU blocker. Plan metadata describes SDK policy, not an attestation of the
|
|
386
|
+
provider's final container configuration; see the SDK Compose security boundary.
|
package/dist/cli.js
CHANGED
|
@@ -344,6 +344,7 @@ export async function runCli(argv) {
|
|
|
344
344
|
...("compose" in loaded.manifest && loaded.manifest.compose
|
|
345
345
|
? {
|
|
346
346
|
compose: {
|
|
347
|
+
...(loaded.manifest.compose.plan.devices ? { devices: loaded.manifest.compose.plan.devices } : {}),
|
|
347
348
|
images: loaded.manifest.compose.plan.images,
|
|
348
349
|
services: loaded.manifest.compose.plan.services,
|
|
349
350
|
mountsBlocked: loaded.manifest.compose.plan.mounts_blocked,
|
|
@@ -688,15 +689,18 @@ export async function runCli(argv) {
|
|
|
688
689
|
outDir: undefined,
|
|
689
690
|
}),
|
|
690
691
|
});
|
|
692
|
+
const builtManifest = standalone && !isNativeLayout(standalone) ? loadStandaloneManifest(standalone) : undefined;
|
|
693
|
+
const devices = builtManifest && builtManifest.ok ? builtManifest.manifest.compose?.plan.devices : undefined;
|
|
691
694
|
const approval = await Promise.all(outputs.map(async (output) => ({ output, ...await spkApprovalDiff(output, options.baseline && resolve(options.baseline)) })));
|
|
692
695
|
const lines = [
|
|
693
696
|
...approval.map(item => item.output + "\n" + formatApprovalDiff(item)),
|
|
694
697
|
...outputs.map((output) => `Built ${output}`),
|
|
698
|
+
...(devices?.gpu ? [`GPU: ${devices.gpu.paths}; GID source: ${devices.gpu.gid_source}; fallback: ${devices.gpu.fallback}`] : []),
|
|
695
699
|
...(reproducible ? reproducibleLines(reproducible) : []),
|
|
696
700
|
];
|
|
697
701
|
if (reproducible && !reproducible.identical)
|
|
698
702
|
throw Object.assign(failure("BUILD_NOT_REPRODUCIBLE", lines.join("\n"), "Find the payload files that differ between builds (timestamps, random paths, unpinned dependencies) and retry after fixing them."), { report: { outputs, reproducible } });
|
|
699
|
-
emit("build", { outputs, approval, ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
|
|
703
|
+
emit("build", { outputs, approval, ...(devices ? { compose: { devices } } : {}), ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
|
|
700
704
|
});
|
|
701
705
|
if (json)
|
|
702
706
|
console.log = (...args) => console.error(...args);
|
|
@@ -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.21.0": { "x86_64": "e27cfeb85fed221b06f9c58166f02c0f701a724308cd17c96a43a843a0b5c4aa", "aarch64": "908db00cbd37d072f6ffc339e7bca24da5efb8cddd4befcbad02c7398beb7191", "armv7": "a0f35d62ec80a1bf3fb49f5e2d1a5f7576eda569a6fd0695084cec6cb8e100bc", "lifecycle": { "x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972", "aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb", "armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681" }, "goToolchain": "go1.26.9" } }
|
|
@@ -36,8 +36,8 @@ export declare function sdkRequirement(backendDir: string): string | undefined;
|
|
|
36
36
|
export declare function goModulePath(backendDir: string): string | undefined;
|
|
37
37
|
/**
|
|
38
38
|
* The module directories of one SDK version. SYNOPKGLAND_SDK_DIR names a
|
|
39
|
-
* local sdk checkout (development and gates only); otherwise
|
|
40
|
-
*
|
|
39
|
+
* local sdk checkout (development and gates only); otherwise modules come from
|
|
40
|
+
* `go mod download`; unchanged executor releases are pinned above.
|
|
41
41
|
*/
|
|
42
42
|
export declare function sdkSource(version: string, env?: NodeJS.ProcessEnv, download?: (spec: string) => string): SdkSource;
|
|
43
43
|
/** The module cache directory of module@version; the cache is read-only, so builds copy it first. */
|
|
@@ -75,16 +75,18 @@ function goMod(backendDir) {
|
|
|
75
75
|
return '';
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
/** SDK releases that reuse an immutable executor release with identical payload bytes. */
|
|
79
|
+
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0' };
|
|
78
80
|
/**
|
|
79
81
|
* The module directories of one SDK version. SYNOPKGLAND_SDK_DIR names a
|
|
80
|
-
* local sdk checkout (development and gates only); otherwise
|
|
81
|
-
*
|
|
82
|
+
* local sdk checkout (development and gates only); otherwise modules come from
|
|
83
|
+
* `go mod download`; unchanged executor releases are pinned above.
|
|
82
84
|
*/
|
|
83
85
|
export function sdkSource(version, env = process.env, download = (spec) => goModDownload(spec, env)) {
|
|
84
86
|
const dir = env.SYNOPKGLAND_SDK_DIR;
|
|
85
87
|
if (dir)
|
|
86
88
|
return { version, goDir: dir, executorDir: join(dir, 'executor'), local: true };
|
|
87
|
-
return { version, goDir: download(`${SDK_MODULE}@v${version}`), executorDir: download(`${EXECUTOR_MODULE}@v${version}`), local: false };
|
|
89
|
+
return { version, goDir: download(`${SDK_MODULE}@v${version}`), executorDir: download(`${EXECUTOR_MODULE}@v${EXECUTOR_RELEASES[version] ?? version}`), local: false };
|
|
88
90
|
}
|
|
89
91
|
const downloaded = new Map();
|
|
90
92
|
/** The module cache directory of module@version; the cache is read-only, so builds copy it first. */
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** SDK planner errors accepted at this versioned tool boundary. */
|
|
2
2
|
export const COMPOSE_ERRORS = {
|
|
3
|
+
COMPOSE_GPU_INVALID: {
|
|
4
|
+
code: "COMPOSE_GPU_INVALID",
|
|
5
|
+
description: "The GPU declaration requires bounded package-UID services.",
|
|
6
|
+
suggestion: "Declare compose.devices.gpu.services with distinct active service names. Keep package UID, no added capabilities, and let the SDK discover Intel render devices and the native videodriver GID.",
|
|
7
|
+
},
|
|
3
8
|
COMPOSE_STARTUP_INVALID: {
|
|
4
9
|
code: "COMPOSE_STARTUP_INVALID",
|
|
5
10
|
description: "The container startup budget is invalid.",
|
|
@@ -212,13 +217,13 @@ export const COMPOSE_ERRORS = {
|
|
|
212
217
|
},
|
|
213
218
|
COMPOSE_SHARE_INVALID: {
|
|
214
219
|
code: "COMPOSE_SHARE_INVALID",
|
|
215
|
-
description: "
|
|
216
|
-
suggestion: "
|
|
220
|
+
description: "Shared Folder selection, permission or fixed mount declaration is invalid.",
|
|
221
|
+
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
222
|
},
|
|
218
223
|
COMPOSE_SHARE_NEEDS_APPROVAL: {
|
|
219
224
|
code: "COMPOSE_SHARE_NEEDS_APPROVAL",
|
|
220
225
|
description: "Declare capabilities.composeMounts: true; user destination grants cannot authorize package mounts.",
|
|
221
|
-
suggestion: "
|
|
226
|
+
suggestion: "Declare the package-wide composeMounts opt-in. Native install selection requires DSM package-user permission; dynamic admin-after-install remains blocked.",
|
|
222
227
|
},
|
|
223
228
|
COMPOSE_SOURCE_UNSAFE: {
|
|
224
229
|
code: "COMPOSE_SOURCE_UNSAFE",
|
|
@@ -1,8 +1,18 @@
|
|
|
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 {
|
|
7
|
+
devices?: {
|
|
8
|
+
gpu?: {
|
|
9
|
+
services: string[];
|
|
10
|
+
resource: string;
|
|
11
|
+
gid_source: string;
|
|
12
|
+
paths: string;
|
|
13
|
+
fallback: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
6
16
|
app_data?: ComposeAppData;
|
|
7
17
|
startup_timeout_seconds?: number;
|
|
8
18
|
ingress?: {
|
|
@@ -71,12 +81,13 @@ export interface ComposeApp {
|
|
|
71
81
|
document: Record<string, any>;
|
|
72
82
|
plan: ComposePlan;
|
|
73
83
|
}
|
|
74
|
-
export declare const COMPOSE_FIELDS: readonly ["startup_timeout_seconds", "resource_prefix", "files", "env_files", "profiles", "web", "data", "shares", "network", "services", "helpers", "sdk"];
|
|
84
|
+
export declare const COMPOSE_FIELDS: readonly ["devices", "startup_timeout_seconds", "resource_prefix", "files", "env_files", "profiles", "web", "data", "shares", "network", "services", "helpers", "sdk"];
|
|
75
85
|
export declare function readComposeApp(value: unknown, root: string, arches: NativeArchFamily[], fail: (e: StandaloneError) => void, context?: {
|
|
76
86
|
package?: string;
|
|
77
87
|
sdk?: string;
|
|
78
88
|
mode?: string;
|
|
79
89
|
composeMounts?: boolean;
|
|
90
|
+
sharedFolder?: SharedFolder;
|
|
80
91
|
resolveImages?: boolean;
|
|
81
92
|
architecture?: NativeArchFamily;
|
|
82
93
|
imageProofs?: unknown;
|
|
@@ -85,12 +96,12 @@ export declare function composeDaemon(m: Pick<StandaloneManifest, "compose" | "l
|
|
|
85
96
|
export declare function composeNetwork(m: Pick<StandaloneManifest, "compose" | "identityGateway" | "spkVersion">): {
|
|
86
97
|
public_ports: number[];
|
|
87
98
|
readiness?: {
|
|
88
|
-
version: string;
|
|
89
|
-
plan_sha256: string;
|
|
90
99
|
health: {
|
|
91
100
|
path: string;
|
|
92
101
|
status: number;
|
|
93
102
|
} | undefined;
|
|
103
|
+
version?: string | undefined;
|
|
104
|
+
plan_sha256?: string | undefined;
|
|
94
105
|
} | undefined;
|
|
95
106
|
ingress?: {
|
|
96
107
|
candidate: number;
|
|
@@ -101,7 +112,7 @@ export declare function composeNetwork(m: Pick<StandaloneManifest, "compose" | "
|
|
|
101
112
|
export declare function composeSource(c: ComposeApp): string;
|
|
102
113
|
/** Retain upstream bytes, the true override and a lock without secret values. */
|
|
103
114
|
export declare function writeComposePayload(c: ComposeApp, root: string, target: string, gateway?: unknown): void;
|
|
104
|
-
/**
|
|
115
|
+
/** Materialize fixed package identity and native wizard share names in the incoming tree. */
|
|
105
116
|
export declare function renderComposeInstall(pkg: string): string;
|
|
106
117
|
/** The same SDK entry point supplies the package-account install materializer. */
|
|
107
118
|
export declare function buildComposeControl(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: import("./build.js").CommandRunner): Promise<string | undefined>;
|
|
@@ -118,3 +129,4 @@ export declare function dsmComposeProblem(c: ComposeApp): {
|
|
|
118
129
|
message: string;
|
|
119
130
|
} | undefined;
|
|
120
131
|
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.";
|
|
132
|
+
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.";
|
|
@@ -12,6 +12,7 @@ import { goChildEnv } from "../runtime/go-env.js";
|
|
|
12
12
|
import { sdkSource } from "../runtime/sdk-source.js";
|
|
13
13
|
import { readDaemon } from "./daemon.js";
|
|
14
14
|
export const COMPOSE_FIELDS = [
|
|
15
|
+
"devices",
|
|
15
16
|
"startup_timeout_seconds",
|
|
16
17
|
"resource_prefix",
|
|
17
18
|
"files",
|
|
@@ -68,7 +69,7 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
|
68
69
|
if (!record(value) ||
|
|
69
70
|
Object.keys(value).some((k) => !COMPOSE_FIELDS.includes(k)) ||
|
|
70
71
|
!Array.isArray(value.files))
|
|
71
|
-
return invalid("compose requires files, with optional env_files, profiles, web, data, shares, network, services, helpers and sdk; old file/targets syntax is removed.");
|
|
72
|
+
return invalid("compose requires files, with optional env_files, profiles, web, data, shares, devices, network, services, helpers and sdk; old file/targets syntax is removed.");
|
|
72
73
|
if (value.sdk !== undefined &&
|
|
73
74
|
(typeof value.sdk !== "string" || !/^\d+\.\d+\.\d+$/.test(value.sdk)))
|
|
74
75
|
return invalid("compose.sdk must name the matching SDK release.", "SDK_COMPOSE_UNSUPPORTED");
|
|
@@ -93,6 +94,7 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
|
93
94
|
package: context.package ?? "ComposeApp",
|
|
94
95
|
identity_mode: context.mode ?? "none",
|
|
95
96
|
compose_mounts: context.composeMounts === true,
|
|
97
|
+
...(context.sharedFolder?.mode === "select" ? { install_shared_folder: context.sharedFolder } : {}),
|
|
96
98
|
resolve_images: context.resolveImages === true,
|
|
97
99
|
registry_mirrors: context.resolveImages &&
|
|
98
100
|
process.env.SYNOPKGLAND_COMPOSE_REGISTRY_MIRRORS
|
|
@@ -171,11 +173,10 @@ export function composeNetwork(m) {
|
|
|
171
173
|
...(c.plan.ingress
|
|
172
174
|
? { ingress: { candidate: c.plan.ingress.candidate } }
|
|
173
175
|
: {}),
|
|
174
|
-
...(!host
|
|
176
|
+
...(c.web && (!host || c.web.health) && !c.plan.mounts_blocked
|
|
175
177
|
? {
|
|
176
178
|
readiness: {
|
|
177
|
-
version: m.spkVersion,
|
|
178
|
-
plan_sha256: c.plan.sha256,
|
|
179
|
+
...(!host ? { version: m.spkVersion, plan_sha256: c.plan.sha256 } : {}),
|
|
179
180
|
health: c.web.health,
|
|
180
181
|
},
|
|
181
182
|
}
|
|
@@ -199,6 +200,7 @@ export function writeComposePayload(c, root, target, gateway) {
|
|
|
199
200
|
mkdirSync(target, { recursive: true });
|
|
200
201
|
for (const [name, content] of Object.entries({
|
|
201
202
|
"docker-compose.yml": c.plan.effective,
|
|
203
|
+
...(c.plan.devices?.gpu ? { "gpu.template.yaml": c.plan.effective } : {}),
|
|
202
204
|
"generated.override.yaml": c.plan.override,
|
|
203
205
|
"compose.lock.json": JSON.stringify(c.plan.lock, null, 2) + "\n",
|
|
204
206
|
"request.json": JSON.stringify({ declaration: c.declaration, sdk: c.sdk, gateway }, null, 2) + "\n",
|
|
@@ -207,7 +209,7 @@ export function writeComposePayload(c, root, target, gateway) {
|
|
|
207
209
|
}))
|
|
208
210
|
writeFileSync(join(target, name), content, { mode: 0o600 });
|
|
209
211
|
}
|
|
210
|
-
/**
|
|
212
|
+
/** Materialize fixed package identity and native wizard share names in the incoming tree. */
|
|
211
213
|
export function renderComposeInstall(pkg) {
|
|
212
214
|
return `#!/bin/sh
|
|
213
215
|
set -eu
|
|
@@ -268,6 +270,7 @@ export function lockComposeImages(m, arch, resolveImages, imageProofs) {
|
|
|
268
270
|
sdk: m.compose.sdk,
|
|
269
271
|
mode: m.identityGateway?.mode ?? "none",
|
|
270
272
|
composeMounts: m.capabilities.composeMounts,
|
|
273
|
+
sharedFolder: m.sharedFolder,
|
|
271
274
|
resolveImages,
|
|
272
275
|
architecture: arch,
|
|
273
276
|
imageProofs,
|
|
@@ -346,3 +349,4 @@ export function dsmComposeProblem(c) {
|
|
|
346
349
|
};
|
|
347
350
|
}
|
|
348
351
|
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.";
|
|
352
|
+
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
|
});
|
|
@@ -606,6 +608,8 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
606
608
|
if (composeSource(c) !==
|
|
607
609
|
readFileSync(join(root, "compose/docker-compose.yml"), "utf8"))
|
|
608
610
|
throw new Error("worker Compose differs from the SDK plan");
|
|
611
|
+
if (c.plan.devices?.gpu && readFileSync(join(root, "compose/gpu.template.yaml"), "utf8") !== c.plan.effective)
|
|
612
|
+
throw new Error("GPU template differs from the SDK plan");
|
|
609
613
|
if (c.plan.override !==
|
|
610
614
|
readFileSync(join(root, "compose/generated.override.yaml"), "utf8"))
|
|
611
615
|
throw new Error("generated override differs from the SDK plan");
|
|
@@ -641,6 +645,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
641
645
|
: {}),
|
|
642
646
|
},
|
|
643
647
|
compose: c,
|
|
648
|
+
sharedFolder: recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))),
|
|
644
649
|
...(g ? { identityGateway: g } : {}),
|
|
645
650
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
646
651
|
};
|
|
@@ -650,6 +655,11 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
650
655
|
throw new Error("gateway configuration differs from the declared package contract");
|
|
651
656
|
if (readOptional(join(dir, "conf/privilege")) !== renderNativePrivilege())
|
|
652
657
|
throw new Error("Compose hooks must run as the package account");
|
|
658
|
+
if (manifest.sharedFolder) {
|
|
659
|
+
for (const [name, text] of Object.entries(expectedWizardFiles(info.package, manifest.sharedFolder)))
|
|
660
|
+
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
661
|
+
throw new Error(`unexpected WIZARD_UIFILES/${name}; native share selection must match the declaration`);
|
|
662
|
+
}
|
|
653
663
|
const resource = renderStandaloneResource(manifest);
|
|
654
664
|
if (readOptional(join(dir, "conf/resource")) !== resource)
|
|
655
665
|
throw new Error("unexpected worker resource; unapproved Shares must never auto-acquire");
|
|
@@ -671,7 +681,6 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
671
681
|
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
672
682
|
throw new Error(`unexpected WIZARD_UIFILES/${name}; retained-volume guidance must match the plan`);
|
|
673
683
|
}
|
|
674
|
-
const appScripts = unwrapAppScripts(join(dir, "scripts"), findings);
|
|
675
684
|
manifest.appScripts = undefined;
|
|
676
685
|
for (const hook of [
|
|
677
686
|
"preinst",
|
|
@@ -725,6 +734,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
725
734
|
if (storageProblem)
|
|
726
735
|
throw new Error(`[${storageProblem.code}] ${storageProblem.message}`);
|
|
727
736
|
base.compose = {
|
|
737
|
+
...(c.plan.devices ? { devices: c.plan.devices } : {}),
|
|
728
738
|
...(c.plan.ingress ? { ingress: c.plan.ingress } : {}),
|
|
729
739
|
images: c.plan.images,
|
|
730
740
|
services: c.plan.services,
|
|
@@ -737,9 +747,10 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
737
747
|
? "none"
|
|
738
748
|
: c.plan.services.find((s) => s.service === c.web?.service)
|
|
739
749
|
?.network === "host"
|
|
740
|
-
? "host-uid-listener"
|
|
750
|
+
? c.web.health ? "host-uid-listener-health" : "host-uid-listener"
|
|
741
751
|
: "health-fixed-ip-lifecycle",
|
|
742
|
-
limitation:
|
|
752
|
+
limitation: c.plan.services.find((s) => s.service === c.web?.service)?.network === "host"
|
|
753
|
+
? COMPOSE_HOST_READINESS_LIMITATION : COMPOSE_READINESS_LIMITATION,
|
|
743
754
|
},
|
|
744
755
|
};
|
|
745
756
|
}
|
|
@@ -1033,7 +1044,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
|
|
|
1033
1044
|
if (!resource["docker-project"])
|
|
1034
1045
|
findings.push("the container is missing the docker-project resource");
|
|
1035
1046
|
for (const key of Object.keys(resource)) {
|
|
1036
|
-
if (!["docker-project", "webservice"].includes(key))
|
|
1047
|
+
if (!["docker-project", "webservice", ...(compose?.plan.devices?.gpu ? ["video-driver"] : [])].includes(key))
|
|
1037
1048
|
findings.push(`Unknown container resource: ${key}`);
|
|
1038
1049
|
}
|
|
1039
1050
|
}
|
|
@@ -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 &&
|
|
@@ -11,6 +11,7 @@ export function renderStandaloneResource(m, portal) {
|
|
|
11
11
|
const dataShare = m.sharedFolder && sharedFolderResource(m.package, m.sharedFolder);
|
|
12
12
|
const webConfig = { ...(portal ? portalResource()['web-config'] : {}), ...nativeGatewayResource(m) };
|
|
13
13
|
const resource = {
|
|
14
|
+
...(m.compose?.plan.devices?.gpu && !m.compose.plan.mounts_blocked ? { "video-driver": {} } : {}),
|
|
14
15
|
...(m.compose && !m.compose.plan.mounts_blocked
|
|
15
16
|
? renderDockerProjectResource(m.package, "compose", false, m.compose.plan.project_name)
|
|
16
17
|
: {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.21.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
|
|