@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,557 @@
1
+ import type {
2
+ ConfigContainerNode,
3
+ ConfigFieldLeaf,
4
+ ConfigFieldTree,
5
+ ConfigPermissionRef,
6
+ SerializablePermission,
7
+ } from "../../sandbox.ts";
8
+ import { SectionLabel } from "../components/permission-section.tsx";
9
+ import { GearIcon } from "../components/icons.tsx";
10
+ import { ConfigField, type ConfigFieldKind } from "../components/config-field.tsx";
11
+ import { PermissionRow } from "../components/permission-row.tsx";
12
+ import { SiloPicker } from "../components/silo-picker.tsx";
13
+ import { DomainEditor } from "../components/domain-editor.tsx";
14
+ import { PERMISSION_META, type PermissionKind, type SiloGrants } from "../../home.ts";
15
+ import type { PublicDomainGrantData } from "../../schema/public-domain.ts";
16
+ import type { SiloOption } from "./adapter.ts";
17
+ import { SectionBlock } from "../components/schema-block.tsx";
18
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "../components/tabs.tsx";
19
+ import { Badge } from "../components/badge.tsx";
20
+
21
+ // The config-field descriptor types are produced once by the sandbox walker
22
+ // (see sandbox.ts); import them rather than mirror them here. A type-only
23
+ // import erases at build, so no QuickJS reaches the client island bundle.
24
+ // Re-exported for the consumers that pull them from this module.
25
+ export type { ConfigContainerNode, ConfigFieldLeaf, ConfigFieldTree };
26
+ export type ConfigField = ConfigFieldLeaf | ConfigContainerNode | ConfigPermissionRef;
27
+
28
+ interface TabsFormProps {
29
+ configFields: ConfigFieldTree;
30
+ permissionsFlat: Record<string, SerializablePermission>;
31
+ configValues: Record<string, unknown>;
32
+ /**
33
+ * Dotted paths of permissions the admin has enabled. Anything not in
34
+ * this set is treated as denied on install.
35
+ */
36
+ enabledPermissions: Set<string>;
37
+ /**
38
+ * Per-path admin-override values. Sparse: only entries the admin
39
+ * actively edited. Renderers fall back to the descriptor's `perm.data`
40
+ * for the displayed default; `descriptorsToGrants` does the same on
41
+ * submit. Presence-only kinds (host.gpu, host.egress, host.management)
42
+ * and peers.* / remote.* never appear here.
43
+ */
44
+ grantValues: Record<string, unknown>;
45
+ /**
46
+ * Dotted paths whose stored value is redacted on the server. The
47
+ * corresponding input renders a "leave blank to keep current" hint; the
48
+ * caller filters those out of the PATCH body when the input is empty.
49
+ */
50
+ redactedPaths?: string[];
51
+ /**
52
+ * Per-redacted-path display mask (e.g. `pk_live_••••`), shown as the secret
53
+ * field's placeholder so the operator can tell which key is set. Optional —
54
+ * the new-app install path has no stored secrets, so it never passes this.
55
+ */
56
+ secretHints?: Record<string, string>;
57
+ onConfigChange: (path: string, value: unknown) => void;
58
+ onPermissionToggle: (path: string, enabled: boolean) => void;
59
+ onGrantValueChange?: (path: string, value: unknown) => void;
60
+ /**
61
+ * A `home.publicDomain` leaf has a hostname typed into its add field that
62
+ * hasn't been turned into a row. Reported per path (an app may declare two
63
+ * leaves) so the panel can gate Install/Save on it.
64
+ */
65
+ onDomainPendingChange?: (path: string, pending: boolean) => void;
66
+ /**
67
+ * Every Silo the host knows. Only fetched when the app declares a
68
+ * `host.silo` permission; the picker narrows to `siloWorkerId`.
69
+ */
70
+ silos?: SiloOption[];
71
+ /** The worker this app deploys to. Null until the operator picks one. */
72
+ siloWorkerId?: string | null;
73
+ siloKind?: "single" | "system";
74
+ /** Create a Silo on `siloWorkerId` and resolve with the stored row. */
75
+ onCreateSilo?: (input: {
76
+ name: string;
77
+ containerPath?: string;
78
+ hostPath?: string;
79
+ }) => Promise<SiloOption>;
80
+ /** Confirm an adopted Silo's setup command from the picker. */
81
+ onAcknowledgeSilo?: (id: string) => Promise<void>;
82
+ /** Disable every input (e.g. while saving). */
83
+ disabled?: boolean;
84
+ /**
85
+ * Per-tab count of still-unmet required items (config + permissions),
86
+ * keyed by the tab's object key. Drives the count badge on each tab so
87
+ * the operator can see at a glance which tabs still need input. Absent /
88
+ * zero = no badge. Only meaningful when the top level is tabs.
89
+ */
90
+ needsByTab?: Record<string, number>;
91
+ }
92
+
93
+ function isKnownPermissionKind(key: string): key is PermissionKind {
94
+ return key in PERMISSION_META;
95
+ }
96
+
97
+ /**
98
+ * The Silo props, bundled once so the tree drills one prop, not five.
99
+ * Null when the host has no Silos to offer at all - `realm-sdk dev` has no
100
+ * Home, so it has no Silo records and no worker model to scope them by. The
101
+ * picker is left out entirely there rather than shown in a state it can never
102
+ * leave; the dev compiler already fails the install with an actionable
103
+ * "no Silo in the fixture" message.
104
+ */
105
+ interface SiloContext {
106
+ silos: SiloOption[];
107
+ workerId: string | null;
108
+ kind: "single" | "system";
109
+ onCreate?: (input: {
110
+ name: string;
111
+ containerPath?: string;
112
+ hostPath?: string;
113
+ }) => Promise<SiloOption>;
114
+ onAcknowledge?: (id: string) => Promise<void>;
115
+ }
116
+
117
+ /**
118
+ * Unified renderer for the config + permissions tree. Walks `configFields`
119
+ * and inlines permission rows at their dotted paths. Optional sections
120
+ * render as a fieldset with an on/off toggle; children render disabled
121
+ * when the section is off.
122
+ */
123
+ export function TabsForm({
124
+ configFields,
125
+ permissionsFlat,
126
+ configValues,
127
+ enabledPermissions,
128
+ grantValues,
129
+ redactedPaths,
130
+ secretHints,
131
+ onConfigChange,
132
+ onPermissionToggle,
133
+ onGrantValueChange,
134
+ onDomainPendingChange,
135
+ silos,
136
+ siloWorkerId,
137
+ siloKind,
138
+ onCreateSilo,
139
+ onAcknowledgeSilo,
140
+ disabled,
141
+ needsByTab,
142
+ }: TabsFormProps) {
143
+ const redactedSet = new Set(redactedPaths ?? []);
144
+ // `silos` absent (not empty) is the host saying it has no Silo model.
145
+ const silo: SiloContext | null = silos
146
+ ? {
147
+ silos,
148
+ workerId: siloWorkerId ?? null,
149
+ kind: siloKind ?? "single",
150
+ onCreate: onCreateSilo,
151
+ onAcknowledge: onAcknowledgeSilo,
152
+ }
153
+ : null;
154
+ const tabEntries = Object.entries(configFields).filter(
155
+ (e): e is [string, ConfigContainerNode] => e[1].type === "tab",
156
+ );
157
+
158
+ if (tabEntries.length === 0) {
159
+ return (
160
+ <div class="flex flex-col gap-4">
161
+ <FlatBlock
162
+ configFields={configFields}
163
+ permissionsFlat={permissionsFlat}
164
+ configValues={configValues}
165
+ enabledPermissions={enabledPermissions}
166
+ grantValues={grantValues}
167
+ redactedSet={redactedSet}
168
+ secretHints={secretHints}
169
+ onConfigChange={onConfigChange}
170
+ onPermissionToggle={onPermissionToggle}
171
+ onGrantValueChange={onGrantValueChange}
172
+ onDomainPendingChange={onDomainPendingChange}
173
+ silo={silo}
174
+ disabled={disabled ?? false}
175
+ />
176
+ </div>
177
+ );
178
+ }
179
+
180
+ return (
181
+ <Tabs defaultValue={tabEntries[0]![0]} data-testid="install-tabs">
182
+ <TabsList>
183
+ {tabEntries.map(([name, field]) => {
184
+ const needs = needsByTab?.[name] ?? 0;
185
+ return (
186
+ <TabsTrigger key={name} value={name} data-testid={`tab-${name}`}>
187
+ <span>{field.title ?? name}</span>
188
+ {needs > 0 && (
189
+ // Calm "needs attention" count (amber, not error-red) so the
190
+ // operator sees which tabs still want input at a glance.
191
+ <Badge
192
+ variant="outline"
193
+ className="min-w-5 border-amber-300/60 bg-amber-50 px-1.5 text-amber-700 tabular-nums"
194
+ data-testid={`tab-needs-${name}`}
195
+ >
196
+ {needs}
197
+ </Badge>
198
+ )}
199
+ </TabsTrigger>
200
+ );
201
+ })}
202
+ </TabsList>
203
+ {tabEntries.map(([name, field]) => {
204
+ const nested = (configValues[name] as Record<string, unknown> | undefined) ?? {};
205
+ return (
206
+ <TabsContent key={name} value={name}>
207
+ {field.description && (
208
+ <p class="text-xs text-muted-foreground leading-snug">{field.description}</p>
209
+ )}
210
+ {Object.entries(field.fields).map(([childName, childField]) => (
211
+ <FieldNode
212
+ key={childName}
213
+ path={`${name}.${childName}`}
214
+ name={childName}
215
+ field={childField}
216
+ values={nested}
217
+ permissionsFlat={permissionsFlat}
218
+ enabledPermissions={enabledPermissions}
219
+ grantValues={grantValues}
220
+ redactedSet={redactedSet}
221
+ secretHints={secretHints}
222
+ onConfigChange={onConfigChange}
223
+ onPermissionToggle={onPermissionToggle}
224
+ onGrantValueChange={onGrantValueChange}
225
+ onDomainPendingChange={onDomainPendingChange}
226
+ silo={silo}
227
+ disabled={disabled ?? false}
228
+ sectionOptionalOff={false}
229
+ />
230
+ ))}
231
+ </TabsContent>
232
+ );
233
+ })}
234
+ </Tabs>
235
+ );
236
+ }
237
+
238
+ interface FlatBlockProps {
239
+ configFields: ConfigFieldTree;
240
+ permissionsFlat: Record<string, SerializablePermission>;
241
+ configValues: Record<string, unknown>;
242
+ enabledPermissions: Set<string>;
243
+ grantValues: Record<string, unknown>;
244
+ redactedSet: Set<string>;
245
+ secretHints?: Record<string, string>;
246
+ onConfigChange: (path: string, value: unknown) => void;
247
+ onPermissionToggle: (path: string, enabled: boolean) => void;
248
+ onGrantValueChange?: (path: string, value: unknown) => void;
249
+ onDomainPendingChange?: (path: string, pending: boolean) => void;
250
+ silo: SiloContext | null;
251
+ disabled: boolean;
252
+ }
253
+
254
+ /**
255
+ * Flat-leaves rendering: one card with every leaf (config + permission)
256
+ * in declared order. The walker stamps permission markers into
257
+ * `configFields` so iteration order matches the source.
258
+ */
259
+ function FlatBlock(props: FlatBlockProps) {
260
+ const {
261
+ configFields,
262
+ permissionsFlat,
263
+ configValues,
264
+ enabledPermissions,
265
+ grantValues,
266
+ redactedSet,
267
+ secretHints,
268
+ onConfigChange,
269
+ onPermissionToggle,
270
+ onGrantValueChange,
271
+ onDomainPendingChange,
272
+ silo,
273
+ disabled,
274
+ } = props;
275
+ const entries = Object.entries(configFields);
276
+ if (entries.length === 0) return null;
277
+ return (
278
+ <div class="bg-trust-bg rounded-3xl shadow-clay-blue p-4 flex flex-col gap-3">
279
+ <SectionLabel
280
+ icon={<GearIcon className="w-4 h-4 text-trust" />}
281
+ className="text-trust pl-0.5"
282
+ >
283
+ Configuration
284
+ </SectionLabel>
285
+ {entries.map(([name, field]) => (
286
+ <FieldNode
287
+ key={name}
288
+ path={name}
289
+ name={name}
290
+ field={field}
291
+ values={configValues}
292
+ permissionsFlat={permissionsFlat}
293
+ enabledPermissions={enabledPermissions}
294
+ grantValues={grantValues}
295
+ redactedSet={redactedSet}
296
+ secretHints={secretHints}
297
+ onConfigChange={onConfigChange}
298
+ onPermissionToggle={onPermissionToggle}
299
+ onGrantValueChange={onGrantValueChange}
300
+ onDomainPendingChange={onDomainPendingChange}
301
+ silo={silo}
302
+ disabled={disabled}
303
+ sectionOptionalOff={false}
304
+ />
305
+ ))}
306
+ </div>
307
+ );
308
+ }
309
+
310
+ interface FieldNodeProps {
311
+ path: string;
312
+ name: string;
313
+ field: ConfigField;
314
+ values: Record<string, unknown>;
315
+ permissionsFlat: Record<string, SerializablePermission>;
316
+ enabledPermissions: Set<string>;
317
+ grantValues: Record<string, unknown>;
318
+ redactedSet: Set<string>;
319
+ secretHints?: Record<string, string>;
320
+ onConfigChange: (path: string, value: unknown) => void;
321
+ onPermissionToggle: (path: string, enabled: boolean) => void;
322
+ onGrantValueChange?: (path: string, value: unknown) => void;
323
+ onDomainPendingChange?: (path: string, pending: boolean) => void;
324
+ silo: SiloContext | null;
325
+ disabled: boolean;
326
+ /** Inherited from an ancestor optional section whose feature flag is OFF. */
327
+ sectionOptionalOff: boolean;
328
+ }
329
+
330
+ function toFieldKind(type: ConfigFieldLeaf["type"]): ConfigFieldKind {
331
+ return type;
332
+ }
333
+
334
+ function FieldNode(props: FieldNodeProps) {
335
+ const {
336
+ path,
337
+ name,
338
+ field,
339
+ values,
340
+ permissionsFlat,
341
+ enabledPermissions,
342
+ grantValues,
343
+ redactedSet,
344
+ secretHints,
345
+ onConfigChange,
346
+ onPermissionToggle,
347
+ onGrantValueChange,
348
+ onDomainPendingChange,
349
+ silo,
350
+ disabled,
351
+ sectionOptionalOff,
352
+ } = props;
353
+
354
+ if (field.type === "tab") {
355
+ // Nested tab inside a section would be a design violation; ignore.
356
+ return null;
357
+ }
358
+
359
+ if (field.type === "permission") {
360
+ const meta = permissionsFlat[field.path];
361
+ if (!meta) return null;
362
+ return (
363
+ <PermissionLeaf
364
+ path={field.path}
365
+ meta={meta}
366
+ enabled={enabledPermissions.has(field.path)}
367
+ grantValues={grantValues}
368
+ onPermissionToggle={onPermissionToggle}
369
+ onGrantValueChange={onGrantValueChange}
370
+ onDomainPendingChange={onDomainPendingChange}
371
+ silo={silo}
372
+ disabled={disabled}
373
+ sectionOptionalOff={sectionOptionalOff}
374
+ />
375
+ );
376
+ }
377
+
378
+ if (field.type === "section") {
379
+ const current = values[name];
380
+ // Toggle reflects whether the section's value is defined.
381
+ const on = field.optional ? current !== undefined : true;
382
+ const nested = on ? ((current as Record<string, unknown> | undefined) ?? {}) : {};
383
+ return (
384
+ <SectionBlock
385
+ name={field.title ?? name}
386
+ optional={field.optional}
387
+ on={on}
388
+ // Flipping on initializes an empty object so children can render.
389
+ // Flipping off resets to undefined so the sandbox sees the section
390
+ // as absent.
391
+ onChange={(next) => onConfigChange(path, next ? {} : undefined)}
392
+ disabled={disabled}
393
+ >
394
+ {Object.entries(field.fields).map(([childName, childField]) => (
395
+ <FieldNode
396
+ key={childName}
397
+ path={`${path}.${childName}`}
398
+ name={childName}
399
+ field={childField}
400
+ values={nested}
401
+ permissionsFlat={permissionsFlat}
402
+ enabledPermissions={enabledPermissions}
403
+ grantValues={grantValues}
404
+ redactedSet={redactedSet}
405
+ secretHints={secretHints}
406
+ onConfigChange={onConfigChange}
407
+ onPermissionToggle={onPermissionToggle}
408
+ onGrantValueChange={onGrantValueChange}
409
+ onDomainPendingChange={onDomainPendingChange}
410
+ silo={silo}
411
+ disabled={disabled || !on}
412
+ sectionOptionalOff={sectionOptionalOff || (field.optional === true && !on)}
413
+ />
414
+ ))}
415
+ </SectionBlock>
416
+ );
417
+ }
418
+
419
+ const leaf = field as ConfigFieldLeaf;
420
+ const id = `cfg-${path.replace(/\./g, "-")}`;
421
+ const label = leaf.label ?? name;
422
+ const current = values[name];
423
+ const isRedacted = redactedSet.has(path);
424
+ return (
425
+ <ConfigField
426
+ id={id}
427
+ kind={toFieldKind(leaf.type)}
428
+ label={label}
429
+ description={leaf.description}
430
+ placeholder={leaf.placeholder == null ? undefined : String(leaf.placeholder)}
431
+ values={leaf.values}
432
+ min={leaf.min}
433
+ max={leaf.max}
434
+ step={leaf.step}
435
+ required={sectionOptionalOff ? undefined : !leaf.optional}
436
+ value={current as string | number | boolean | undefined}
437
+ onChange={(v) => onConfigChange(path, v)}
438
+ disabled={disabled}
439
+ redacted={isRedacted}
440
+ secretMask={isRedacted ? secretHints?.[path] : undefined}
441
+ />
442
+ );
443
+ }
444
+
445
+ interface PermissionLeafProps {
446
+ path: string;
447
+ meta: SerializablePermission;
448
+ enabled: boolean;
449
+ grantValues: Record<string, unknown>;
450
+ onPermissionToggle: (path: string, enabled: boolean) => void;
451
+ onGrantValueChange?: (path: string, value: unknown) => void;
452
+ onDomainPendingChange?: (path: string, pending: boolean) => void;
453
+ silo: SiloContext | null;
454
+ disabled: boolean;
455
+ /** Inherited from an ancestor optional section whose feature flag is OFF. */
456
+ sectionOptionalOff: boolean;
457
+ }
458
+
459
+ function PermissionLeaf(props: PermissionLeafProps) {
460
+ const {
461
+ path,
462
+ meta,
463
+ enabled,
464
+ grantValues,
465
+ onPermissionToggle,
466
+ onGrantValueChange,
467
+ onDomainPendingChange,
468
+ silo,
469
+ disabled,
470
+ sectionOptionalOff,
471
+ } = props;
472
+ const kindKey = meta.kind;
473
+ if (!isKnownPermissionKind(kindKey)) return null;
474
+ // grantValues is sparse — only entries the admin actively edited. Fall
475
+ // back to the descriptor's perm.data so untouched fields show the
476
+ // proposed value. `??` on null/undefined only, so an admin-cleared
477
+ // empty string passes through verbatim; the server's grantSchema
478
+ // (z.coerce.number()...) does the parse and rejects it as invalid.
479
+ const portArg =
480
+ kindKey === "host.port"
481
+ ? ((grantValues[path] ?? meta.data) as string | number | undefined)
482
+ : undefined;
483
+ const urlArg =
484
+ kindKey === "remote.fetchUrl" || kindKey === "remote.artifact"
485
+ ? (meta.data as { url: string }).url
486
+ : undefined;
487
+ // The descriptor carries the author's affordances; the grant carries the
488
+ // operator's chosen Silo id. Different shapes, so no descriptor fallback —
489
+ // an unpicked Silo is genuinely nothing, and the Install gate blocks it.
490
+ const siloControl =
491
+ kindKey === "host.silo" && silo ? (
492
+ <SiloPicker
493
+ ask={meta.data as SiloGrants}
494
+ silos={silo.silos}
495
+ workerId={silo.workerId}
496
+ kind={silo.kind}
497
+ value={(grantValues[path] as string | undefined) ?? null}
498
+ onChange={(id) => onGrantValueChange?.(path, id)}
499
+ onCreate={async (input) => {
500
+ if (!silo.onCreate) throw new Error("This host can't create Silos.");
501
+ const created = await silo.onCreate(input);
502
+ onGrantValueChange?.(path, created.id);
503
+ }}
504
+ onAcknowledge={silo.onAcknowledge}
505
+ disabled={disabled}
506
+ />
507
+ ) : undefined;
508
+ // Same shape as the Silo picker: the operator supplies the elevated value,
509
+ // and the descriptor (`true`) has nothing to fall back to. Editing lives
510
+ // here rather than on the Domains page, which is a read surface in v1 (see
511
+ // `todo/ideas/naming-layer.md`, slice 3).
512
+ const domainControl =
513
+ kindKey === "home.publicDomain" ? (
514
+ <DomainEditor
515
+ value={grantValues[path] as PublicDomainGrantData | undefined}
516
+ onChange={(next) => onGrantValueChange?.(path, next)}
517
+ // Not locked behind the switch, unlike every other control here:
518
+ // supplying a hostname IS the operator granting the permission, so
519
+ // adding one flips the switch. A domain typed under an off switch is
520
+ // dropped at submit by `descriptorsToGrants`, and blank is a valid
521
+ // grant, so no save-gate would catch that.
522
+ onEnable={() => onPermissionToggle(path, true)}
523
+ onPendingChange={
524
+ onDomainPendingChange ? (pending) => onDomainPendingChange(path, pending) : undefined
525
+ }
526
+ // Greys the rows and the mode checkbox rather than locking the whole
527
+ // control: adding stays live because adding is what turns the switch
528
+ // on, but a parked list must not read as a published one.
529
+ enabled={enabled}
530
+ disabled={disabled}
531
+ />
532
+ ) : undefined;
533
+ const titleLabel = portArg != null ? String(portArg) : undefined;
534
+ const reason = (meta.meta as { reason?: string }).reason ?? "";
535
+ const isOptional = (meta.meta as { isOptional?: boolean }).isOptional;
536
+ return (
537
+ <div data-perm-path={path}>
538
+ <PermissionRow
539
+ kind={kindKey}
540
+ reason={reason}
541
+ required={sectionOptionalOff ? undefined : !isOptional}
542
+ checked={enabled}
543
+ onCheckedChange={(v) => onPermissionToggle(path, v)}
544
+ label={titleLabel}
545
+ port={portArg}
546
+ onPortChange={onGrantValueChange ? (v) => onGrantValueChange(path, v) : undefined}
547
+ control={siloControl ?? domainControl}
548
+ url={urlArg}
549
+ />
550
+ {sectionOptionalOff && enabled && (
551
+ <p class="text-xs text-muted-foreground leading-snug italic mt-1.5">
552
+ Part of an inactive feature.
553
+ </p>
554
+ )}
555
+ </div>
556
+ );
557
+ }