@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,205 @@
1
+ package helper
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "net"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ )
12
+
13
+ func fixture(t *testing.T) Client {
14
+ t.Helper()
15
+ i := Inventory{Package: "Fixture", Version: "1.0.0-0001", Protocol: 1, PayloadDigest: strings.Repeat("a", 64), RegistryDigest: strings.Repeat("b", 64)}
16
+ run, err := os.MkdirTemp("/tmp", "m1a-")
17
+ if err != nil {
18
+ t.Fatal(err)
19
+ }
20
+ t.Cleanup(func() { os.RemoveAll(run) })
21
+ return Client{Expected: i, StateDir: t.TempDir(), RunDir: run, Owner: uint32(os.Getuid()), Peer: func(*net.UnixConn) error { return nil }}
22
+ }
23
+
24
+ func record(t *testing.T, c Client, r Record) {
25
+ t.Helper()
26
+ b, _ := json.Marshal(r)
27
+ if err := os.WriteFile(filepath.Join(c.StateDir, "approval.json"), b, 0644); err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ }
31
+
32
+ func live(t *testing.T, c Client, i Inventory, reply func(Request) Response) {
33
+ t.Helper()
34
+ l, err := net.ListenUnix("unix", &net.UnixAddr{Name: filepath.Join(c.RunDir, "helper.sock"), Net: "unix"})
35
+ if err != nil {
36
+ t.Fatal(err)
37
+ }
38
+ t.Cleanup(func() { l.Close() })
39
+ go func() {
40
+ for {
41
+ conn, err := l.AcceptUnix()
42
+ if err != nil {
43
+ return
44
+ }
45
+ go func() {
46
+ defer conn.Close()
47
+ var req Request
48
+ if json.NewDecoder(conn).Decode(&req) != nil {
49
+ return
50
+ }
51
+ r := Respond(i, false, req)
52
+ if reply != nil {
53
+ r = reply(req)
54
+ }
55
+ json.NewEncoder(conn).Encode(r)
56
+ }()
57
+ }
58
+ }()
59
+ }
60
+
61
+ func TestSixStatesAndPriority(t *testing.T) {
62
+ c := fixture(t)
63
+ check := func(want string) {
64
+ t.Helper()
65
+ s := c.Status(context.Background())
66
+ if s.State != want {
67
+ t.Fatalf("want %s, got %+v", want, s)
68
+ }
69
+ }
70
+ c.Expected = Inventory{}
71
+ check("not-enabled")
72
+ c.Expected = fixture(t).Expected
73
+ check("needs-approval")
74
+ record(t, c, Record{Inventory: c.Expected, Enabled: true})
75
+ check("stopped")
76
+ live(t, c, c.Expected, nil)
77
+ check("ready")
78
+ old := c.Expected
79
+ old.Protocol++
80
+ record(t, c, Record{Inventory: old, Enabled: true})
81
+ check("incompatible")
82
+ record(t, c, Record{Inventory: old, Enabled: false})
83
+ check("not-enabled")
84
+ }
85
+
86
+ func TestLiveProbeCannotBeReplacedByApprovalOrHistoricalOutput(t *testing.T) {
87
+ c := fixture(t)
88
+ record(t, c, Record{Inventory: c.Expected, Enabled: true})
89
+ if err := os.WriteFile(filepath.Join(c.RunDir, "helper.sock"), []byte("ready"), 0644); err != nil {
90
+ t.Fatal(err)
91
+ }
92
+ if got := c.Status(context.Background()).State; got != "incompatible" {
93
+ t.Fatal(got)
94
+ }
95
+ }
96
+
97
+ func TestHandshakeFailuresAreRedacted(t *testing.T) {
98
+ for _, kind := range []string{"nonce", "protocol", "payload", "worker", "version", "peer"} {
99
+ t.Run(kind, func(t *testing.T) {
100
+ c := fixture(t)
101
+ record(t, c, Record{Inventory: c.Expected, Enabled: true})
102
+ live(t, c, c.Expected, func(req Request) Response {
103
+ r := Respond(c.Expected, false, req)
104
+ switch kind {
105
+ case "nonce":
106
+ r.Nonce = "replay"
107
+ case "protocol":
108
+ r.Inventory.Protocol++
109
+ case "payload":
110
+ r.Inventory.PayloadDigest = "foreign"
111
+ case "worker":
112
+ r.Inventory.RegistryDigest = "foreign"
113
+ case "version":
114
+ r.Inventory.Version = "old"
115
+ }
116
+ return r
117
+ })
118
+ if kind == "peer" {
119
+ c.Peer = func(*net.UnixConn) error { return os.ErrPermission }
120
+ }
121
+ s := c.Status(context.Background())
122
+ if s.State != "incompatible" {
123
+ t.Fatalf("%+v", s)
124
+ }
125
+ b, _ := json.Marshal(s)
126
+ if strings.Contains(string(b), c.StateDir) || strings.Contains(string(b), "foreign") {
127
+ t.Fatal(string(b))
128
+ }
129
+ })
130
+ }
131
+ c := fixture(t)
132
+ record(t, c, Record{Inventory: c.Expected, Enabled: true})
133
+ live(t, c, c.Expected, func(req Request) Response { return Response{Inventory: c.Expected, Nonce: req.Nonce, Error: "failed"} })
134
+ if c.Status(context.Background()).State != "failed" {
135
+ t.Fatal("health failure must be failed")
136
+ }
137
+ }
138
+
139
+ func TestUntrustedPersistentStateFailsClosed(t *testing.T) {
140
+ for _, kind := range []string{"writable", "symlink", "malformed"} {
141
+ t.Run(kind, func(t *testing.T) {
142
+ c := fixture(t)
143
+ record(t, c, Record{Inventory: c.Expected, Enabled: true})
144
+ p := filepath.Join(c.StateDir, "approval.json")
145
+ switch kind {
146
+ case "writable":
147
+ os.Chmod(p, 0666)
148
+ case "symlink":
149
+ os.Rename(p, p+".real")
150
+ os.Symlink(p+".real", p)
151
+ case "malformed":
152
+ os.WriteFile(p, []byte(`{"enabled":false}`), 0644)
153
+ }
154
+ if c.Status(context.Background()).State != "incompatible" {
155
+ t.Fatal("untrusted state accepted")
156
+ }
157
+ })
158
+ }
159
+ }
160
+
161
+ func TestOldHelperRejectsNewInventoryAndAllUnimplementedWork(t *testing.T) {
162
+ i := fixture(t).Expected
163
+ for _, field := range []string{"protocol", "payload", "workers", "version", "package"} {
164
+ new := i
165
+ switch field {
166
+ case "protocol":
167
+ new.Protocol++
168
+ case "payload":
169
+ new.PayloadDigest = strings.Repeat("c", 64)
170
+ case "workers":
171
+ new.RegistryDigest = strings.Repeat("c", 64)
172
+ case "version":
173
+ new.Version = "2.0.0-0001"
174
+ case "package":
175
+ new.Package = "Other"
176
+ }
177
+ r := Respond(i, false, Request{Inventory: new, Op: "work", Nonce: "test"})
178
+ if r.Error != "incompatible" || r.OK {
179
+ t.Fatalf("%s: %+v", field, r)
180
+ }
181
+ }
182
+ if r := Respond(i, true, Request{Inventory: i, Op: "health"}); r.OK || r.Error != "disabled" {
183
+ t.Fatal(r)
184
+ }
185
+ if r := Respond(i, false, Request{Inventory: i, Op: "work"}); r.OK || r.Error != "unsupported" {
186
+ t.Fatal(r)
187
+ }
188
+ }
189
+
190
+ func TestMissingEnableFieldIsNotARevocationRecord(t *testing.T) {
191
+ c := fixture(t)
192
+ b, _ := json.Marshal(map[string]any{"inventory": c.Expected})
193
+ os.WriteFile(filepath.Join(c.StateDir, "approval.json"), b, 0644)
194
+ if c.Status(context.Background()).State != "incompatible" {
195
+ t.Fatal("incomplete approval treated as trusted disable")
196
+ }
197
+ }
198
+
199
+ func TestProcessRecordIsNotReadableByOrdinaryOSUsers(t *testing.T) {
200
+ c, _ := controlFixture(t)
201
+ s, err := os.Stat(filepath.Join(c.RunDir, "process.json"))
202
+ if err != nil || s.Mode().Perm() != 0600 {
203
+ t.Fatalf("process record permissions: %v %v", s, err)
204
+ }
205
+ }
@@ -0,0 +1,84 @@
1
+ package helper
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "io"
8
+ "syscall"
9
+ "time"
10
+ )
11
+
12
+ // Also runs after exec: the parent can die between fork's prctl and exec.
13
+ func armParentDeath(expected int, set func() error, parent func() int) error {
14
+ if expected <= 1 || parent() != expected {
15
+ return errTrust
16
+ }
17
+ if err := set(); err != nil {
18
+ return err
19
+ }
20
+ if parent() != expected {
21
+ return errTrust
22
+ }
23
+ return nil
24
+ }
25
+
26
+ type supervisedChild interface {
27
+ exited() (bool, error)
28
+ signal(syscall.Signal) error
29
+ }
30
+
31
+ // Caller is the sole reaper and signal owner. A zombie retains its PID until
32
+ // exited() reaps it; a background Wait goroutine would invalidate this proof.
33
+ func stopChild(ctx context.Context, child supervisedChild, grace time.Duration) error {
34
+ if err := ctx.Err(); err != nil {
35
+ return err
36
+ }
37
+ done, err := child.exited()
38
+ if err != nil || done {
39
+ return err
40
+ }
41
+ if err := child.signal(syscall.SIGTERM); err != nil {
42
+ return err
43
+ }
44
+ escalate := time.Now().Add(grace)
45
+ killed := false
46
+ for {
47
+ if err := ctx.Err(); err != nil {
48
+ return err
49
+ }
50
+ done, err := child.exited()
51
+ if err != nil || done {
52
+ return err
53
+ }
54
+ if !killed && !time.Now().Before(escalate) {
55
+ if err := child.signal(syscall.SIGKILL); err != nil {
56
+ return err
57
+ }
58
+ killed = true
59
+ }
60
+ timer := time.NewTimer(time.Millisecond)
61
+ select {
62
+ case <-ctx.Done():
63
+ timer.Stop()
64
+ return ctx.Err()
65
+ case <-timer.C:
66
+ }
67
+ }
68
+ }
69
+
70
+ // Read to actual connection EOF. A LimitReader's synthetic EOF at the size
71
+ // cap can never certify supervisor exit, so reaching that cap is a refusal.
72
+ func readSupervisorReply(reader io.Reader, want Inventory, nonce string) error {
73
+ b, err := io.ReadAll(io.LimitReader(reader, 4097))
74
+ if err != nil || len(b) > 4096 {
75
+ return errTrust
76
+ }
77
+ var res Response
78
+ d := json.NewDecoder(bytes.NewReader(b))
79
+ d.DisallowUnknownFields()
80
+ if d.Decode(&res) != nil || d.Decode(new(any)) != io.EOF || !res.OK || res.Error != "" || res.Inventory != want || res.Nonce != nonce {
81
+ return errTrust
82
+ }
83
+ return nil
84
+ }
@@ -0,0 +1,124 @@
1
+ package helper
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "strings"
8
+ "syscall"
9
+ "testing"
10
+ "time"
11
+ )
12
+
13
+ func TestParentDeathSetupChecksBeforeAndAfterPrctl(t *testing.T) {
14
+ for _, tc := range []struct {
15
+ before, after int
16
+ setErr error
17
+ ok bool
18
+ }{
19
+ {42, 42, nil, true}, {1, 1, nil, false}, {42, 1, nil, false}, {42, 42, syscall.EPERM, false},
20
+ } {
21
+ calls, sets := 0, 0
22
+ err := armParentDeath(42, func() error { sets++; return tc.setErr }, func() int {
23
+ calls++
24
+ if calls == 1 {
25
+ return tc.before
26
+ }
27
+ return tc.after
28
+ })
29
+ if (err == nil) != tc.ok {
30
+ t.Fatalf("%+v: %v", tc, err)
31
+ }
32
+ if tc.before != 42 && sets != 0 {
33
+ t.Fatal("already reparented child armed")
34
+ }
35
+ if tc.ok && calls != 2 {
36
+ t.Fatal("parent race not checked")
37
+ }
38
+ }
39
+ }
40
+
41
+ type childFixture struct {
42
+ reaped bool
43
+ err error
44
+ signals []syscall.Signal
45
+ exitOn syscall.Signal
46
+ }
47
+
48
+ func (c *childFixture) exited() (bool, error) { return c.reaped, c.err }
49
+ func (c *childFixture) signal(s syscall.Signal) error {
50
+ if c.reaped {
51
+ panic("signalled a reaped child")
52
+ }
53
+ c.signals = append(c.signals, s)
54
+ if s == c.exitOn {
55
+ c.reaped = true
56
+ }
57
+ return nil
58
+ }
59
+ func TestOnlyUnreapedOwnedChildCanBeSignalled(t *testing.T) {
60
+ for _, tc := range []struct {
61
+ reaped bool
62
+ err error
63
+ exit syscall.Signal
64
+ count int
65
+ ok bool
66
+ }{
67
+ {true, nil, 0, 0, true}, {false, syscall.ECHILD, 0, 0, false}, {false, nil, syscall.SIGTERM, 1, true}, {false, nil, syscall.SIGKILL, 2, true}, {false, nil, 0, 2, false},
68
+ } {
69
+ c := &childFixture{reaped: tc.reaped, err: tc.err, exitOn: tc.exit}
70
+ ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
71
+ err := stopChild(ctx, c, time.Millisecond)
72
+ cancel()
73
+ if (err == nil) != tc.ok || len(c.signals) != tc.count {
74
+ t.Fatalf("%+v: err=%v signals=%v", tc, err, c.signals)
75
+ }
76
+ if c.reaped {
77
+ n := len(c.signals)
78
+ if stopChild(context.Background(), c, time.Millisecond) != nil || len(c.signals) != n {
79
+ t.Fatal("reaped PID reused")
80
+ }
81
+ }
82
+ }
83
+ }
84
+ func TestCancelledStopCannotSignal(t *testing.T) {
85
+ ctx, cancel := context.WithCancel(context.Background())
86
+ cancel()
87
+ c := &childFixture{}
88
+ if stopChild(ctx, c, time.Millisecond) == nil || len(c.signals) != 0 {
89
+ t.Fatal("cancelled operation signalled")
90
+ }
91
+ }
92
+
93
+ func TestSupervisorReplyRequiresRealEOFAndStrictBoundedAcknowledgement(t *testing.T) {
94
+ i := fixture(t).Expected
95
+ nonce := strings.Repeat("c", 64)
96
+ good, _ := json.Marshal(Response{Inventory: i, Nonce: nonce, OK: true})
97
+ for _, tc := range []struct {
98
+ data []byte
99
+ ok bool
100
+ }{
101
+ {good, true}, {append(append([]byte{}, good...), []byte(" {}")...), false},
102
+ {bytes.Replace(good, []byte(nonce), []byte(strings.Repeat("d", 64)), 1), false},
103
+ {append(append([]byte{}, good[:len(good)-1]...), []byte(",\"unknown\":true}")...), false},
104
+ {append(append([]byte{}, good...), bytes.Repeat([]byte(" "), 4097)...), false},
105
+ } {
106
+ if (readSupervisorReply(bytes.NewReader(tc.data), i, nonce) == nil) != tc.ok {
107
+ t.Fatalf("reply accepted=%v expected=%v", !tc.ok, tc.ok)
108
+ }
109
+ }
110
+ if readSupervisorReply(&brokenReplyReader{data: good}, i, nonce) == nil {
111
+ t.Fatal("read error treated as cooperative exit")
112
+ }
113
+ }
114
+
115
+ type brokenReplyReader struct{ data []byte }
116
+
117
+ func (r *brokenReplyReader) Read(p []byte) (int, error) {
118
+ if len(r.data) == 0 {
119
+ return 0, syscall.ECONNRESET
120
+ }
121
+ n := copy(p, r.data)
122
+ r.data = r.data[n:]
123
+ return n, nil
124
+ }
@@ -0,0 +1,192 @@
1
+ package helper
2
+
3
+ import (
4
+ "context"
5
+ "crypto/rand"
6
+ "encoding/hex"
7
+ "encoding/json"
8
+ "errors"
9
+ "io"
10
+ "net"
11
+ "os"
12
+ "os/exec"
13
+ "path/filepath"
14
+ "runtime"
15
+ "strconv"
16
+ "syscall"
17
+ "time"
18
+ )
19
+
20
+ func setParentDeath() error {
21
+ _, _, err := syscall.Syscall6(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0, 0, 0, 0)
22
+ if err != 0 {
23
+ return err
24
+ }
25
+ return nil
26
+ }
27
+
28
+ type ownedLinuxChild struct {
29
+ process *os.Process
30
+ reaped bool
31
+ }
32
+
33
+ func (c *ownedLinuxChild) exited() (bool, error) {
34
+ if c.reaped {
35
+ return true, nil
36
+ }
37
+ var status syscall.WaitStatus
38
+ pid, err := syscall.Wait4(c.process.Pid, &status, syscall.WNOHANG, nil)
39
+ if errors.Is(err, syscall.EINTR) {
40
+ return false, nil
41
+ }
42
+ if err != nil {
43
+ return false, err
44
+ } // ECHILD never permits signalling.
45
+ if pid == c.process.Pid {
46
+ c.reaped = true
47
+ c.process.Release()
48
+ }
49
+ return c.reaped, nil
50
+ }
51
+ func (c *ownedLinuxChild) signal(s syscall.Signal) error {
52
+ if c.reaped {
53
+ return errTrust
54
+ }
55
+ // This PID came from our own Start, and ONLY this event loop calls Wait4.
56
+ // Even if it exits here it remains an unreaped zombie, pinning its PID.
57
+ return syscall.Kill(c.process.Pid, s)
58
+ }
59
+
60
+ type supervisorRequest struct {
61
+ Process Process `json:"process"`
62
+ Op string `json:"op"`
63
+ Nonce string `json:"nonce"`
64
+ }
65
+
66
+ // The thread creating the child must remain alive: PDEATHSIG is tied to that
67
+ // Linux thread, not merely the supervisor's thread group / Go process.
68
+ func superviseLinux(c Client, binary string) error {
69
+ runtime.LockOSThread()
70
+ defer runtime.UnlockOSThread()
71
+ r, err := readRecord(c.StateDir, 0)
72
+ if err != nil || !r.Enabled || r.Inventory != c.Expected {
73
+ return errTrust
74
+ }
75
+ identity, err := inspectSupervisor(os.Getpid(), c.Expected)
76
+ if err != nil {
77
+ return err
78
+ }
79
+ if err := writeProcess(c.RunDir, identity); err != nil {
80
+ return err
81
+ }
82
+ socket := filepath.Join(c.RunDir, "supervisor.sock")
83
+ l, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
84
+ if err != nil {
85
+ return err
86
+ }
87
+ l.SetUnlinkOnClose(false)
88
+ defer l.Close()
89
+ if err := os.Chmod(socket, 0600); err != nil {
90
+ return err
91
+ }
92
+ cmd := exec.Command(binary, "serve-child")
93
+ cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "SYNOPKGLAND_PARENT=" + strconv.Itoa(os.Getpid())}
94
+ // Go sets prctl before exec and rechecks the parent immediately afterward.
95
+ cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
96
+ if err := cmd.Start(); err != nil {
97
+ return err
98
+ }
99
+ child := &ownedLinuxChild{process: cmd.Process}
100
+ // No cmd.Wait goroutine: reaping and signalling are serialized below.
101
+ for {
102
+ if _, err := child.exited(); err != nil {
103
+ return err
104
+ }
105
+ l.SetDeadline(time.Now().Add(20 * time.Millisecond))
106
+ conn, err := l.AcceptUnix()
107
+ if err != nil {
108
+ if e, ok := err.(net.Error); ok && e.Timeout() {
109
+ continue
110
+ }
111
+ return err
112
+ }
113
+ conn.SetDeadline(time.Now().Add(3 * time.Second))
114
+ peer, err := peerCredentials(conn)
115
+ if err != nil || peer.Uid != 0 {
116
+ conn.Close()
117
+ continue
118
+ }
119
+ var req supervisorRequest
120
+ d := json.NewDecoder(io.LimitReader(conn, 4097))
121
+ d.DisallowUnknownFields()
122
+ if d.Decode(&req) != nil || req.Process != identity || req.Op != "stop" || len(req.Nonce) != 64 || !digestPattern.MatchString(req.Nonce) {
123
+ conn.Close()
124
+ continue
125
+ }
126
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
127
+ err = stopChild(ctx, child, time.Second)
128
+ cancel()
129
+ // Kernel process-exit fd cleanup is not ordered: close the listener
130
+ // before ACK, so connection EOF cannot race a still-listening socket.
131
+ if err == nil {
132
+ err = l.Close()
133
+ }
134
+ res := Response{Inventory: c.Expected, Nonce: req.Nonce, OK: err == nil}
135
+ if err != nil {
136
+ res.Error = "cleanup-required"
137
+ }
138
+ json.NewEncoder(conn).Encode(res)
139
+ if err == nil {
140
+ // Keep this connection open until OS exit. An ACK plus EOF proves both
141
+ // that the child was reaped and this supervisor finished cooperatively.
142
+ os.Exit(0)
143
+ }
144
+ conn.Close()
145
+ }
146
+ }
147
+
148
+ func (p LinuxProcesses) Stop(want Process) error {
149
+ if want.Mode != "supervised" {
150
+ return errTrust
151
+ }
152
+ // Loss/reboot/PID reuse is not sufficient proof of child teardown. Preserve
153
+ // the record for fixed admin repair; never signal this recorded PID.
154
+ actual, err := inspectSupervisor(want.PID, want.Inventory)
155
+ if err != nil || actual != want {
156
+ return errTrust
157
+ }
158
+ socket := filepath.Join(p.RunDir, "supervisor.sock")
159
+ st, err := os.Lstat(socket)
160
+ if err != nil {
161
+ return err
162
+ }
163
+ stat, ok := st.Sys().(*syscall.Stat_t)
164
+ if !ok || stat.Uid != 0 || st.Mode()&os.ModeSocket == 0 || st.Mode().Perm() != 0600 {
165
+ return errTrust
166
+ }
167
+ conn, err := net.DialTimeout("unix", socket, time.Second)
168
+ if err != nil {
169
+ return err
170
+ }
171
+ defer conn.Close()
172
+ peer, err := peerCredentials(conn.(*net.UnixConn))
173
+ if err != nil || peer.Uid != 0 || int(peer.Pid) != want.PID {
174
+ return errTrust
175
+ }
176
+ // Recheck after binding the connection too. A connected socket cannot be
177
+ // substituted by a later process that reused the path or PID.
178
+ actual, err = inspectSupervisor(want.PID, want.Inventory)
179
+ if err != nil || actual != want {
180
+ return errTrust
181
+ }
182
+ conn.SetDeadline(time.Now().Add(3 * time.Second))
183
+ var nonce [32]byte
184
+ if _, err := rand.Read(nonce[:]); err != nil {
185
+ return err
186
+ }
187
+ req := supervisorRequest{Process: want, Op: "stop", Nonce: hex.EncodeToString(nonce[:])}
188
+ if err := json.NewEncoder(conn).Encode(req); err != nil {
189
+ return err
190
+ }
191
+ return readSupervisorReply(conn, want.Inventory, req.Nonce)
192
+ }