@synopackageland/cli 0.3.9 → 0.4.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.
Files changed (286) hide show
  1. package/README.md +20 -9
  2. package/dist/bin.js +1 -2
  3. package/dist/build-spk/generator.d.ts +0 -3
  4. package/dist/build-spk/generator.js +135 -287
  5. package/dist/build-spk/native-lifecycle.js +0 -3
  6. package/dist/build-spk/native-payload.js +0 -1
  7. package/dist/cli.js +171 -151
  8. package/dist/compose/analyze.d.ts +1 -2
  9. package/dist/compose/analyze.js +5 -48
  10. package/dist/contract/unsupported-types.d.ts +10 -0
  11. package/dist/contract/unsupported-types.js +49 -0
  12. package/dist/contract/validate.js +25 -2
  13. package/dist/contract/web-launch.js +0 -2
  14. package/dist/deploy/deploy.d.ts +1 -4
  15. package/dist/deploy/deploy.js +57 -156
  16. package/dist/deploy/homes-values.js +1 -1
  17. package/dist/deploy/plan.d.ts +18 -0
  18. package/dist/deploy/plan.js +54 -0
  19. package/dist/deploy/port-preflight.d.ts +21 -0
  20. package/dist/deploy/port-preflight.js +42 -0
  21. package/dist/deploy/spk-version.d.ts +2 -14
  22. package/dist/deploy/spk-version.js +25 -111
  23. package/dist/dev.d.ts +1 -3
  24. package/dist/dev.js +14 -28
  25. package/dist/error-registry.js +810 -857
  26. package/dist/host/dsm-client.js +23 -263
  27. package/dist/host/dsm-port-profile.d.ts +47 -0
  28. package/dist/host/dsm-port-profile.js +206 -0
  29. package/dist/host/port-info-lookup.d.ts +9 -0
  30. package/dist/host/port-info-lookup.js +87 -0
  31. package/dist/host/port-info.d.ts +33 -0
  32. package/dist/host/port-info.js +66 -0
  33. package/dist/host/types.d.ts +6 -78
  34. package/dist/index.d.ts +0 -9
  35. package/dist/index.js +0 -6
  36. package/dist/info.d.ts +0 -1
  37. package/dist/info.js +3 -10
  38. package/dist/init.d.ts +11 -1
  39. package/dist/init.js +112 -4
  40. package/dist/log.js +2 -3
  41. package/dist/native/validate.js +0 -1
  42. package/dist/standalone/binary.d.ts +9 -0
  43. package/dist/standalone/binary.js +67 -0
  44. package/dist/standalone/build.d.ts +25 -0
  45. package/dist/standalone/build.js +108 -0
  46. package/dist/standalone/dsm-window.d.ts +26 -0
  47. package/dist/standalone/dsm-window.js +251 -0
  48. package/dist/standalone/helper.d.ts +31 -0
  49. package/dist/standalone/helper.js +104 -0
  50. package/dist/standalone/i18n.d.ts +28 -0
  51. package/dist/standalone/i18n.js +161 -0
  52. package/dist/standalone/inspect-spk.d.ts +21 -0
  53. package/dist/standalone/inspect-spk.js +160 -0
  54. package/dist/standalone/manifest.d.ts +71 -0
  55. package/dist/standalone/manifest.js +276 -0
  56. package/dist/standalone/root-messages.d.ts +7 -0
  57. package/dist/standalone/root-messages.js +250 -0
  58. package/dist/standalone/root-ops.d.ts +42 -0
  59. package/dist/standalone/root-ops.js +101 -0
  60. package/dist/standalone/root-window.d.ts +35 -0
  61. package/dist/standalone/root-window.js +290 -0
  62. package/dist/standalone/spk.d.ts +22 -0
  63. package/dist/standalone/spk.js +205 -0
  64. package/dist/templates/binary/synology-app.yaml +7 -0
  65. package/dist/templates/hello-files/compose.yaml +0 -1
  66. package/dist/templates/hello-web/compose.yaml +13 -10
  67. package/dist/templates/hello-web/html/app.js +1 -23
  68. package/dist/templates/hello-web/html/index.html +1 -3
  69. package/dist/templates/hello-web-page/compose.yaml +13 -10
  70. package/dist/templates/hello-web-page/html/app.js +1 -23
  71. package/dist/templates/hello-web-page/html/index.html +1 -3
  72. package/dist/templates/native-go/README.md +268 -0
  73. package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
  74. package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
  75. package/dist/templates/native-go/backend/go.mod +3 -0
  76. package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
  77. package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
  78. package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
  79. package/dist/templates/native-go/backend/internal/api/server.go +155 -0
  80. package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
  81. package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
  82. package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
  83. package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
  84. package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  85. package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
  86. package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
  87. package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  88. package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  89. package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  90. package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
  91. package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  92. package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
  93. package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
  94. package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
  95. package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  96. package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  97. package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  98. package/dist/templates/native-go/dsm-package.yaml +58 -0
  99. package/dist/templates/native-go/gitignore +1 -0
  100. package/dist/templates/native-go/i18n/glossary.json +662 -0
  101. package/dist/templates/native-go/i18n/messages/de.json +28 -0
  102. package/dist/templates/native-go/i18n/messages/en.json +28 -0
  103. package/dist/templates/native-go/i18n/messages/es.json +28 -0
  104. package/dist/templates/native-go/i18n/messages/fr.json +28 -0
  105. package/dist/templates/native-go/i18n/messages/it.json +28 -0
  106. package/dist/templates/native-go/i18n/messages/ja.json +28 -0
  107. package/dist/templates/native-go/i18n/messages/ko.json +28 -0
  108. package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
  109. package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
  110. package/dist/templates/native-go/ui/app.css +35 -0
  111. package/dist/templates/native-go/ui/app.js +94 -0
  112. package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  113. package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  114. package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
  115. package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
  116. package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
  117. package/dist/templates/native-go-better-auth/node/package.json +1 -0
  118. package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  119. package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  120. package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  121. package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  122. package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
  123. package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  124. package/dist/templates/native-go-root/root/whoami.sh +6 -0
  125. package/dist/templates/native-go-vue/README.md +110 -0
  126. package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
  127. package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
  128. package/dist/templates/native-go-vue/frontend/package.json +6 -0
  129. package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
  130. package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
  131. package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
  132. package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
  133. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  134. package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
  135. package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
  136. package/dist/templates/native-go-vue/gitignore +2 -0
  137. package/dist/types.d.ts +1 -1
  138. package/dist/validate.js +11 -7
  139. package/package.json +3 -7
  140. package/schema/synology-app.schema.json +36 -37
  141. package/skills/SKILL.md +4 -48
  142. package/skills/reference/create.md +11 -83
  143. package/skills/reference/deploy.md +7 -78
  144. package/skills/reference/diagnose.md +4 -101
  145. package/skills/reference/files.md +5 -41
  146. package/skills/reference/identity.md +3 -29
  147. package/skills/reference/wrap.md +0 -2
  148. package/templates/binary/synology-app.yaml +7 -0
  149. package/templates/hello-files/compose.yaml +0 -1
  150. package/templates/hello-web/compose.yaml +13 -10
  151. package/templates/hello-web/html/app.js +1 -23
  152. package/templates/hello-web/html/index.html +1 -3
  153. package/templates/hello-web-page/compose.yaml +13 -10
  154. package/templates/hello-web-page/html/app.js +1 -23
  155. package/templates/hello-web-page/html/index.html +1 -3
  156. package/templates/native-go/README.md +268 -0
  157. package/templates/native-go/backend/cmd/app/main.go +49 -0
  158. package/templates/native-go/backend/cmd/helper/main.go +38 -0
  159. package/templates/native-go/backend/go.mod +3 -0
  160. package/templates/native-go/backend/internal/api/errors.go +33 -0
  161. package/templates/native-go/backend/internal/api/registry.go +94 -0
  162. package/templates/native-go/backend/internal/api/registry_test.go +329 -0
  163. package/templates/native-go/backend/internal/api/server.go +155 -0
  164. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  165. package/templates/native-go/backend/internal/app/routes.go +127 -0
  166. package/templates/native-go/backend/internal/app/routes_test.go +167 -0
  167. package/templates/native-go/backend/internal/dsm/auth.go +118 -0
  168. package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  169. package/templates/native-go/backend/internal/helper/control.go +219 -0
  170. package/templates/native-go/backend/internal/helper/control_test.go +242 -0
  171. package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  172. package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  173. package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  174. package/templates/native-go/backend/internal/helper/process_other.go +5 -0
  175. package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  176. package/templates/native-go/backend/internal/helper/status.go +271 -0
  177. package/templates/native-go/backend/internal/helper/status_test.go +205 -0
  178. package/templates/native-go/backend/internal/helper/supervision.go +84 -0
  179. package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  180. package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  181. package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  182. package/templates/native-go/dsm-package.yaml +58 -0
  183. package/templates/native-go/gitignore +1 -0
  184. package/templates/native-go/i18n/glossary.json +662 -0
  185. package/templates/native-go/i18n/messages/de.json +28 -0
  186. package/templates/native-go/i18n/messages/en.json +28 -0
  187. package/templates/native-go/i18n/messages/es.json +28 -0
  188. package/templates/native-go/i18n/messages/fr.json +28 -0
  189. package/templates/native-go/i18n/messages/it.json +28 -0
  190. package/templates/native-go/i18n/messages/ja.json +28 -0
  191. package/templates/native-go/i18n/messages/ko.json +28 -0
  192. package/templates/native-go/i18n/messages/zh-CN.json +28 -0
  193. package/templates/native-go/i18n/messages/zh-TW.json +28 -0
  194. package/templates/native-go/ui/app.css +35 -0
  195. package/templates/native-go/ui/app.js +94 -0
  196. package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  197. package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  198. package/templates/native-go-better-auth/node/app.mjs +72 -0
  199. package/templates/native-go-better-auth/node/build.mjs +2 -0
  200. package/templates/native-go-better-auth/node/package-lock.json +862 -0
  201. package/templates/native-go-better-auth/node/package.json +1 -0
  202. package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  203. package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  204. package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  205. package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  206. package/templates/native-go-root/backend/internal/app/root.go +107 -0
  207. package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  208. package/templates/native-go-root/root/whoami.sh +6 -0
  209. package/templates/native-go-vue/README.md +110 -0
  210. package/templates/native-go-vue/frontend/DESIGN.md +78 -0
  211. package/templates/native-go-vue/frontend/package-lock.json +2507 -0
  212. package/templates/native-go-vue/frontend/package.json +6 -0
  213. package/templates/native-go-vue/frontend/src/App.vue +132 -0
  214. package/templates/native-go-vue/frontend/src/context.ts +17 -0
  215. package/templates/native-go-vue/frontend/src/main.ts +26 -0
  216. package/templates/native-go-vue/frontend/src/style.css +14 -0
  217. package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  218. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  219. package/templates/native-go-vue/frontend/vite.config.ts +8 -0
  220. package/templates/native-go-vue/gitignore +2 -0
  221. package/dist/attestation/app-package.d.ts +0 -86
  222. package/dist/attestation/app-package.js +0 -206
  223. package/dist/attestation/install-on-host.d.ts +0 -14
  224. package/dist/attestation/install-on-host.js +0 -52
  225. package/dist/attestation/keyring.d.ts +0 -21
  226. package/dist/attestation/keyring.js +0 -168
  227. package/dist/attestation/platform-service.d.ts +0 -70
  228. package/dist/attestation/platform-service.js +0 -250
  229. package/dist/build-spk/broker-glue.d.ts +0 -16
  230. package/dist/build-spk/broker-glue.js +0 -40
  231. package/dist/deploy/catalog-publish.d.ts +0 -3
  232. package/dist/deploy/catalog-publish.js +0 -7
  233. package/dist/doctor.d.ts +0 -22
  234. package/dist/doctor.js +0 -721
  235. package/dist/error-collector.d.ts +0 -2
  236. package/dist/error-collector.js +0 -46
  237. package/dist/host/broker-paths.d.ts +0 -1
  238. package/dist/host/broker-paths.js +0 -1
  239. package/dist/host/broker-upgrade-preflight.d.ts +0 -61
  240. package/dist/host/broker-upgrade-preflight.js +0 -156
  241. package/dist/host/digests.d.ts +0 -5
  242. package/dist/host/digests.js +0 -63
  243. package/dist/host/fake-nas.d.ts +0 -41
  244. package/dist/host/fake-nas.js +0 -174
  245. package/dist/mock-broker/bootstrap.d.ts +0 -2
  246. package/dist/mock-broker/bootstrap.js +0 -8
  247. package/dist/mock-broker/files.d.ts +0 -60
  248. package/dist/mock-broker/files.js +0 -408
  249. package/dist/mock-broker/oidc.d.ts +0 -31
  250. package/dist/mock-broker/oidc.js +0 -161
  251. package/dist/mock-broker/path.d.ts +0 -9
  252. package/dist/mock-broker/path.js +0 -50
  253. package/dist/mock-broker/server.d.ts +0 -19
  254. package/dist/mock-broker/server.js +0 -350
  255. package/dist/publish/build-ledger.d.ts +0 -22
  256. package/dist/publish/build-ledger.js +0 -26
  257. package/dist/publish/community-path.d.ts +0 -6
  258. package/dist/publish/community-path.js +0 -41
  259. package/dist/publish/curated-release.d.ts +0 -78
  260. package/dist/publish/curated-release.js +0 -883
  261. package/dist/publish/file-build-ledger.d.ts +0 -18
  262. package/dist/publish/file-build-ledger.js +0 -268
  263. package/dist/publish/github-pr.d.ts +0 -25
  264. package/dist/publish/github-pr.js +0 -85
  265. package/dist/publish/github-repo.d.ts +0 -8
  266. package/dist/publish/github-repo.js +0 -21
  267. package/dist/publish/platform-service-release.d.ts +0 -60
  268. package/dist/publish/platform-service-release.js +0 -333
  269. package/dist/publish/preflight.d.ts +0 -24
  270. package/dist/publish/preflight.js +0 -138
  271. package/dist/publish/publish.d.ts +0 -27
  272. package/dist/publish/publish.js +0 -136
  273. package/dist/publish/sync-definition.d.ts +0 -4
  274. package/dist/publish/sync-definition.js +0 -48
  275. package/dist/release-bin.d.ts +0 -2
  276. package/dist/release-bin.js +0 -23
  277. package/dist/templates/hello-web/html/sdk.js +0 -68
  278. package/dist/templates/hello-web-page/html/sdk.js +0 -68
  279. package/dist/templates/native/synology-app.yaml +0 -7
  280. package/dist/test-host.d.ts +0 -15
  281. package/dist/test-host.js +0 -37
  282. package/templates/hello-web/html/sdk.js +0 -68
  283. package/templates/hello-web-page/html/sdk.js +0 -68
  284. package/templates/native/synology-app.yaml +0 -7
  285. /package/dist/templates/{native → binary}/native.yaml +0 -0
  286. /package/templates/{native → binary}/native.yaml +0 -0
@@ -0,0 +1,167 @@
1
+ package helper
2
+
3
+ import (
4
+ "context"
5
+ "crypto/sha256"
6
+ "encoding/hex"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ "os/exec"
12
+ "path/filepath"
13
+ "runtime"
14
+ "strconv"
15
+ "strings"
16
+ "syscall"
17
+ "time"
18
+ )
19
+
20
+ func executableDigest(path string) (string, error) {
21
+ f, err := os.Open(path)
22
+ if err != nil {
23
+ return "", err
24
+ }
25
+ defer f.Close()
26
+ st, err := f.Stat()
27
+ if err != nil {
28
+ return "", err
29
+ }
30
+ s, ok := st.Sys().(*syscall.Stat_t)
31
+ if !ok || s.Uid != 0 || st.Mode().Perm()&0022 != 0 || !st.Mode().IsRegular() || st.Size() > 64<<20 {
32
+ return "", errTrust
33
+ }
34
+ h := sha256.New()
35
+ if _, err := io.Copy(h, f); err != nil {
36
+ return "", err
37
+ }
38
+ return hex.EncodeToString(h.Sum(nil)), nil
39
+ }
40
+
41
+ func inspectSupervisor(pid int, i Inventory) (Process, error) {
42
+ base := fmt.Sprintf("/proc/%d/", pid)
43
+ b, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
44
+ if err != nil {
45
+ return Process{}, err
46
+ }
47
+ stat, err := os.ReadFile(base + "stat")
48
+ if err != nil {
49
+ return Process{}, err
50
+ }
51
+ end := strings.LastIndex(string(stat), ") ")
52
+ if end < 0 {
53
+ return Process{}, errTrust
54
+ }
55
+ fields := strings.Fields(string(stat)[end+2:])
56
+ if len(fields) < 20 {
57
+ return Process{}, errTrust
58
+ }
59
+ status, err := os.ReadFile(base + "status")
60
+ if err != nil {
61
+ return Process{}, err
62
+ }
63
+ root := false
64
+ for _, line := range strings.Split(string(status), "\n") {
65
+ if strings.HasPrefix(line, "Uid:") {
66
+ u := strings.Fields(line)
67
+ root = len(u) == 5 && u[1] == "0" && u[2] == "0" && u[3] == "0" && u[4] == "0"
68
+ }
69
+ }
70
+ if !root {
71
+ return Process{}, errTrust
72
+ }
73
+ cmd, err := os.ReadFile(base + "cmdline")
74
+ if err != nil {
75
+ return Process{}, err
76
+ }
77
+ wantExe := "/var/lib/synopkgland-" + i.Package + "/payload-" + i.PayloadDigest
78
+ if string(cmd) != wantExe+"\x00supervise\x00" {
79
+ return Process{}, errTrust
80
+ }
81
+ st, err := os.Stat(base + "exe")
82
+ if err != nil {
83
+ return Process{}, err
84
+ }
85
+ s := st.Sys().(*syscall.Stat_t)
86
+ digest, err := executableDigest(base + "exe")
87
+ if err != nil || digest != i.PayloadDigest {
88
+ return Process{}, errTrust
89
+ }
90
+ return Process{Inventory: i, Mode: "supervised", PID: pid, Boot: strings.TrimSpace(string(b)), Start: fields[19], Device: uint64(s.Dev), Inode: s.Ino}, nil
91
+ }
92
+
93
+ type LinuxProcesses struct {
94
+ Client
95
+ Binary string
96
+ }
97
+
98
+ func (p LinuxProcesses) Start(i Inventory) error {
99
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
100
+ defer cancel()
101
+ if i != p.Expected {
102
+ return errTrust
103
+ }
104
+ if err := protected(p.RunDir, 0, true); err != nil {
105
+ return err
106
+ }
107
+ cmd := exec.Command(p.Binary, "supervise")
108
+ cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin"}
109
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
110
+ if err := cmd.Start(); err != nil {
111
+ return err
112
+ }
113
+ go cmd.Wait()
114
+ if waitHealthy(ctx, func(ctx context.Context) bool {
115
+ state, _ := p.probe(ctx, filepath.Join(p.RunDir, "helper.sock"))
116
+ return state == "ready"
117
+ }) == nil {
118
+ return nil
119
+ }
120
+ return errors.New("helper start timeout; residual preserved")
121
+ }
122
+
123
+ // RunLinux runs a fixed control action or the health-only server. Its caller
124
+ // must be the hash-verified, root-owned copy, never the SPK staging executable.
125
+ func RunLinux(base Inventory, action string) error {
126
+ if os.Geteuid() != 0 {
127
+ return errTrust
128
+ }
129
+ exe, err := os.Executable()
130
+ if err != nil {
131
+ return err
132
+ }
133
+ digest, err := executableDigest(exe)
134
+ if err != nil {
135
+ return err
136
+ }
137
+ base.PayloadDigest = digest
138
+ if !base.Valid() || exe != "/var/lib/synopkgland-"+base.Package+"/payload-"+digest {
139
+ return errTrust
140
+ }
141
+ c := NewClient(base)
142
+ if err := protected(c.StateDir, 0, true); err != nil {
143
+ return err
144
+ }
145
+ if err := protected(c.RunDir, 0, true); err != nil {
146
+ return err
147
+ }
148
+ switch action {
149
+ case "supervise":
150
+ return superviseLinux(c, exe)
151
+ case "serve-child":
152
+ parent, err := strconv.Atoi(os.Getenv("SYNOPKGLAND_PARENT"))
153
+ if err != nil {
154
+ return errTrust
155
+ }
156
+ runtime.LockOSThread()
157
+ defer runtime.UnlockOSThread()
158
+ if err := armParentDeath(parent, setParentDeath, os.Getppid); err != nil {
159
+ return err
160
+ }
161
+ if _, err := inspectSupervisor(parent, base); err != nil {
162
+ return err
163
+ }
164
+ return serveLinux(c)
165
+ }
166
+ return (Controller{Client: c, Processes: LinuxProcesses{Client: c, Binary: exe}}).Control(context.Background(), action)
167
+ }
@@ -0,0 +1,5 @@
1
+ //go:build !linux
2
+
3
+ package helper
4
+
5
+ func RunLinux(Inventory, string) error { return errTrust }
@@ -0,0 +1,83 @@
1
+ package helper
2
+
3
+ import (
4
+ "encoding/json"
5
+ "io"
6
+ "net"
7
+ "os"
8
+ "os/user"
9
+ "path/filepath"
10
+ "strconv"
11
+ "time"
12
+ )
13
+
14
+ func serveLinux(c Client) error {
15
+ r, err := readRecord(c.StateDir, 0)
16
+ if err != nil || !r.Enabled || r.Inventory != c.Expected {
17
+ return errTrust
18
+ }
19
+ // Only the supervisor writes process.json.
20
+ // The package group can connect; ordinary users reach only the authenticated
21
+ // CGI status route. SO_PEERCRED also checks the package's current numeric UID.
22
+ u, err := user.Lookup(c.Expected.Package)
23
+ if err != nil {
24
+ return err
25
+ }
26
+ uid, err := strconv.ParseUint(u.Uid, 10, 32)
27
+ if err != nil {
28
+ return err
29
+ }
30
+ group, err := user.LookupGroup(c.Expected.Package)
31
+ if err != nil {
32
+ return err
33
+ }
34
+ gid, err := strconv.Atoi(group.Gid)
35
+ if err != nil {
36
+ return err
37
+ }
38
+ socket := filepath.Join(c.RunDir, "helper.sock")
39
+ l, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
40
+ if err != nil {
41
+ return err
42
+ }
43
+ l.SetUnlinkOnClose(false)
44
+ defer l.Close()
45
+ if err := os.Chown(socket, 0, gid); err != nil {
46
+ return err
47
+ }
48
+ if err := os.Chmod(socket, 0660); err != nil {
49
+ return err
50
+ }
51
+ // Bound concurrency and each frame's size/time. No privileged mutations are
52
+ // accepted over this socket; lifecycle control remains fixed admin tasks.
53
+ slots := make(chan struct{}, 8)
54
+ for {
55
+ conn, err := l.AcceptUnix()
56
+ if err != nil {
57
+ return err
58
+ }
59
+ select {
60
+ case slots <- struct{}{}:
61
+ default:
62
+ conn.Close()
63
+ continue
64
+ }
65
+ go func() {
66
+ defer func() { conn.Close(); <-slots }()
67
+ peer, err := peerUID(conn)
68
+ if err != nil || (peer != 0 && peer != uint32(uid)) {
69
+ return
70
+ }
71
+ conn.SetDeadline(time.Now().Add(time.Second))
72
+ var req Request
73
+ d := json.NewDecoder(io.LimitReader(conn, 4097))
74
+ d.DisallowUnknownFields()
75
+ if d.Decode(&req) != nil {
76
+ return
77
+ }
78
+ r, err := readRecord(c.StateDir, 0)
79
+ disabled := err != nil || !r.Enabled || r.Inventory != c.Expected
80
+ json.NewEncoder(conn).Encode(Respond(c.Expected, disabled, req))
81
+ }()
82
+ }
83
+ }
@@ -0,0 +1,271 @@
1
+ // Package helper implements the opt-in per-package lifecycle contract. It has
2
+ // no file/grant/job API: all unimplemented operations fail closed.
3
+ package helper
4
+
5
+ import (
6
+ "context"
7
+ "crypto/rand"
8
+ "crypto/sha256"
9
+ "encoding/hex"
10
+ "encoding/json"
11
+ "errors"
12
+ "io"
13
+ "io/fs"
14
+ "net"
15
+ "os"
16
+ "path/filepath"
17
+ "regexp"
18
+ "syscall"
19
+ "time"
20
+ )
21
+
22
+ type Inventory struct {
23
+ Package string `json:"package"`
24
+ Version string `json:"version"`
25
+ Protocol int `json:"protocol"`
26
+ PayloadDigest string `json:"payloadDigest"`
27
+ RegistryDigest string `json:"registryDigest"`
28
+ }
29
+
30
+ var digestPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
31
+ var packagePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]{1,63}$`)
32
+ var versionPattern = regexp.MustCompile(`^\d+(\.\d+){0,3}-\d{4}$`)
33
+
34
+ func (i Inventory) Valid() bool {
35
+ return packagePattern.MatchString(i.Package) && versionPattern.MatchString(i.Version) && i.Protocol > 0 && digestPattern.MatchString(i.PayloadDigest) && digestPattern.MatchString(i.RegistryDigest)
36
+ }
37
+
38
+ type Record struct {
39
+ Inventory Inventory `json:"inventory"`
40
+ Enabled bool `json:"enabled"`
41
+ Residual bool `json:"residual,omitempty"`
42
+ }
43
+ type Request struct {
44
+ Inventory Inventory `json:"inventory"`
45
+ Op string `json:"op"`
46
+ Nonce string `json:"nonce"`
47
+ }
48
+ type Response struct {
49
+ Inventory Inventory `json:"inventory"`
50
+ Nonce string `json:"nonce"`
51
+ OK bool `json:"ok"`
52
+ Error string `json:"error,omitempty"`
53
+ }
54
+
55
+ func Respond(i Inventory, disabled bool, req Request) Response {
56
+ r := Response{Inventory: i, Nonce: req.Nonce}
57
+ switch {
58
+ case disabled:
59
+ r.Error = "disabled"
60
+ case !i.Valid() || req.Inventory != i:
61
+ r.Error = "incompatible"
62
+ case req.Op != "health":
63
+ r.Error = "unsupported"
64
+ default:
65
+ r.OK = true
66
+ }
67
+ return r
68
+ }
69
+
70
+ // State is safe to expose to any authenticated DSM user. Paths, PIDs, worker
71
+ // internals, scheduler definitions and raw errors never enter this envelope.
72
+ type State struct {
73
+ State string `json:"state"`
74
+ Reason string `json:"reason"`
75
+ PackageRunning string `json:"packageRunning"`
76
+ Scheduler string `json:"scheduler"`
77
+ Cleanup string `json:"cleanup,omitempty"`
78
+ }
79
+
80
+ type Client struct {
81
+ Expected Inventory
82
+ StateDir, RunDir string
83
+ // Production uses UID 0 and RootPeer. Tests inject only local fixture identity.
84
+ Owner uint32
85
+ Peer func(*net.UnixConn) error
86
+ }
87
+
88
+ func NewClient(i Inventory) Client {
89
+ return Client{Expected: i, StateDir: "/var/lib/synopkgland-" + i.Package, RunDir: "/run/synopkgland-helper-" + i.Package, Peer: RootPeer}
90
+ }
91
+
92
+ // LocalClient is a nonprivileged diagnostic entry point for package hooks.
93
+ // Hashing the local executable establishes only the expected build inventory;
94
+ // readiness still requires the protected record and root socket handshake.
95
+ func LocalClient(i Inventory) (Client, error) {
96
+ exe, err := os.Executable()
97
+ if err != nil {
98
+ return Client{}, err
99
+ }
100
+ f, err := os.Open(exe)
101
+ if err != nil {
102
+ return Client{}, err
103
+ }
104
+ defer f.Close()
105
+ h := sha256.New()
106
+ if _, err := io.Copy(h, io.LimitReader(f, (64<<20)+1)); err != nil {
107
+ return Client{}, err
108
+ }
109
+ i.PayloadDigest = hex.EncodeToString(h.Sum(nil))
110
+ if !i.Valid() {
111
+ return Client{}, errTrust
112
+ }
113
+ return NewClient(i), nil
114
+ }
115
+
116
+ func (c Client) CheckStopped(ctx context.Context) error {
117
+ s := c.Status(ctx)
118
+ if s.Cleanup != "" || (s.State != "not-enabled" && s.State != "stopped" && s.State != "needs-approval") {
119
+ return errors.New("approved helper teardown required")
120
+ }
121
+ for _, file := range []string{"process.json", "helper.sock", "supervisor.sock"} {
122
+ if _, err := os.Lstat(filepath.Join(c.RunDir, file)); !errors.Is(err, fs.ErrNotExist) {
123
+ return errors.New("helper residue; approved teardown required")
124
+ }
125
+ }
126
+ return nil
127
+ }
128
+
129
+ var errTrust = errors.New("untrusted helper state")
130
+
131
+ func protected(path string, owner uint32, directory bool) error {
132
+ st, err := os.Lstat(path)
133
+ if err != nil {
134
+ return err
135
+ }
136
+ s, ok := st.Sys().(*syscall.Stat_t)
137
+ if !ok || s.Uid != owner || st.Mode().Perm()&0022 != 0 || (directory && !st.IsDir()) || (!directory && !st.Mode().IsRegular()) {
138
+ return errTrust
139
+ }
140
+ return nil
141
+ }
142
+
143
+ func readRecord(dir string, owner uint32) (Record, error) {
144
+ var r Record
145
+ if err := protected(dir, owner, true); err != nil {
146
+ return r, err
147
+ }
148
+ f, err := os.OpenFile(filepath.Join(dir, "approval.json"), os.O_RDONLY|syscall.O_NOFOLLOW, 0)
149
+ if err != nil {
150
+ return r, err
151
+ }
152
+ defer f.Close()
153
+ st, err := f.Stat()
154
+ if err != nil {
155
+ return r, err
156
+ }
157
+ s, ok := st.Sys().(*syscall.Stat_t)
158
+ if !ok || s.Uid != owner || st.Mode().Perm()&0022 != 0 || !st.Mode().IsRegular() || st.Size() > 4096 {
159
+ return r, errTrust
160
+ }
161
+ d := json.NewDecoder(io.LimitReader(f, 4097))
162
+ d.DisallowUnknownFields()
163
+ var decoded struct {
164
+ Inventory Inventory `json:"inventory"`
165
+ Enabled *bool `json:"enabled"`
166
+ Residual bool `json:"residual,omitempty"`
167
+ }
168
+ if d.Decode(&decoded) != nil || d.Decode(new(any)) != io.EOF || !decoded.Inventory.Valid() || decoded.Enabled == nil {
169
+ return r, errTrust
170
+ }
171
+ r = Record{Inventory: decoded.Inventory, Enabled: *decoded.Enabled, Residual: decoded.Residual}
172
+ return r, nil
173
+ }
174
+
175
+ func (c Client) Status(ctx context.Context) State {
176
+ result := func(state, reason string) State {
177
+ return State{State: state, Reason: reason, PackageRunning: "unknown", Scheduler: "not-probed"}
178
+ }
179
+ if c.Expected.Package == "" {
180
+ return result("not-enabled", "not-requested")
181
+ }
182
+ if !c.Expected.Valid() {
183
+ return result("incompatible", "invalid-inventory")
184
+ }
185
+ r, err := readRecord(c.StateDir, c.Owner)
186
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
187
+ return result("incompatible", "untrusted-approval")
188
+ }
189
+ if err == nil && !r.Enabled {
190
+ s := result("not-enabled", "disabled")
191
+ if r.Residual {
192
+ s.Cleanup = "required"
193
+ }
194
+ return s
195
+ }
196
+ if err == nil && r.Inventory != c.Expected {
197
+ return result("incompatible", "inventory-mismatch")
198
+ }
199
+ if err == nil && r.Residual {
200
+ s := result("failed", "incomplete-transition")
201
+ s.Cleanup = "required"
202
+ return s
203
+ }
204
+ // Conflict has precedence over missing approval. Even an unapproved leftover
205
+ // socket is probed before claiming that authorization alone can fix it.
206
+ if e := protected(c.RunDir, c.Owner, true); e != nil && !errors.Is(e, fs.ErrNotExist) {
207
+ return result("incompatible", "untrusted-runtime")
208
+ }
209
+ socket := filepath.Join(c.RunDir, "helper.sock")
210
+ st, e := os.Lstat(socket)
211
+ if e == nil {
212
+ s, ok := st.Sys().(*syscall.Stat_t)
213
+ if !ok || s.Uid != c.Owner || st.Mode()&os.ModeSocket == 0 || st.Mode().Perm()&0002 != 0 {
214
+ return result("incompatible", "untrusted-socket")
215
+ }
216
+ state, reason := c.probe(ctx, socket)
217
+ if state != "ready" {
218
+ return result(state, reason)
219
+ }
220
+ } else if !errors.Is(e, fs.ErrNotExist) {
221
+ return result("failed", "runtime-unreadable")
222
+ }
223
+ if err != nil {
224
+ return result("needs-approval", "approval-missing")
225
+ }
226
+ if e != nil {
227
+ return result("stopped", "runtime-missing")
228
+ }
229
+ return result("ready", "live-handshake")
230
+ }
231
+
232
+ func (c Client) probe(ctx context.Context, socket string) (string, string) {
233
+ deadline := time.Now().Add(time.Second)
234
+ if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
235
+ deadline = d
236
+ }
237
+ dialer := net.Dialer{Deadline: deadline}
238
+ conn, err := dialer.DialContext(ctx, "unix", socket)
239
+ if err != nil {
240
+ if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, fs.ErrNotExist) {
241
+ return "stopped", "runtime-not-listening"
242
+ }
243
+ return "failed", "probe-failed"
244
+ }
245
+ defer conn.Close()
246
+ if c.Peer == nil || c.Peer(conn.(*net.UnixConn)) != nil {
247
+ return "incompatible", "untrusted-peer"
248
+ }
249
+ conn.SetDeadline(deadline)
250
+ var nonce [32]byte
251
+ if _, err := rand.Read(nonce[:]); err != nil {
252
+ return "failed", "probe-failed"
253
+ }
254
+ req := Request{c.Expected, "health", hex.EncodeToString(nonce[:])}
255
+ if json.NewEncoder(conn).Encode(req) != nil {
256
+ return "failed", "probe-failed"
257
+ }
258
+ var res Response
259
+ d := json.NewDecoder(io.LimitReader(conn, 4097))
260
+ d.DisallowUnknownFields()
261
+ if d.Decode(&res) != nil {
262
+ return "failed", "invalid-handshake"
263
+ }
264
+ if res.Nonce != req.Nonce || res.Inventory != c.Expected || res.Error == "incompatible" {
265
+ return "incompatible", "handshake-mismatch"
266
+ }
267
+ if !res.OK || res.Error != "" {
268
+ return "failed", "health-refused"
269
+ }
270
+ return "ready", "live-handshake"
271
+ }