@synopackageland/cli 0.3.10 → 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 (280) 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 +45 -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/spk-version.d.ts +2 -14
  20. package/dist/deploy/spk-version.js +25 -111
  21. package/dist/dev.d.ts +1 -3
  22. package/dist/dev.js +14 -28
  23. package/dist/error-registry.js +810 -862
  24. package/dist/host/dsm-client.js +12 -272
  25. package/dist/host/dsm-port-profile.d.ts +0 -1
  26. package/dist/host/dsm-port-profile.js +0 -2
  27. package/dist/host/types.d.ts +1 -83
  28. package/dist/index.d.ts +0 -9
  29. package/dist/index.js +0 -6
  30. package/dist/info.d.ts +0 -1
  31. package/dist/info.js +3 -10
  32. package/dist/init.d.ts +11 -1
  33. package/dist/init.js +112 -4
  34. package/dist/log.js +2 -3
  35. package/dist/native/validate.js +0 -1
  36. package/dist/standalone/binary.d.ts +9 -0
  37. package/dist/standalone/binary.js +67 -0
  38. package/dist/standalone/build.d.ts +25 -0
  39. package/dist/standalone/build.js +108 -0
  40. package/dist/standalone/dsm-window.d.ts +26 -0
  41. package/dist/standalone/dsm-window.js +251 -0
  42. package/dist/standalone/helper.d.ts +31 -0
  43. package/dist/standalone/helper.js +104 -0
  44. package/dist/standalone/i18n.d.ts +28 -0
  45. package/dist/standalone/i18n.js +161 -0
  46. package/dist/standalone/inspect-spk.d.ts +21 -0
  47. package/dist/standalone/inspect-spk.js +160 -0
  48. package/dist/standalone/manifest.d.ts +71 -0
  49. package/dist/standalone/manifest.js +276 -0
  50. package/dist/standalone/root-messages.d.ts +7 -0
  51. package/dist/standalone/root-messages.js +250 -0
  52. package/dist/standalone/root-ops.d.ts +42 -0
  53. package/dist/standalone/root-ops.js +101 -0
  54. package/dist/standalone/root-window.d.ts +35 -0
  55. package/dist/standalone/root-window.js +290 -0
  56. package/dist/standalone/spk.d.ts +22 -0
  57. package/dist/standalone/spk.js +205 -0
  58. package/dist/templates/binary/synology-app.yaml +7 -0
  59. package/dist/templates/hello-files/compose.yaml +0 -1
  60. package/dist/templates/hello-web/compose.yaml +13 -10
  61. package/dist/templates/hello-web/html/app.js +1 -23
  62. package/dist/templates/hello-web/html/index.html +1 -3
  63. package/dist/templates/hello-web-page/compose.yaml +13 -10
  64. package/dist/templates/hello-web-page/html/app.js +1 -23
  65. package/dist/templates/hello-web-page/html/index.html +1 -3
  66. package/dist/templates/native-go/README.md +268 -0
  67. package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
  68. package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
  69. package/dist/templates/native-go/backend/go.mod +3 -0
  70. package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
  71. package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
  72. package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
  73. package/dist/templates/native-go/backend/internal/api/server.go +155 -0
  74. package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
  75. package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
  76. package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
  77. package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
  78. package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  79. package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
  80. package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
  81. package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  82. package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  83. package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  84. package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
  85. package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  86. package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
  87. package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
  88. package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
  89. package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  90. package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  91. package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  92. package/dist/templates/native-go/dsm-package.yaml +58 -0
  93. package/dist/templates/native-go/gitignore +1 -0
  94. package/dist/templates/native-go/i18n/glossary.json +662 -0
  95. package/dist/templates/native-go/i18n/messages/de.json +28 -0
  96. package/dist/templates/native-go/i18n/messages/en.json +28 -0
  97. package/dist/templates/native-go/i18n/messages/es.json +28 -0
  98. package/dist/templates/native-go/i18n/messages/fr.json +28 -0
  99. package/dist/templates/native-go/i18n/messages/it.json +28 -0
  100. package/dist/templates/native-go/i18n/messages/ja.json +28 -0
  101. package/dist/templates/native-go/i18n/messages/ko.json +28 -0
  102. package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
  103. package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
  104. package/dist/templates/native-go/ui/app.css +35 -0
  105. package/dist/templates/native-go/ui/app.js +94 -0
  106. package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  107. package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  108. package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
  109. package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
  110. package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
  111. package/dist/templates/native-go-better-auth/node/package.json +1 -0
  112. package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  113. package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  114. package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  115. package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  116. package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
  117. package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  118. package/dist/templates/native-go-root/root/whoami.sh +6 -0
  119. package/dist/templates/native-go-vue/README.md +110 -0
  120. package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
  121. package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
  122. package/dist/templates/native-go-vue/frontend/package.json +6 -0
  123. package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
  124. package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
  125. package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
  126. package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
  127. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  128. package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
  129. package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
  130. package/dist/templates/native-go-vue/gitignore +2 -0
  131. package/dist/types.d.ts +1 -1
  132. package/dist/validate.js +11 -7
  133. package/package.json +3 -7
  134. package/schema/synology-app.schema.json +36 -37
  135. package/skills/SKILL.md +4 -48
  136. package/skills/reference/create.md +11 -83
  137. package/skills/reference/deploy.md +7 -78
  138. package/skills/reference/diagnose.md +4 -101
  139. package/skills/reference/files.md +5 -41
  140. package/skills/reference/identity.md +3 -29
  141. package/skills/reference/wrap.md +0 -2
  142. package/templates/binary/synology-app.yaml +7 -0
  143. package/templates/hello-files/compose.yaml +0 -1
  144. package/templates/hello-web/compose.yaml +13 -10
  145. package/templates/hello-web/html/app.js +1 -23
  146. package/templates/hello-web/html/index.html +1 -3
  147. package/templates/hello-web-page/compose.yaml +13 -10
  148. package/templates/hello-web-page/html/app.js +1 -23
  149. package/templates/hello-web-page/html/index.html +1 -3
  150. package/templates/native-go/README.md +268 -0
  151. package/templates/native-go/backend/cmd/app/main.go +49 -0
  152. package/templates/native-go/backend/cmd/helper/main.go +38 -0
  153. package/templates/native-go/backend/go.mod +3 -0
  154. package/templates/native-go/backend/internal/api/errors.go +33 -0
  155. package/templates/native-go/backend/internal/api/registry.go +94 -0
  156. package/templates/native-go/backend/internal/api/registry_test.go +329 -0
  157. package/templates/native-go/backend/internal/api/server.go +155 -0
  158. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  159. package/templates/native-go/backend/internal/app/routes.go +127 -0
  160. package/templates/native-go/backend/internal/app/routes_test.go +167 -0
  161. package/templates/native-go/backend/internal/dsm/auth.go +118 -0
  162. package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  163. package/templates/native-go/backend/internal/helper/control.go +219 -0
  164. package/templates/native-go/backend/internal/helper/control_test.go +242 -0
  165. package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  166. package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  167. package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  168. package/templates/native-go/backend/internal/helper/process_other.go +5 -0
  169. package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  170. package/templates/native-go/backend/internal/helper/status.go +271 -0
  171. package/templates/native-go/backend/internal/helper/status_test.go +205 -0
  172. package/templates/native-go/backend/internal/helper/supervision.go +84 -0
  173. package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  174. package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  175. package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  176. package/templates/native-go/dsm-package.yaml +58 -0
  177. package/templates/native-go/gitignore +1 -0
  178. package/templates/native-go/i18n/glossary.json +662 -0
  179. package/templates/native-go/i18n/messages/de.json +28 -0
  180. package/templates/native-go/i18n/messages/en.json +28 -0
  181. package/templates/native-go/i18n/messages/es.json +28 -0
  182. package/templates/native-go/i18n/messages/fr.json +28 -0
  183. package/templates/native-go/i18n/messages/it.json +28 -0
  184. package/templates/native-go/i18n/messages/ja.json +28 -0
  185. package/templates/native-go/i18n/messages/ko.json +28 -0
  186. package/templates/native-go/i18n/messages/zh-CN.json +28 -0
  187. package/templates/native-go/i18n/messages/zh-TW.json +28 -0
  188. package/templates/native-go/ui/app.css +35 -0
  189. package/templates/native-go/ui/app.js +94 -0
  190. package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  191. package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  192. package/templates/native-go-better-auth/node/app.mjs +72 -0
  193. package/templates/native-go-better-auth/node/build.mjs +2 -0
  194. package/templates/native-go-better-auth/node/package-lock.json +862 -0
  195. package/templates/native-go-better-auth/node/package.json +1 -0
  196. package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  197. package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  198. package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  199. package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  200. package/templates/native-go-root/backend/internal/app/root.go +107 -0
  201. package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  202. package/templates/native-go-root/root/whoami.sh +6 -0
  203. package/templates/native-go-vue/README.md +110 -0
  204. package/templates/native-go-vue/frontend/DESIGN.md +78 -0
  205. package/templates/native-go-vue/frontend/package-lock.json +2507 -0
  206. package/templates/native-go-vue/frontend/package.json +6 -0
  207. package/templates/native-go-vue/frontend/src/App.vue +132 -0
  208. package/templates/native-go-vue/frontend/src/context.ts +17 -0
  209. package/templates/native-go-vue/frontend/src/main.ts +26 -0
  210. package/templates/native-go-vue/frontend/src/style.css +14 -0
  211. package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  212. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  213. package/templates/native-go-vue/frontend/vite.config.ts +8 -0
  214. package/templates/native-go-vue/gitignore +2 -0
  215. package/dist/attestation/app-package.d.ts +0 -86
  216. package/dist/attestation/app-package.js +0 -206
  217. package/dist/attestation/install-on-host.d.ts +0 -14
  218. package/dist/attestation/install-on-host.js +0 -52
  219. package/dist/attestation/keyring.d.ts +0 -21
  220. package/dist/attestation/keyring.js +0 -168
  221. package/dist/attestation/platform-service.d.ts +0 -70
  222. package/dist/attestation/platform-service.js +0 -250
  223. package/dist/build-spk/broker-glue.d.ts +0 -16
  224. package/dist/build-spk/broker-glue.js +0 -40
  225. package/dist/deploy/catalog-publish.d.ts +0 -3
  226. package/dist/deploy/catalog-publish.js +0 -7
  227. package/dist/doctor.d.ts +0 -22
  228. package/dist/doctor.js +0 -822
  229. package/dist/error-collector.d.ts +0 -2
  230. package/dist/error-collector.js +0 -46
  231. package/dist/host/broker-paths.d.ts +0 -1
  232. package/dist/host/broker-paths.js +0 -1
  233. package/dist/host/broker-upgrade-preflight.d.ts +0 -61
  234. package/dist/host/broker-upgrade-preflight.js +0 -156
  235. package/dist/host/digests.d.ts +0 -5
  236. package/dist/host/digests.js +0 -63
  237. package/dist/host/fake-nas.d.ts +0 -49
  238. package/dist/host/fake-nas.js +0 -193
  239. package/dist/mock-broker/bootstrap.d.ts +0 -2
  240. package/dist/mock-broker/bootstrap.js +0 -8
  241. package/dist/mock-broker/files.d.ts +0 -60
  242. package/dist/mock-broker/files.js +0 -408
  243. package/dist/mock-broker/oidc.d.ts +0 -31
  244. package/dist/mock-broker/oidc.js +0 -161
  245. package/dist/mock-broker/path.d.ts +0 -9
  246. package/dist/mock-broker/path.js +0 -50
  247. package/dist/mock-broker/server.d.ts +0 -19
  248. package/dist/mock-broker/server.js +0 -350
  249. package/dist/publish/build-ledger.d.ts +0 -22
  250. package/dist/publish/build-ledger.js +0 -26
  251. package/dist/publish/community-path.d.ts +0 -6
  252. package/dist/publish/community-path.js +0 -41
  253. package/dist/publish/curated-release.d.ts +0 -78
  254. package/dist/publish/curated-release.js +0 -883
  255. package/dist/publish/file-build-ledger.d.ts +0 -18
  256. package/dist/publish/file-build-ledger.js +0 -268
  257. package/dist/publish/github-pr.d.ts +0 -25
  258. package/dist/publish/github-pr.js +0 -85
  259. package/dist/publish/github-repo.d.ts +0 -8
  260. package/dist/publish/github-repo.js +0 -21
  261. package/dist/publish/platform-service-release.d.ts +0 -60
  262. package/dist/publish/platform-service-release.js +0 -333
  263. package/dist/publish/preflight.d.ts +0 -24
  264. package/dist/publish/preflight.js +0 -138
  265. package/dist/publish/publish.d.ts +0 -27
  266. package/dist/publish/publish.js +0 -136
  267. package/dist/publish/sync-definition.d.ts +0 -4
  268. package/dist/publish/sync-definition.js +0 -48
  269. package/dist/release-bin.d.ts +0 -2
  270. package/dist/release-bin.js +0 -23
  271. package/dist/templates/hello-web/html/sdk.js +0 -68
  272. package/dist/templates/hello-web-page/html/sdk.js +0 -68
  273. package/dist/templates/native/synology-app.yaml +0 -7
  274. package/dist/test-host.d.ts +0 -15
  275. package/dist/test-host.js +0 -37
  276. package/templates/hello-web/html/sdk.js +0 -68
  277. package/templates/hello-web-page/html/sdk.js +0 -68
  278. package/templates/native/synology-app.yaml +0 -7
  279. /package/dist/templates/{native → binary}/native.yaml +0 -0
  280. /package/templates/{native → binary}/native.yaml +0 -0
@@ -0,0 +1,242 @@
1
+ package helper
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "net"
7
+ "os"
8
+ "path/filepath"
9
+ "testing"
10
+ "time"
11
+ )
12
+
13
+ func staleSocket(t *testing.T, p string) {
14
+ l, err := net.ListenUnix("unix", &net.UnixAddr{Name: p, Net: "unix"})
15
+ if err != nil {
16
+ t.Fatal(err)
17
+ }
18
+ l.SetUnlinkOnClose(false)
19
+ l.Close()
20
+ }
21
+
22
+ type fakeProcesses struct {
23
+ current Process
24
+ stopped, started int
25
+ stopErr error
26
+ }
27
+
28
+ func (f *fakeProcesses) Stop(want Process) error {
29
+ if f.stopErr != nil {
30
+ return f.stopErr
31
+ }
32
+ if want != f.current {
33
+ return errTrust
34
+ }
35
+ f.stopped++
36
+ return nil
37
+ }
38
+ func (f *fakeProcesses) Start(Inventory) error { f.started++; return nil }
39
+
40
+ func controlFixture(t *testing.T) (Controller, *fakeProcesses) {
41
+ c := fixture(t)
42
+ p := Process{Inventory: c.Expected, Mode: "supervised", PID: 1234, Boot: "fixture-boot", Start: "100", Device: 1, Inode: 2}
43
+ f := &fakeProcesses{current: p}
44
+ ctl := Controller{Client: c, Processes: f}
45
+ record(t, c, Record{Inventory: c.Expected, Enabled: true})
46
+ if err := writeProcess(c.RunDir, p); err != nil {
47
+ t.Fatal(err)
48
+ }
49
+ return ctl, f
50
+ }
51
+
52
+ func TestTeardownAndRecoveryVerifyOwnedProcess(t *testing.T) {
53
+ for _, kind := range []string{"pid", "boot", "start", "inode", "digest", "package"} {
54
+ t.Run(kind, func(t *testing.T) {
55
+ c, f := controlFixture(t)
56
+ switch kind {
57
+ case "pid":
58
+ f.current.PID++
59
+ case "boot":
60
+ f.current.Boot = "new-boot"
61
+ case "start":
62
+ f.current.Start = "reused"
63
+ case "inode":
64
+ f.current.Inode++
65
+ case "digest":
66
+ f.current.Inventory.PayloadDigest = "foreign"
67
+ case "package":
68
+ f.current.Inventory.Package = "Other"
69
+ }
70
+ if c.Control(context.Background(), "recover") == nil || f.stopped != 0 || f.started != 0 {
71
+ t.Fatal("foreign process touched")
72
+ }
73
+ if _, err := os.Stat(filepath.Join(c.RunDir, "process.json")); err != nil {
74
+ t.Fatal("failure evidence removed")
75
+ }
76
+ })
77
+ }
78
+ c, f := controlFixture(t)
79
+ if err := c.Control(context.Background(), "teardown"); err != nil {
80
+ t.Fatal(err)
81
+ }
82
+ if f.stopped != 1 || f.started != 0 {
83
+ t.Fatal(f)
84
+ }
85
+ if _, err := os.Stat(filepath.Join(c.RunDir, "process.json")); !os.IsNotExist(err) {
86
+ t.Fatal(err)
87
+ }
88
+ if err := c.Control(context.Background(), "recover"); err != nil || f.started != 1 {
89
+ t.Fatalf("%v %+v", err, f)
90
+ }
91
+ }
92
+
93
+ func TestDisableIsDurableAndPrecedesFailedCleanup(t *testing.T) {
94
+ c, f := controlFixture(t)
95
+ f.stopErr = errors.New("fixture stop failed")
96
+ if c.Control(context.Background(), "disable") == nil {
97
+ t.Fatal("cleanup falsely succeeded")
98
+ }
99
+ r, err := readRecord(c.StateDir, c.Owner)
100
+ if err != nil || r.Enabled || !r.Residual {
101
+ t.Fatalf("%+v %v", r, err)
102
+ }
103
+ s := c.Status(context.Background())
104
+ if s.State != "not-enabled" || s.Cleanup != "required" {
105
+ t.Fatal(s)
106
+ }
107
+ if c.Control(context.Background(), "recover") == nil || f.started != 0 {
108
+ t.Fatal("old recovery resurrected disabled integration")
109
+ }
110
+ f.stopErr = nil
111
+ if err := c.Control(context.Background(), "disable"); err != nil {
112
+ t.Fatal(err)
113
+ }
114
+ r, _ = readRecord(c.StateDir, c.Owner)
115
+ if r.Enabled || r.Residual {
116
+ t.Fatal(r)
117
+ }
118
+ if err := c.Control(context.Background(), "enable"); err != nil || f.started != 1 {
119
+ t.Fatalf("%v %+v", err, f)
120
+ }
121
+ }
122
+
123
+ func TestMissingApprovalAndUnknownActionCannotStart(t *testing.T) {
124
+ c, f := controlFixture(t)
125
+ os.Remove(filepath.Join(c.StateDir, "approval.json"))
126
+ if c.Control(context.Background(), "recover") == nil {
127
+ t.Fatal("recover without approval")
128
+ }
129
+ if c.Control(context.Background(), "arbitrary-command") == nil || f.started != 0 {
130
+ t.Fatal("arbitrary action accepted")
131
+ }
132
+ }
133
+
134
+ func TestSocketResidueCannotUnlinkForeignFilesOrLiveListeners(t *testing.T) {
135
+ for _, kind := range []string{"file", "symlink", "live", "stale"} {
136
+ t.Run(kind, func(t *testing.T) {
137
+ c, f := controlFixture(t)
138
+ os.Remove(filepath.Join(c.RunDir, "process.json"))
139
+ p := filepath.Join(c.RunDir, "helper.sock")
140
+ switch kind {
141
+ case "file":
142
+ os.WriteFile(p, []byte("foreign"), 0600)
143
+ case "symlink":
144
+ os.Symlink("foreign", p)
145
+ case "live":
146
+ live(t, c.Client, c.Expected, nil)
147
+ case "stale":
148
+ staleSocket(t, p)
149
+ }
150
+ err := c.Control(context.Background(), "teardown")
151
+ if kind == "stale" {
152
+ if err != nil {
153
+ t.Fatal(err)
154
+ }
155
+ if _, err := os.Lstat(p); !os.IsNotExist(err) {
156
+ t.Fatal("stale socket not removed")
157
+ }
158
+ } else {
159
+ if err == nil {
160
+ t.Fatal("foreign residue accepted")
161
+ }
162
+ if _, err := os.Lstat(p); err != nil {
163
+ t.Fatal("foreign residue removed")
164
+ }
165
+ }
166
+ if f.stopped != 0 {
167
+ t.Fatal("unrecorded process signalled")
168
+ }
169
+ })
170
+ }
171
+ }
172
+
173
+ func TestLifecycleGateBlocksResidueAndAllowsCleanStoppedState(t *testing.T) {
174
+ c, _ := controlFixture(t)
175
+ if c.CheckStopped(context.Background()) == nil {
176
+ t.Fatal("process residue accepted")
177
+ }
178
+ if err := c.Control(context.Background(), "teardown"); err != nil {
179
+ t.Fatal(err)
180
+ }
181
+ if err := c.CheckStopped(context.Background()); err != nil {
182
+ t.Fatal(err)
183
+ }
184
+ record(t, c.Client, Record{Inventory: c.Expected, Enabled: false, Residual: true})
185
+ if c.CheckStopped(context.Background()) == nil {
186
+ t.Fatal("failed disable accepted")
187
+ }
188
+ }
189
+
190
+ func TestStartupProbesShareOneDeadline(t *testing.T) {
191
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
192
+ defer cancel()
193
+ err := waitHealthy(ctx, func(probeCtx context.Context) bool {
194
+ if probeCtx != ctx {
195
+ t.Fatal("probe got an independent deadline")
196
+ }
197
+ <-probeCtx.Done()
198
+ return false
199
+ })
200
+ if !errors.Is(err, context.DeadlineExceeded) {
201
+ t.Fatal(err)
202
+ }
203
+ if err := waitHealthy(context.Background(), func(context.Context) bool { return true }); err != nil {
204
+ t.Fatal(err)
205
+ }
206
+ }
207
+
208
+ func TestSupervisorSocketResidueAlsoBlocksLifecycle(t *testing.T) {
209
+ c, _ := controlFixture(t)
210
+ os.Remove(filepath.Join(c.RunDir, "process.json"))
211
+ p := filepath.Join(c.RunDir, "supervisor.sock")
212
+ l, err := net.ListenUnix("unix", &net.UnixAddr{Name: p, Net: "unix"})
213
+ if err != nil {
214
+ t.Fatal(err)
215
+ }
216
+ defer l.Close()
217
+ if c.CheckStopped(context.Background()) == nil {
218
+ t.Fatal("unidentified supervisor accepted")
219
+ }
220
+ if c.Control(context.Background(), "teardown") == nil {
221
+ t.Fatal("live supervisor ignored")
222
+ }
223
+ }
224
+
225
+ func TestOnlySupervisedProcessRecordsAreReadable(t *testing.T) {
226
+ for _, mode := range []string{"", "supervised", "unknown"} {
227
+ c, f := controlFixture(t)
228
+ p := f.current
229
+ p.Mode = mode
230
+ if err := writeProcess(c.RunDir, p); err != nil {
231
+ t.Fatal(err)
232
+ }
233
+ got, err := readProcess(c.RunDir, c.Owner)
234
+ if mode != "supervised" {
235
+ if err == nil {
236
+ t.Fatal("unsupervised process mode accepted")
237
+ }
238
+ } else if err != nil || got != p {
239
+ t.Fatalf("mode %q: %+v %v", mode, got, err)
240
+ }
241
+ }
242
+ }
@@ -0,0 +1,44 @@
1
+ package helper
2
+
3
+ import (
4
+ "net"
5
+ "syscall"
6
+ )
7
+
8
+ func peerCredentials(conn *net.UnixConn) (*syscall.Ucred, error) {
9
+ raw, err := conn.SyscallConn()
10
+ if err != nil {
11
+ return nil, err
12
+ }
13
+ var cred *syscall.Ucred
14
+ var sockErr error
15
+ err = raw.Control(func(fd uintptr) {
16
+ cred, sockErr = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
17
+ })
18
+ if err != nil {
19
+ return nil, err
20
+ }
21
+ if sockErr != nil {
22
+ return nil, sockErr
23
+ }
24
+ return cred, nil
25
+ }
26
+
27
+ func peerUID(conn *net.UnixConn) (uint32, error) {
28
+ cred, err := peerCredentials(conn)
29
+ if err != nil {
30
+ return 0, err
31
+ }
32
+ return cred.Uid, nil
33
+ }
34
+
35
+ func RootPeer(conn *net.UnixConn) error {
36
+ uid, err := peerUID(conn)
37
+ if err != nil {
38
+ return err
39
+ }
40
+ if uid != 0 {
41
+ return errTrust
42
+ }
43
+ return nil
44
+ }
@@ -0,0 +1,7 @@
1
+ //go:build !linux
2
+
3
+ package helper
4
+
5
+ import "net"
6
+
7
+ func RootPeer(*net.UnixConn) error { return errTrust }
@@ -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
+ }