@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc

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 (230) hide show
  1. package/LICENSE-EXCEPTION +26 -0
  2. package/README.md +34 -0
  3. package/dist/chunk-3R6KEFOI.js +22 -0
  4. package/dist/chunk-67BDAMYX.js +376 -0
  5. package/dist/chunk-AITGS27R.js +139 -0
  6. package/dist/chunk-CMAEHQ4F.js +6 -0
  7. package/dist/chunk-MLKGABMK.js +9 -0
  8. package/dist/chunk-O3UIK34B.js +1430 -0
  9. package/dist/chunk-VHTUSZUX.js +176 -0
  10. package/dist/cli.js +9006 -0
  11. package/dist/docker.js +54 -0
  12. package/dist/index.js +1053 -0
  13. package/dist/ingress.js +436 -0
  14. package/dist/jsx-runtime.js +2 -0
  15. package/dist/preact.js +22 -0
  16. package/dist/service-url.js +11 -0
  17. package/dist/services.js +213 -0
  18. package/dist/test.js +5069 -0
  19. package/dist/ui.js +254 -0
  20. package/dist/worker-protocol.js +89 -0
  21. package/package.json +112 -0
  22. package/src/app-icon.ts +74 -0
  23. package/src/apply.ts +52 -0
  24. package/src/bundle-ui.ts +211 -0
  25. package/src/bundle.ts +546 -0
  26. package/src/cli/build-dev-landing.ts +84 -0
  27. package/src/cli/build-install-panel.ts +90 -0
  28. package/src/cli/build-runtime.ts +133 -0
  29. package/src/cli/build.ts +63 -0
  30. package/src/cli/dev.ts +107 -0
  31. package/src/cli/down.ts +60 -0
  32. package/src/cli/main.ts +56 -0
  33. package/src/cli/test.ts +61 -0
  34. package/src/cli/validate.ts +37 -0
  35. package/src/compile.ts +100 -0
  36. package/src/create-app.ts +982 -0
  37. package/src/deep-merge.ts +33 -0
  38. package/src/dev/fixture.ts +48 -0
  39. package/src/dev/install-handler.ts +383 -0
  40. package/src/dev/server.ts +968 -0
  41. package/src/dev/stack.ts +108 -0
  42. package/src/dev/state.ts +159 -0
  43. package/src/docker.ts +96 -0
  44. package/src/errors.ts +61 -0
  45. package/src/factory.ts +481 -0
  46. package/src/garage.ts +56 -0
  47. package/src/generator/hash.ts +6 -0
  48. package/src/generator/resolve-refs.ts +207 -0
  49. package/src/grants-builder.ts +100 -0
  50. package/src/grants.ts +260 -0
  51. package/src/home-api.ts +172 -0
  52. package/src/home-cookie.ts +10 -0
  53. package/src/home-registry.ts +120 -0
  54. package/src/home.ts +187 -0
  55. package/src/host-registry.ts +143 -0
  56. package/src/http-header.ts +20 -0
  57. package/src/identifiers.ts +49 -0
  58. package/src/index.ts +66 -0
  59. package/src/info.ts +64 -0
  60. package/src/ini.ts +50 -0
  61. package/src/json.ts +14 -0
  62. package/src/jsx-runtime/index.ts +5 -0
  63. package/src/keys.ts +29 -0
  64. package/src/mysql.ts +56 -0
  65. package/src/pack-build.ts +108 -0
  66. package/src/pack.ts +115 -0
  67. package/src/peer-registry.ts +121 -0
  68. package/src/peer-world.ts +117 -0
  69. package/src/permission-meta.ts +116 -0
  70. package/src/permissions.ts +108 -0
  71. package/src/pkg-root.ts +34 -0
  72. package/src/postgres.ts +79 -0
  73. package/src/preact/index.ts +27 -0
  74. package/src/redis.ts +43 -0
  75. package/src/ref.ts +271 -0
  76. package/src/remote-registry.ts +33 -0
  77. package/src/required-host-kinds.ts +110 -0
  78. package/src/resolve-context.ts +111 -0
  79. package/src/resolve.ts +471 -0
  80. package/src/runtime/docker/compose/compose-schema.ts +118 -0
  81. package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
  82. package/src/runtime/docker/compose/default-hardening.ts +21 -0
  83. package/src/runtime/docker/compose/effects.ts +107 -0
  84. package/src/runtime/docker/compose/index.ts +17 -0
  85. package/src/runtime/docker/compose/labels.ts +39 -0
  86. package/src/runtime/docker/compose/types.ts +78 -0
  87. package/src/runtime/docker/home-proxy-server/index.ts +177 -0
  88. package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
  89. package/src/runtime/docker/ingress/build-config.ts +819 -0
  90. package/src/runtime/docker/ingress/index.ts +16 -0
  91. package/src/runtime/docker/ingress/types.ts +182 -0
  92. package/src/runtime/docker/log-frames.ts +121 -0
  93. package/src/runtime/docker/logs-server/index.ts +366 -0
  94. package/src/runtime/docker/spawn-server/context-key.ts +82 -0
  95. package/src/runtime/docker/spawn-server/index.ts +492 -0
  96. package/src/runtime/docker/spawn-server/protocol.ts +70 -0
  97. package/src/runtime/docker/spawn-server/spec.ts +122 -0
  98. package/src/runtime/docker/stats-server/disks.ts +161 -0
  99. package/src/runtime/docker/stats-server/index.ts +322 -0
  100. package/src/runtime/home-client/index.ts +71 -0
  101. package/src/runtime/log-line.ts +24 -0
  102. package/src/runtime/logs-client/index.ts +175 -0
  103. package/src/runtime/spawn-client/index.ts +93 -0
  104. package/src/runtime/spawn-client/kaniko.ts +79 -0
  105. package/src/runtime/stats-client/index.ts +65 -0
  106. package/src/runtime/stats-sample.ts +95 -0
  107. package/src/safe-json.ts +37 -0
  108. package/src/sandbox.ts +1209 -0
  109. package/src/schema/build-apps.ts +38 -0
  110. package/src/schema/home.ts +217 -0
  111. package/src/schema/host.ts +792 -0
  112. package/src/schema/inputs.ts +26 -0
  113. package/src/schema/nodes.ts +75 -0
  114. package/src/schema/peer.ts +400 -0
  115. package/src/schema/public-domain.ts +116 -0
  116. package/src/schema/remote.ts +74 -0
  117. package/src/security.ts +594 -0
  118. package/src/serialize.ts +21 -0
  119. package/src/service-definition.ts +284 -0
  120. package/src/service-url.ts +64 -0
  121. package/src/service.ts +39 -0
  122. package/src/services/index.ts +5 -0
  123. package/src/shared.ts +65 -0
  124. package/src/speculative-silo.ts +96 -0
  125. package/src/stable-secret.ts +14 -0
  126. package/src/surfaces.ts +264 -0
  127. package/src/tailwind-bin.ts +56 -0
  128. package/src/test/boot.ts +192 -0
  129. package/src/test/compile.ts +501 -0
  130. package/src/test/describe.ts +117 -0
  131. package/src/test/index.ts +11 -0
  132. package/src/test/ingress-fixture.ts +37 -0
  133. package/src/test/types.ts +318 -0
  134. package/src/trusted-headers.ts +79 -0
  135. package/src/types.ts +126 -0
  136. package/src/ui/components/aside.tsx +96 -0
  137. package/src/ui/components/badge.tsx +42 -0
  138. package/src/ui/components/button.tsx +59 -0
  139. package/src/ui/components/card.tsx +106 -0
  140. package/src/ui/components/checkbox.tsx +37 -0
  141. package/src/ui/components/code-block.tsx +29 -0
  142. package/src/ui/components/combobox.tsx +119 -0
  143. package/src/ui/components/config-field.tsx +284 -0
  144. package/src/ui/components/dialog.tsx +137 -0
  145. package/src/ui/components/domain-editor.tsx +317 -0
  146. package/src/ui/components/domain-list.ts +205 -0
  147. package/src/ui/components/dropdown-menu.tsx +73 -0
  148. package/src/ui/components/empty-state.tsx +51 -0
  149. package/src/ui/components/highlight.ts +59 -0
  150. package/src/ui/components/icons.tsx +101 -0
  151. package/src/ui/components/image-posture.tsx +85 -0
  152. package/src/ui/components/input.tsx +28 -0
  153. package/src/ui/components/label.tsx +16 -0
  154. package/src/ui/components/line-chart.tsx +113 -0
  155. package/src/ui/components/page.tsx +39 -0
  156. package/src/ui/components/permission-row.tsx +262 -0
  157. package/src/ui/components/permission-section.tsx +51 -0
  158. package/src/ui/components/pie-chart.tsx +245 -0
  159. package/src/ui/components/schema-block.tsx +60 -0
  160. package/src/ui/components/separator.tsx +42 -0
  161. package/src/ui/components/sheet.tsx +130 -0
  162. package/src/ui/components/sidebar.tsx +650 -0
  163. package/src/ui/components/silo-picker.tsx +510 -0
  164. package/src/ui/components/skeleton.tsx +13 -0
  165. package/src/ui/components/sonner.tsx +33 -0
  166. package/src/ui/components/switch.tsx +58 -0
  167. package/src/ui/components/table-view.ts +219 -0
  168. package/src/ui/components/table.tsx +506 -0
  169. package/src/ui/components/tabs.tsx +59 -0
  170. package/src/ui/components/timeago.tsx +84 -0
  171. package/src/ui/components/tooltip.tsx +56 -0
  172. package/src/ui/components.json +21 -0
  173. package/src/ui/dev/dev-shell.tsx +145 -0
  174. package/src/ui/home/home-grid.tsx +177 -0
  175. package/src/ui/hooks/use-mobile.ts +19 -0
  176. package/src/ui/index.ts +189 -0
  177. package/src/ui/install/adapter.ts +193 -0
  178. package/src/ui/install/deploy-targets-picker.tsx +185 -0
  179. package/src/ui/install/install-app-panel.tsx +912 -0
  180. package/src/ui/install/install-error.tsx +62 -0
  181. package/src/ui/install/perms-inactive.ts +50 -0
  182. package/src/ui/install/required-fields.ts +37 -0
  183. package/src/ui/install/tabs-form.tsx +557 -0
  184. package/src/ui/tokens.css +279 -0
  185. package/src/ui/utils.ts +78 -0
  186. package/src/ui-error.ts +139 -0
  187. package/src/ui-runtime/dev-landing-entry.tsx +258 -0
  188. package/src/ui-runtime/index.ts +83 -0
  189. package/src/ui-runtime/install-entry.tsx +119 -0
  190. package/src/ui-runtime/mount.ts +367 -0
  191. package/src/ui-runtime/runtime.css +56 -0
  192. package/src/ui-surface.ts +451 -0
  193. package/src/valkey.ts +43 -0
  194. package/src/worker/extract-tar.ts +125 -0
  195. package/src/worker/identity.ts +183 -0
  196. package/src/worker/index.ts +16 -0
  197. package/src/worker/public-ip.ts +92 -0
  198. package/src/worker/queue.ts +54 -0
  199. package/src/worker/reconcile.ts +127 -0
  200. package/src/worker/start-worker.ts +227 -0
  201. package/src/worker/ui-proxy.ts +335 -0
  202. package/src/worker/uplink.ts +523 -0
  203. package/src/worker/validate.ts +34 -0
  204. package/src/worker/verify-relay.ts +102 -0
  205. package/src/worker-protocol/app-dirs.ts +94 -0
  206. package/src/worker-protocol/home-proxy-paths.ts +76 -0
  207. package/src/worker-protocol/index.ts +55 -0
  208. package/src/worker-protocol/ingress.ts +166 -0
  209. package/src/worker-protocol/logs-socket-path.ts +50 -0
  210. package/src/worker-protocol/networks.ts +39 -0
  211. package/src/worker-protocol/node-version.ts +70 -0
  212. package/src/worker-protocol/silo-paths.ts +114 -0
  213. package/src/worker-protocol/spawn-paths.ts +165 -0
  214. package/src/worker-protocol/stats-socket-path.ts +40 -0
  215. package/src/worker-protocol/update-files.ts +77 -0
  216. package/src/worker-protocol/uplink/constants.ts +86 -0
  217. package/src/worker-protocol/uplink/errors.ts +61 -0
  218. package/src/worker-protocol/uplink/frame.ts +114 -0
  219. package/src/worker-protocol/uplink/identity.ts +40 -0
  220. package/src/worker-protocol/uplink/index.ts +63 -0
  221. package/src/worker-protocol/uplink/methods.ts +462 -0
  222. package/src/worker-protocol/uplink/mux.ts +646 -0
  223. package/src/worker-protocol/uplink/protocol.ts +11 -0
  224. package/src/worker-protocol/uplink/rpc.ts +309 -0
  225. package/src/worker-protocol/uplink/session.ts +540 -0
  226. package/src/worker-protocol/uplink/test-support.ts +188 -0
  227. package/src/worker-protocol/worker-mount.ts +27 -0
  228. package/src/worker-protocol/worker-state.ts +190 -0
  229. package/src/xml.ts +49 -0
  230. package/src/yaml.ts +15 -0
@@ -0,0 +1,278 @@
1
+ import {
2
+ REALM_APP_DIR_GID,
3
+ isPreparedAppDir,
4
+ scopeBindSource,
5
+ type WorkerApp,
6
+ type WorkerState,
7
+ } from "../../../worker-protocol/index.ts";
8
+ import { HARDENING } from "./default-hardening.ts";
9
+ import { buildContainerLabels } from "./labels.ts";
10
+ import { EFFECTS, type EffectContext } from "./effects.ts";
11
+ import type {
12
+ ComposeConfig,
13
+ ComposeDeploy,
14
+ ComposeHealthcheck,
15
+ ComposeNetworkDef,
16
+ ComposeService,
17
+ ComposeVolume,
18
+ } from "./types.ts";
19
+
20
+ /**
21
+ * One app's rendered compose project. The worker runs each of these as
22
+ * its own `docker compose --project-name realm_<appId>` up, in the order
23
+ * returned by `convertToDockerCompose` (providers before consumers).
24
+ */
25
+ export interface AppComposeProject {
26
+ appId: string;
27
+ compose: ComposeConfig;
28
+ }
29
+
30
+ export interface ConvertOptions {
31
+ /**
32
+ * Worker-host timezone (e.g. `"Europe/Stockholm"`), typically read
33
+ * from `/etc/timezone` by the worker spine. Stamped as `TZ=` on
34
+ * every service that doesn't already define its own `TZ` env. Apps
35
+ * win: setting `env.TZ` in realm.ts opts out for that service.
36
+ */
37
+ tz?: string;
38
+ /**
39
+ * Platform string (`linux/amd64`, `linux/arm64`) stamped on every
40
+ * service. Suppresses Docker's "platform mismatch" warning on hosts
41
+ * that emulate (e.g. Apple Silicon running amd64 images via Rosetta).
42
+ * Empty/unset omits the field, leaving Docker to pick.
43
+ */
44
+ platform?: string;
45
+ }
46
+
47
+ /**
48
+ * Render a WorkerState — runtime-neutral, post-resolution
49
+ * data — into one docker-compose project per app. The worker runs each
50
+ * project under its own `--project-name realm_<appId>`, sequentially, in
51
+ * the returned order: providers before the apps that consume them.
52
+ *
53
+ * The renderer trusts the wire: refs are already walked, image digests
54
+ * are already pinned, cross-app `dependsOn` edges are pre-stitched. No
55
+ * grant checks, no peer-registry walks. Future runtimes
56
+ * (`convertToQuadlet`, `convertToKube`) operate on the same wire.
57
+ *
58
+ * Cross-app `depends_on` is NOT emitted into compose — it can't span
59
+ * projects. The topological order returned here carries that ordering
60
+ * instead; intra-app readiness still rides `restart: on-failure`.
61
+ */
62
+ export function convertToDockerCompose(
63
+ state: WorkerState,
64
+ opts: ConvertOptions = {},
65
+ ): AppComposeProject[] {
66
+ return topoSortApps(state.apps).map((app) => {
67
+ const services: Record<string, ComposeService> = {};
68
+ const networks: Record<string, ComposeNetworkDef> = {};
69
+ renderApp(app, services, networks, opts);
70
+ return {
71
+ appId: app.appId,
72
+ compose: {
73
+ services,
74
+ ...(Object.keys(networks).length > 0 ? { networks } : {}),
75
+ },
76
+ };
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Order apps so every provider comes before the apps that consume one of
82
+ * its services (Kahn's algorithm over the cross-app `dependsOn` provider
83
+ * edges). The worker runs the per-app ups in this order, so a consumer's
84
+ * provider is already up by the time its own up runs.
85
+ *
86
+ * Edges to apps not present in this wire are ignored. Apps left in a
87
+ * cycle (or with no edges) keep their input order and are appended last;
88
+ * they converge across reconciles via `restart: on-failure`.
89
+ */
90
+ function topoSortApps(apps: WorkerApp[]): WorkerApp[] {
91
+ const present = new Set(apps.map((a) => a.appId));
92
+ // For each app, the set of in-wire provider appIds it depends on.
93
+ const providersOf = new Map<string, Set<string>>();
94
+ for (const app of apps) {
95
+ const providers = new Set<string>();
96
+ for (const edges of Object.values(app.dependsOn)) {
97
+ for (const edge of edges) {
98
+ if (edge.providerAppId !== app.appId && present.has(edge.providerAppId)) {
99
+ providers.add(edge.providerAppId);
100
+ }
101
+ }
102
+ }
103
+ providersOf.set(app.appId, providers);
104
+ }
105
+
106
+ const sorted: WorkerApp[] = [];
107
+ const placed = new Set<string>();
108
+ // Each pass emits, in input order, every not-yet-placed app whose
109
+ // providers are all placed. A pass that places nothing means the rest
110
+ // is a cycle.
111
+ let progressed = true;
112
+ while (progressed && placed.size < apps.length) {
113
+ progressed = false;
114
+ for (const app of apps) {
115
+ if (placed.has(app.appId)) continue;
116
+ const ready = [...providersOf.get(app.appId)!].every((p) => placed.has(p));
117
+ if (!ready) continue;
118
+ sorted.push(app);
119
+ placed.add(app.appId);
120
+ progressed = true;
121
+ }
122
+ }
123
+ // Cycle remainder: append in input order.
124
+ for (const app of apps) {
125
+ if (!placed.has(app.appId)) sorted.push(app);
126
+ }
127
+ return sorted;
128
+ }
129
+
130
+ function renderApp(
131
+ app: WorkerApp,
132
+ services: Record<string, ComposeService>,
133
+ networks: Record<string, ComposeNetworkDef>,
134
+ opts: ConvertOptions,
135
+ ): void {
136
+ const { appId, services: appServices, egress, externalNetworks, allowedAbsoluteHostPaths } = app;
137
+ const exemptHostPaths = new Set(allowedAbsoluteHostPaths);
138
+ // Home pre-computes the app's union of external networks, and an EFFECT
139
+ // can add more per service during render. Both are honored.
140
+ const appExternalNetworks = new Set<string>(externalNetworks);
141
+
142
+ for (const [name, svc] of Object.entries(appServices)) {
143
+ const volumes: ComposeVolume[] = [];
144
+ // Whether this service mounts a directory the worker prepares with
145
+ // REALM_APP_DIR_GID + setgid. The container has to join that group or an
146
+ // image running as a baked-in uid can't write its own storage; the same
147
+ // predicate decides membership and what the worker prepares.
148
+ let ownsPreparedDirs = false;
149
+ if (svc.volumes) {
150
+ for (const v of svc.volumes) {
151
+ const hostPath = v.hostPath;
152
+ const source = exemptHostPaths.has(hostPath) ? hostPath : scopeBindSource(hostPath, appId);
153
+ if (isPreparedAppDir(appId, v, exemptHostPaths)) ownsPreparedDirs = true;
154
+ // Read-only is opt-in. A mount an app declared is a mount it means to
155
+ // use, and the overwhelmingly common case is writing to it.
156
+ volumes.push({
157
+ type: "bind",
158
+ source,
159
+ target: v.containerPath,
160
+ ...(v.readOnly ? { read_only: true } : {}),
161
+ });
162
+ }
163
+ }
164
+
165
+ let environment: Record<string, string> | undefined;
166
+ if (svc.env) {
167
+ environment = {};
168
+ for (const [k, val] of Object.entries(svc.env)) {
169
+ environment[k] = String(val);
170
+ }
171
+ }
172
+ if (opts.tz && !(environment && "TZ" in environment)) {
173
+ environment ??= {};
174
+ environment.TZ = opts.tz;
175
+ }
176
+
177
+ let healthcheck: ComposeHealthcheck | undefined;
178
+ if (svc.healthcheck) {
179
+ const hc = svc.healthcheck;
180
+ // Compose-spec rejects bare integers — durations need a unit suffix.
181
+ healthcheck = {
182
+ test: hc.test,
183
+ interval: `${hc.interval}s`,
184
+ timeout: `${hc.timeout}s`,
185
+ retries: hc.retries,
186
+ start_period: `${hc.startPeriod}s`,
187
+ };
188
+ }
189
+
190
+ let deploy: ComposeDeploy | undefined;
191
+ const { memory, cpu, replicas } = svc;
192
+ if (memory != null || cpu != null || replicas > 1) {
193
+ deploy = {};
194
+ if (replicas > 1) deploy.replicas = replicas;
195
+ if (memory || cpu) {
196
+ deploy.resources = {
197
+ limits: {
198
+ ...(memory ? { memory } : {}),
199
+ ...(cpu ? { cpus: String(cpu) } : {}),
200
+ },
201
+ };
202
+ }
203
+ }
204
+
205
+ const labels = buildContainerLabels({
206
+ appId,
207
+ serviceName: name,
208
+ customLabels: svc.labels,
209
+ });
210
+
211
+ const composeService: ComposeService = {
212
+ image: svc.image,
213
+ ...(opts.platform ? { platform: opts.platform } : {}),
214
+ ...(environment ? { environment } : {}),
215
+ ...(svc.command ? { command: svc.command } : {}),
216
+ labels,
217
+ ...(volumes.length > 0 ? { volumes } : {}),
218
+ ...(ownsPreparedDirs ? { group_add: [REALM_APP_DIR_GID] } : {}),
219
+ ...(healthcheck ? { healthcheck } : {}),
220
+ ...(deploy ? { deploy } : {}),
221
+ cap_drop: [...HARDENING.cap_drop],
222
+ cap_add: [...HARDENING.cap_add],
223
+ security_opt: [...HARDENING.security_opt],
224
+ pids_limit: HARDENING.pids_limit,
225
+ ulimits: { ...HARDENING.ulimits },
226
+ ...(svc.shmSizeMb != null ? { shm_size: `${svc.shmSizeMb}m` } : {}),
227
+ restart: HARDENING.restart,
228
+ networks: { [`realm_${appId}`]: { aliases: [name] } },
229
+ logging: {
230
+ driver: HARDENING.logging.driver,
231
+ options: { ...HARDENING.logging.options },
232
+ },
233
+ };
234
+
235
+ const filesUnused = new Map<string, string>();
236
+ const ctx: EffectContext = {
237
+ appId,
238
+ serviceName: name,
239
+ composeService,
240
+ externalNetworks: appExternalNetworks,
241
+ // The renderer doesn't materialize inline file bytes — Home ships them
242
+ // over the wire — but EFFECTS keeps the `files` parameter. No effect
243
+ // writes to it today; one that did would surface in the golden tests.
244
+ files: filesUnused,
245
+ };
246
+ for (const effect of Object.values(EFFECTS)) {
247
+ effect(svc, ctx);
248
+ }
249
+
250
+ // svc.files is keyed by container path; values are sha256 hashes Home
251
+ // pre-computed. Inline content is keyed by IDENTITY (sha256 of
252
+ // `appId:serviceKey:fileKey`), so the host path stays stable across content
253
+ // changes and a rewrite lands in place — what lets `--watch` consumers
254
+ // reload without a container recreate. Artifact refs carry their own hash.
255
+ if (svc.files != null) {
256
+ for (const [containerPath, sha] of Object.entries(svc.files)) {
257
+ composeService.volumes ??= [];
258
+ composeService.volumes.push({
259
+ type: "bind",
260
+ source: `./files/${sha}`,
261
+ target: containerPath,
262
+ read_only: true,
263
+ });
264
+ }
265
+ }
266
+
267
+ services[`${appId}_${name}`] = composeService;
268
+ }
269
+
270
+ networks[`realm_${appId}`] = {
271
+ name: `realm_${appId}`,
272
+ ...(!egress ? { internal: true } : {}),
273
+ };
274
+
275
+ // External networks (management-net, peer `realm_<otherApp>`) get no
276
+ // top-level declaration: nothing in the `services` block references them.
277
+ // The worker connects to them post-up via `docker network connect`.
278
+ }
@@ -0,0 +1,21 @@
1
+ // Drop every capability, add back only what an ordinary image needs to boot:
2
+ // CHOWN to claim its own data dir, SETGID/SETUID for the gosu drop to a service
3
+ // user, NET_BIND_SERVICE to bind below 1024.
4
+ //
5
+ // DAC_OVERRIDE is here because without it those same entrypoints work only on
6
+ // their FIRST boot: uid 0 with no capabilities still faces ordinary permission
7
+ // checks, so once an image has tightened its own data dir (postgres chmods
8
+ // PGDATA to 0700 and refuses to start otherwise) root can no longer descend it
9
+ // and the entrypoint dies on the traversal. It costs little — the boundary is
10
+ // the mount gate in security.ts, not in-container DAC, and read-only mounts
11
+ // are unaffected (MS_RDONLY is VFS-enforced). These 5 are a subset of the 14
12
+ // capabilities Docker's own default grants.
13
+ export const HARDENING = {
14
+ cap_drop: ["ALL"],
15
+ cap_add: ["CHOWN", "DAC_OVERRIDE", "SETGID", "SETUID", "NET_BIND_SERVICE"],
16
+ security_opt: ["no-new-privileges:true"],
17
+ pids_limit: 4096,
18
+ ulimits: { nofile: 65536, nproc: 4096 },
19
+ logging: { driver: "json-file", options: { "max-size": "10m", "max-file": "3" } },
20
+ restart: "on-failure",
21
+ } as const;
@@ -0,0 +1,107 @@
1
+ import type { HostPermissionKind } from "../../../permissions.ts";
2
+ import type { WireService } from "../../../worker-protocol/index.ts";
3
+ import type { ComposeService } from "./types.ts";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Compose effects table
7
+ // ---------------------------------------------------------------------------
8
+ //
9
+ // One entry per host permission kind. Each effect mutates the
10
+ // `ComposeService` (and the shared externalNetworks/files maps) from the
11
+ // resolved service's fields. The gate has already validated the service
12
+ // against grants by the time this runs, so effects only translate.
13
+ //
14
+ // `Record<HostPermissionKind, EffectFn>` keeps the table in lock-step with
15
+ // the kinds derived in permissions.ts: a new kind without an effect, or a
16
+ // stale effect for a deleted one, is a compile error.
17
+
18
+ export interface EffectContext {
19
+ appId: string;
20
+ serviceName: string;
21
+ composeService: ComposeService;
22
+ externalNetworks: Set<string>;
23
+ files: Map<string, string>;
24
+ }
25
+
26
+ type EffectFn = (svc: WireService, ctx: EffectContext) => void;
27
+
28
+ export const EFFECTS: Record<HostPermissionKind, EffectFn> = {
29
+ "host.gpu": (svc, ctx) => {
30
+ if (svc.gpu === true) {
31
+ ctx.composeService.devices = [...(ctx.composeService.devices ?? []), "/dev/dri:/dev/dri"];
32
+ }
33
+ },
34
+
35
+ "host.port": (svc, ctx) => {
36
+ if (!svc.hostPorts || svc.hostPorts.length === 0) return;
37
+ const cs = ctx.composeService;
38
+ cs.ports ??= [];
39
+ for (const hp of svc.hostPorts) {
40
+ cs.ports.push({
41
+ target: hp.containerPort,
42
+ published: hp.hostPort,
43
+ ...(hp.protocol ? { protocol: hp.protocol } : {}),
44
+ });
45
+ }
46
+ },
47
+
48
+ // A Silo's volume is mounted by an ordinary `volumes:` entry; the one thing
49
+ // the renderer adds is group membership, so it lands exactly where the mount
50
+ // lands, per service - a sidecar that doesn't mount the shared library never
51
+ // joins the group. `group_add` needs no cooperation from the image (unlike
52
+ // `user:`), so a root image, a baked-in uid, and a PUID all work.
53
+ "host.silo": (svc, ctx) => {
54
+ // Seeded from what's already there: a service that also mounts its own
55
+ // storage carries REALM_APP_DIR_GID from the base render, and joining a
56
+ // Silo must add to that membership rather than replace it - dropping it
57
+ // would take away the app's *own* directory to give it a shared one.
58
+ const gids = new Set<number>(ctx.composeService.group_add ?? []);
59
+ for (const v of svc.volumes ?? []) {
60
+ if (v.gid !== undefined) gids.add(v.gid);
61
+ }
62
+ if (gids.size > 0) ctx.composeService.group_add = [...gids].sort((a, b) => a - b);
63
+ },
64
+
65
+ // joinNetworks doesn't land in compose: the worker reconciles network
66
+ // membership post-up with `docker network connect/disconnect`, so adding
67
+ // or removing one doesn't recreate the container. The data rides the wire
68
+ // on `services[*].joinNetworks` and the worker reads it directly.
69
+ "host.management": () => {},
70
+
71
+ "host.egress": () => {},
72
+
73
+ // `iptables: true` rolls up the two raw primitives the firewall needs:
74
+ // NET_ADMIN to mutate netfilter, and host network mode so rules apply to
75
+ // host traffic and Docker's DOCKER-USER chain. Authoring stays at the
76
+ // intent level; the grant gate has already validated the use.
77
+ "host.iptables": (svc, ctx) => {
78
+ if (svc.iptables !== true) return;
79
+ if (!ctx.composeService.cap_add.includes("NET_ADMIN")) {
80
+ ctx.composeService.cap_add.push("NET_ADMIN");
81
+ }
82
+ ctx.composeService.network_mode = "host";
83
+ // host network mode and the per-project default network are
84
+ // mutually exclusive in compose.
85
+ delete ctx.composeService.networks;
86
+ },
87
+
88
+ // host.logs has no compose effect — the socket the value resolves to is
89
+ // mounted by the app via a normal `volumes:` entry. The grant exempts
90
+ // that mount from the absolute-mount gate (see graph.ts collectAllowed).
91
+ "host.logs": () => {},
92
+
93
+ // host.stats has no compose effect — same shape as host.logs: the app mounts
94
+ // the socket dir via a normal `volumes:` entry and the grant exempts it.
95
+ "host.stats": () => {},
96
+
97
+ // host.spawn has no compose effect — the socket/scratch/artifacts paths
98
+ // are mounted via normal `volumes:` entries and the network rides
99
+ // `joinNetworks`. The grant exempts all three from the absolute-mount /
100
+ // network.join gates (see graph.ts collectAllowed).
101
+ "host.spawn": () => {},
102
+
103
+ // No compose effect — `host.rocketLaunch` is a probe-the-UX permission
104
+ // that no worker advertises, so the gate blocks any app declaring it
105
+ // before it could ever reach this table.
106
+ "host.rocketLaunch": () => {},
107
+ };
@@ -0,0 +1,17 @@
1
+ // @yourrealm/sdk/runtime/docker/compose — docker-compose renderer.
2
+ //
3
+ // Transforms `WorkerState` (from @yourrealm/sdk/worker-protocol) into a
4
+ // docker-compose.yml. No I/O, no docker socket, no network. Home uses it in
5
+ // integration tests; the Worker uses it at reconcile time to produce the
6
+ // file `docker compose up` consumes.
7
+
8
+ export {
9
+ type AppComposeProject,
10
+ convertToDockerCompose,
11
+ type ConvertOptions,
12
+ } from "./convert-to-docker-compose.ts";
13
+ export { HARDENING } from "./default-hardening.ts";
14
+ export { buildContainerLabels } from "./labels.ts";
15
+ export { EFFECTS, type EffectContext } from "./effects.ts";
16
+ export { composeSchema } from "./compose-schema.ts";
17
+ export type { ComposeConfig, ComposeNetworkDef, ComposeService } from "./types.ts";
@@ -0,0 +1,39 @@
1
+ import { KINDS, LABELS } from "@local/docker";
2
+
3
+ interface ContainerLabelOptions {
4
+ appId: string;
5
+ serviceName: string;
6
+ customLabels?: string[];
7
+ }
8
+
9
+ const RESERVED_PREFIXES = ["realm.", "com.docker.", "org.opencontainers."];
10
+
11
+ /**
12
+ * Realm container metadata labels (managed, app, service). Routing
13
+ * data lives in the Caddy JSON config the proxy app generates from
14
+ * `peers.routedApps()`, not on container labels.
15
+ *
16
+ * Custom labels declared in realm.ts pass through unchanged unless they
17
+ * collide with a reserved prefix, in which case they're dropped.
18
+ */
19
+ export function buildContainerLabels(options: ContainerLabelOptions): Record<string, string> {
20
+ const labels: Record<string, string> = {};
21
+
22
+ if (options.customLabels) {
23
+ for (const label of options.customLabels) {
24
+ const eqIdx = label.indexOf("=");
25
+ if (eqIdx === -1) continue;
26
+ const key = label.slice(0, eqIdx);
27
+ const keyLower = key.toLowerCase();
28
+ if (RESERVED_PREFIXES.some((p) => keyLower.startsWith(p))) continue;
29
+ labels[key] = label.slice(eqIdx + 1);
30
+ }
31
+ }
32
+
33
+ labels[LABELS.MANAGED] = "true";
34
+ labels[LABELS.KIND] = KINDS.APP_SERVICE;
35
+ labels[LABELS.APP] = options.appId;
36
+ labels[LABELS.SERVICE] = options.serviceName;
37
+
38
+ return labels;
39
+ }
@@ -0,0 +1,78 @@
1
+ // Compose-shape types the docker renderer emits. They live beside the
2
+ // renderer, which owns the docker dialect end-to-end; Home uses them only as
3
+ // TypeScript types via the @yourrealm/sdk/compile re-exports.
4
+
5
+ interface ComposeVolume {
6
+ type: "bind" | "volume";
7
+ source: string;
8
+ target: string;
9
+ read_only?: boolean;
10
+ }
11
+
12
+ interface ComposePort {
13
+ target: number;
14
+ published: number;
15
+ protocol?: "tcp" | "udp";
16
+ }
17
+
18
+ interface ComposeHealthcheck {
19
+ test: string[];
20
+ interval?: string;
21
+ timeout?: string;
22
+ retries?: number;
23
+ start_period?: string;
24
+ }
25
+
26
+ interface ComposeDeploy {
27
+ replicas?: number;
28
+ resources?: {
29
+ limits?: {
30
+ memory?: string;
31
+ cpus?: string;
32
+ };
33
+ };
34
+ }
35
+
36
+ interface ComposeDependsOn {
37
+ [serviceName: string]: { condition: "service_started" | "service_healthy" };
38
+ }
39
+
40
+ export interface ComposeService {
41
+ image: string;
42
+ platform?: string;
43
+ environment?: Record<string, string>;
44
+ command?: string[];
45
+ entrypoint?: string[];
46
+ labels?: Record<string, string>;
47
+ volumes?: ComposeVolume[];
48
+ /** Supplementary groups, derived from the gid-carrying volumes mounted. */
49
+ group_add?: number[];
50
+ ports?: ComposePort[];
51
+ healthcheck?: ComposeHealthcheck;
52
+ deploy?: ComposeDeploy;
53
+ cap_drop: string[];
54
+ cap_add: string[];
55
+ security_opt: string[];
56
+ pids_limit: number;
57
+ ulimits: { nofile: number; nproc: number };
58
+ shm_size?: string;
59
+ devices?: string[];
60
+ network_mode?: string;
61
+ restart: string;
62
+ networks?: Record<string, { aliases?: string[] }>;
63
+ depends_on?: ComposeDependsOn;
64
+ logging: { driver: string; options: Record<string, string> };
65
+ }
66
+
67
+ export interface ComposeNetworkDef {
68
+ external?: true;
69
+ internal?: true;
70
+ name: string;
71
+ }
72
+
73
+ export interface ComposeConfig {
74
+ services: Record<string, ComposeService>;
75
+ networks?: Record<string, ComposeNetworkDef>;
76
+ }
77
+
78
+ export type { ComposeVolume, ComposePort, ComposeHealthcheck, ComposeDeploy, ComposeDependsOn };