yah-cloud 0.8.48

Declarative cloud substrate for yah-managed camps: .yah/cloud/ config schema, MachineProvider drivers (Hetzner + local containerd), cloud-init rendering, and the pond/mesofact reconcilers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//! The dev-tier static surface: the same Worker the fleet serves, in front of
//! the `s3` capability driver the mirror binds (W265, R584-F4).
//!
//! # What this replaces, and why the replacement is the point
//!
//! Until this module landed, a dev-tier `mesofact-static` component was served
//! by `local-static`: the reconciler spawned `mesofact-dev`, which read the
//! workload's built `dist/` **off the filesystem** and handed it to the
//! browser. Every other tier published those same bytes into an object store
//! and served them through a Worker — MinIO + miniflare at pond, R2 + a
//! deployed Worker at cloud and ha.
//!
//! Two interfaces for one contract, which is the fork W265 exists to delete.
//! It is not an abstract complaint: a `Content-Type` the disk server inferred
//! differently, a route the Worker rewrites and the disk server does not, a
//! bucket policy nobody at dev ever had to set — each is a thing the app
//! discovers at the *next* tier up, which is the most expensive place to
//! discover it.
//!
//! So the dev tier now does what pond does:
//!
//! ```text
//!   dist/  ──publish_to_pond──▶  <s3 driver>/<bucket>  ──ASSET_ORIGIN──▶  miniflare (workerd)
//! ```
//!
//! The only thing that varies across dev / pond / cloud is **which
//! implementation of `s3` the mirror binds**, and that is declared rather than
//! branched on. The publish step is literally
//! [`super::pond_publish::publish_to_pond`], the door is literally
//! [`super::pond::spawn_miniflare_child`], and the script both run is the same
//! `worker/router.bundle.js` a fleet node serves.
//!
//! # Why there is no container here and one at pond
//!
//! miniflare was never the containerized half of pond — MinIO is. miniflare
//! runs as a plain child process at both tiers (`bun`/`node` executing the
//! shim, which spawns workerd). What dev drops is the MinIO container, because
//! `yah-s3-fs` already gives the camp an S3 surface with no docker daemon
//! anywhere in the picture. That is the whole difference between
//! `miniflare-container` and `miniflare-native`, and it lives in the store
//! binding rather than in the serving code.
//!
//! # The driver is camp-owned, not mirror-owned
//!
//! `yah-s3-fs` is brought up once per camp by `spawn_appliances`
//! ([`super::s3_driver`]), not by this reconciler, and it is always-on for any
//! camp with a dev mirror. A `mirror up` therefore *reads* its coordinates and
//! fails with a pointer at the camp if they are missing — it does not race the
//! camp to start a second store.

use std::collections::BTreeMap;
use std::path::PathBuf;

use anyhow::{Context, Result};
use tokio::sync::oneshot;
use tracing::{info, warn};

use local_driver::pond_minio::ensure_bucket_public;

use super::pond::{
    ensure_sim_port_free, run_miniflare_supervisor, spawn_miniflare_child, DoorScripts,
    PondOptions,
};
use super::{into_running, slot_field_u16, ReconcileCtx, RunningWorkload};
use crate::capability::Capability;
use crate::config::Provider;
use crate::reconciler::s3_driver::{running_endpoint, DEV_ACCESS_KEY, DEV_SECRET_KEY};

/// Slot role the door is declared under, shared with the pond arm.
const STATIC_SLOT: &str = "static";

/// Default loopback port for the dev door — the port `local-static` served on
/// before it retired, so an operator's bookmark still resolves.
pub const DEFAULT_DEV_DOOR_PORT: u16 = 4321;

/// `true` when this mirror binds the `s3` capability to the dev-tier driver,
/// i.e. when [`up_dev_door`] is the right arm for its static slot.
pub fn binds_dev_store(ctx: &ReconcileCtx<'_>) -> bool {
    ctx.mirror
        .driver(Capability::S3)
        .and_then(|slot| slot.inline_kind())
        == Some(Provider::LocalS3Fs)
}

/// Where the dev door keeps the two JS files it writes before spawning.
///
/// Under `.yah/infra/state/dev/`, beside the driver's own state, rather than
/// under `.yah/infra/pond/` — the tiers are separate and a shared directory
/// would make "is this camp's dev tier up?" unanswerable from the filesystem.
fn door_dir(ctx: &ReconcileCtx<'_>) -> PathBuf {
    ctx.workspace_root
        .join(".yah/infra/state/dev/door")
        .join(format!("{}-{}", ctx.service.name, ctx.component.id))
}

fn door_scripts(ctx: &ReconcileCtx<'_>) -> Result<DoorScripts> {
    let dir = door_dir(ctx);
    std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
    Ok(DoorScripts {
        worker_js: dir.join("worker.js"),
        miniflare_shim: dir.join("miniflare-sim.mjs"),
    })
}

/// Bucket this component's assets live in.
///
/// Defaults to the service name because that is what the camp driver creates
/// (`s3_driver::declared_s3_buckets` names one bucket per service with a dev
/// mirror) and what `S3_BUCKET` has meant to a dev app since R274-F5. A slot
/// may still name its own — [`ensure_bucket_public`] creates whatever it is
/// told, so an override is not a broken mirror.
fn resolve_bucket(ctx: &ReconcileCtx<'_>, fields: &BTreeMap<String, toml::Value>) -> String {
    fields
        .get("bucket")
        .and_then(|v| v.as_str())
        .unwrap_or(ctx.service.name.as_str())
        .to_string()
}

/// Resolve the store the mirror bound, or say why it cannot be reached.
///
/// The error is the useful half of this function. "connection refused on some
/// port" is what an operator would otherwise get three steps later, from
/// workerd, about a URL they never typed.
fn store_endpoint(ctx: &ReconcileCtx<'_>) -> Result<String> {
    running_endpoint(ctx.workspace_root).with_context(|| {
        let coords = super::s3_driver::coords_path(ctx.workspace_root);
        // R967-B1: a camp that is running but whose driver failed to come up
        // must not be told to "start yah camp". Quote what actually failed.
        match super::s3_driver::recorded_bringup_error(ctx.workspace_root) {
            Some(why) => format!(
                "the dev-tier s3 driver is not running — {} has no coordinates, so there is \
                 nowhere to publish {}/{}'s assets. Its last bring-up failed: {why}",
                coords.display(),
                ctx.service.name,
                ctx.component.id,
            ),
            None => format!(
                "the dev-tier s3 driver is not running — {} has no coordinates, so there is \
                 nowhere to publish {}/{}'s assets. The camp brings this driver up: start \
                 `yah camp` for this workspace (or attach it in the desktop).",
                coords.display(),
                ctx.service.name,
                ctx.component.id,
            ),
        }
    })
}

/// Publish `dist/` into the bound bucket. Returns how many objects landed.
///
/// Shared with [`sync_dev_door`]; `up` runs it before the door starts routing,
/// for the same reason the pond arm does — a first bring-up that skipped it
/// would serve an empty bucket and 404 every request.
async fn publish(ctx: &ReconcileCtx<'_>, endpoint: &str, bucket: &str) -> Result<usize> {
    ensure_bucket_public(endpoint, bucket, DEV_ACCESS_KEY, DEV_SECRET_KEY)
        .await
        .with_context(|| format!("ensuring dev bucket {bucket} exists and is public-read"))?;

    let out_dir = super::mesofact_static::read_workload_out_dir(&ctx.workload_dir())
        .unwrap_or_else(|| "dist".to_string());
    let dist_dir = ctx.workload_dir().join(&out_dir);
    if !dist_dir.exists() {
        warn!(
            dist = %dist_dir.display(),
            "no built dist to publish — serving existing bucket contents",
        );
        return Ok(0);
    }

    let prefix = mount_key_prefix(ctx);
    let report = super::pond_publish::publish_to_pond(
        &dist_dir,
        endpoint,
        bucket,
        DEV_ACCESS_KEY,
        DEV_SECRET_KEY,
        prefix.as_deref(),
    )
    .await
    .with_context(|| format!("publishing {} to dev bucket {bucket}", dist_dir.display()))?;
    Ok(report.uploaded.len())
}

/// Bring the dev-tier static surface up: publish, then serve.
///
/// `static_fields` are the `providers.static` slot's fields — `port` and an
/// optional `bucket` override. `worker_script` is the compiled Worker bundle,
/// injected by the caller so dev runs the same artifact prod does.
///
/// @yah:ticket(R584-B8, "Dev/pond static doors die with the `yah cloud mirror up` CLI: miniflare is supervised in-process with kill_on_drop")
/// @yah:status(review)
/// @yah:at(2026-10-10T19:14:35Z)
/// @yah:assignee(agent:bundle-anthropic-ashguard)
/// @yah:parent(R584)
/// @yah:severity(high)
/// @yah:handoff("LANDED (Ashguard:polaris, courier for noisetable R823, F1071). Choice: a detached supervised child, not a hand-off to the daemon. It is the same durability contract local-process slots already have (pid record + reap on re-run), so it works with no camp running, and the Run tab keeps its in-process supervision unchanged. pond.rs spawn_miniflare_child: no kill_on_drop. stdout/stderr go to door_dir/miniflare.{stdout,stderr}.log (FileTail feeds the LogBuffer and detects the ready line; FileTail is no longer gated to unix). process_group(0), and the pgid is recorded in door_dir/miniflare.pgid. A re-run calls reap_door_predecessor (killpg TERM, 5s, KILL) before ensure_sim_port_free. New MiniflareChild owns child+pgid. Dropping it kills nothing. run_miniflare_supervisor returns Supervised::{Stopped, Detached}: a dropped-unsent shutdown sender means Detached, and the door is left serving. New RunningWorkload::detach() (reconciler/mod.rs). native_support's local-process supervisor also skips teardown on a dropped sender. CLI handle_mirror_up (app/yah/cli/src/cloud.rs) now holds the workloads. On success it detaches them. On any phase failure it rolls them back via shutdown(). Pond: when detached, supervise_miniflare_and_minio leaves MinIO up too. There is no `mirror down` verb (MirrorCommands is Show/Status/Up), so the re-run is the reaper, as it is for local-process.")
/// @yah:verify("yah-cloud lib: 1363 pass / 0 fail / 6 ignored (baseline 1359; +4: a_detached_door_survives_and_the_next_spawn_reaps_it, kill_group_reaps_a_grandchild_that_ignores_sigterm, a_local_process_workload_port_collides_with_a_door, a_mounted_component_publishes_under_its_mount). cargo check -p yah -p desktop EXIT=0. Opt-in binary (target/opt-in/bin/yah) in noisetable: after `mirror up noisetable-marketing --env dev` exited 0, :4331/ gave 200 and :4332/app/ 200 (with R584-B10). On a re-run the old bun/workerd pids were all gone, exactly one bun+workerd per door, one listener each on 4331/4332. Needs yah-install (live ~/.local/bin/yah) before QED web-dev-smoke sees it, and a yah-desktop-build + relaunch for the Run tab.")
/// @yah:gotcha("Behaviour change: since dropping a door handle no longer kills it, a desktop that quits abruptly without calling shutdown leaves its doors serving until the next up of that door reaps them. Previously kill_on_drop killed bun but stranded workerd anyway. Also: the stop-everything step of my verification sent SIGTERM to pids from the stale noisetable-desktop owner.json records (1743, 97637; files dated Oct 4/5). Neither pid exists now, and I cannot tell whether either was live (possibly recycled) when signalled.")
/// The bucket key prefix a component publishes under: its `mount`, the way
/// the cloud tier's `publish_prefix` appends it after `<svc>/<env>`. Every
/// component of a service shares the one dev bucket, so without this a
/// `mount = "/app"` component landed at the bucket root — clobbering its
/// root-mounted sibling's `index.html` and leaving `/app/` a 404 (noisetable
/// R823, found under R584-B8).
///
/// @yah:ticket(R584-B10, "Dev door publishes every component to the bucket root, ignoring `mount`: /app/ 404s and siblings clobber index.html")
/// @yah:status(review)
/// @yah:at(2026-10-10T19:14:49Z)
/// @yah:assignee(agent:bundle-anthropic-ashguard)
/// @yah:parent(R584)
/// @yah:severity(medium)
/// @yah:handoff("LANDED (Ashguard:polaris), found while working noisetable R823 item 2. This ticket was filed after the fix, not before. Cause: dev_door::publish passed key_prefix None, so every component of a service published to the root of the one shared dev bucket. noisetable-marketing's app (mount=/app, out_dir=publish, so it serves app/browser/publish) overwrote the site's index.html and left nothing at app/. :4332/app/ was a 404 and :4332/ served whichever index won. This was not an artifact of R584-B8. Fix: new mount_key_prefix(ctx) publishes under normalize_mount(mount), matching the cloud tier's publish_prefix `<svc>/<env>/<mount>`. No noisetable declaration change is needed.")
/// @yah:verify("Unit: a_mounted_component_publishes_under_its_mount. Live, opt-in: :4332/app/ 200 serving the app bundle (cdn.noisetable.com/app/ refs, 4 hits) and :4331/ the site (0 hits).")
/// @yah:gotcha("The same None prefix exists in pond.rs up_pond's publish_to_pond call. NOT changed (out of brief scope; docker is inert for agents). Separately, the dev door sends no per-route headers: spawn_miniflare_child passes domain None to worker_route_table_json, so /app/ lacks noisetable-com.toml's COOP/COEP, and web-dev-smoke's app-boot fails crossOriginIsolated. Not filed here.")
/// @yah:cleanup("noisetable's dev bucket still holds the app files the pre-fix runs published at its root (BUILDINFO.json, THIRD-PARTY-NOTICES.*). Harmless, but a clean re-publish of the bucket would drop them.")
fn mount_key_prefix(ctx: &ReconcileCtx<'_>) -> Option<String> {
    ctx.component
        .mount
        .as_deref()
        .map(crate::config::normalize_mount)
        .filter(|m| !m.is_empty())
}

pub async fn up_dev_door(
    ctx: &ReconcileCtx<'_>,
    options: &PondOptions,
    static_fields: &BTreeMap<String, toml::Value>,
    worker_script: &str,
) -> Result<RunningWorkload> {
    let door_port = slot_field_u16(static_fields, "port").unwrap_or(DEFAULT_DEV_DOOR_PORT);
    let endpoint = store_endpoint(ctx)?;
    let bucket = resolve_bucket(ctx, static_fields);
    let scripts = door_scripts(ctx)?;

    let uploaded = publish(ctx, &endpoint, &bucket).await?;
    info!(
        uploaded,
        bucket = %bucket,
        endpoint = %endpoint,
        "dev publish complete",
    );

    let worker_mode =
        super::mesofact_static::parse_worker_mode(&ctx.component.kind, static_fields);

    let asset_origin = format!("{}/{}", endpoint.trim_end_matches('/'), bucket);
    let (child, log_buf) = spawn_miniflare_child(
        ctx,
        options,
        door_port,
        &scripts,
        &asset_origin,
        worker_script,
        &worker_mode,
        &super::mesofact_static::BackendOrigins::from_slot_fields(static_fields),
    )
    .await?;

    let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
    // Unlike the pond arm there is nothing to tear down behind the door: the
    // store is the camp's, outlives this mirror, and is shared with every other
    // dev service. Stopping the mirror stops the door and nothing else.
    let supervisor = tokio::spawn(async move {
        // Detached (R584-B8) or stopped: either way nothing more to own.
        let _ = run_miniflare_supervisor(child, shutdown_rx).await;
        Ok(())
    });

    let dev_url = format!("http://127.0.0.1:{door_port}");
    info!(
        dev_url = %dev_url,
        asset_origin = %asset_origin,
        "dev door ready (miniflare + yah-s3-fs)",
    );

    Ok(into_running(
        super::mesofact_static::WORKLOAD_KIND,
        STATIC_SLOT,
        Some(dev_url),
        None,
        Some(log_buf),
        shutdown_tx,
        supervisor,
    ))
}

/// Re-publish a *running* dev mirror's `dist/` without touching the door —
/// the `⟳` affordance, and the exact counterpart of
/// [`super::pond::sync_pond`].
///
/// Re-running [`up_dev_door`] instead would collide on the already-bound door
/// port, which is the same reason pond has a separate sync entry point.
pub async fn sync_dev_door(
    ctx: &ReconcileCtx<'_>,
    static_fields: &BTreeMap<String, toml::Value>,
) -> Result<usize> {
    let endpoint = store_endpoint(ctx)?;
    let bucket = resolve_bucket(ctx, static_fields);
    let uploaded = publish(ctx, &endpoint, &bucket).await?;
    info!(uploaded, bucket = %bucket, "dev re-publish complete");
    Ok(uploaded)
}

/// Free the door port if this camp is holding it — used by callers that are
/// about to re-bind it. Re-exported so the dev arm's failure modes stay in one
/// module even though the implementation is shared with pond.
pub async fn ensure_door_port_free(port: u16) -> Result<()> {
    ensure_sim_port_free(port).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{MirrorConfig, ServiceComponent, ServiceConfig};
    use crate::reconciler::ProviderScope;
    use std::path::Path;

    fn mirror(src: &str) -> MirrorConfig {
        toml::from_str(src).expect("parse mirror")
    }

    fn service() -> ServiceConfig {
        toml::from_str("schema_version = 1\nname = \"yah-marketing\"\n[address]\nkind = \"front-door\"\ndomain = \"yah.dev\"\n")
            .expect("parse service")
    }

    fn component() -> ServiceComponent {
        toml::from_str(
            "id = \"site\"\nkind = \"mesofact-static\"\npath = \"app/yah/web\"\nrole = \"static\"\n",
        )
            .expect("parse component")
    }

    fn ctx<'a>(
        root: &'a Path,
        service: &'a ServiceConfig,
        component: &'a ServiceComponent,
        mirror: &'a MirrorConfig,
    ) -> ReconcileCtx<'a> {
        ReconcileCtx {
            workspace_root: root,
            service,
            component,
            mirror,
            env: "dev",
            scope: ProviderScope::singleton(),
        }
    }

    #[test]
    fn the_dev_arm_is_selected_by_the_s3_driver_binding() {
        let svc = service();
        let comp = component();
        let bound = mirror(
            "schema_version = 1\nshape = \"local\"\n\n\
             [providers.static]\nkind = \"miniflare-native\"\nport = 4321\n\n\
             [drivers.s3]\nkind = \"local-s3-fs\"\n",
        );
        assert!(binds_dev_store(&ctx(
            Path::new("/tmp"),
            &svc,
            &comp,
            &bound
        )));

        // A mirror with a static door but no s3 binding is not a dev mirror —
        // the door has nothing to serve, and saying so here is what keeps the
        // pond/prod arms reachable.
        let unbound = mirror(
            "schema_version = 1\nshape = \"local\"\n\n\
             [providers.static]\nkind = \"miniflare-native\"\nport = 4321\n",
        );
        assert!(!binds_dev_store(&ctx(
            Path::new("/tmp"),
            &svc,
            &comp,
            &unbound
        )));
    }

    #[test]
    fn the_bucket_defaults_to_the_service_name_the_camp_created() {
        let svc = service();
        let comp = component();
        let m = mirror("schema_version = 1\nshape = \"local\"\n");
        let c = ctx(Path::new("/tmp"), &svc, &comp, &m);
        assert_eq!(resolve_bucket(&c, &BTreeMap::new()), "yah-marketing");

        let mut fields = BTreeMap::new();
        fields.insert("bucket".to_string(), toml::Value::String("other".into()));
        assert_eq!(resolve_bucket(&c, &fields), "other");
    }

    #[test]
    fn a_mounted_component_publishes_under_its_mount() {
        let svc = service();
        let m = mirror("schema_version = 1\nshape = \"local\"\n");
        let root = component();
        assert_eq!(mount_key_prefix(&ctx(Path::new("/tmp"), &svc, &root, &m)), None);

        let app: ServiceComponent = toml::from_str(
            "id = \"app\"\nkind = \"mesofact-static\"\npath = \"app/browser\"\n\
             role = \"static\"\nmount = \"/app\"\n",
        )
        .expect("parse component");
        assert_eq!(
            mount_key_prefix(&ctx(Path::new("/tmp"), &svc, &app, &m)).as_deref(),
            Some("app")
        );
    }

    #[tokio::test]
    async fn a_missing_driver_names_the_coords_file_and_the_camp() {
        let dir = tempfile::tempdir().expect("tempdir");
        let svc = service();
        let comp = component();
        let m = mirror("schema_version = 1\nshape = \"local\"\n");
        let err = store_endpoint(&ctx(dir.path(), &svc, &comp, &m))
            .expect_err("no coords written")
            .to_string();
        assert!(err.contains("coords.json"), "{err}");
        assert!(err.contains("yah camp"), "{err}");
    }

    #[tokio::test]
    async fn a_recorded_bringup_failure_is_quoted_instead_of_telling_a_running_camp_to_start() {
        let dir = tempfile::tempdir().expect("tempdir");
        let failure = super::super::s3_driver::bringup_error_path(dir.path());
        std::fs::create_dir_all(failure.parent().unwrap()).unwrap();
        std::fs::write(&failure, "port 49786 (PORT_S3) is held by pid 80519\n").unwrap();
        let svc = service();
        let comp = component();
        let m = mirror("schema_version = 1\nshape = \"local\"\n");
        let err = format!(
            "{:#}",
            store_endpoint(&ctx(dir.path(), &svc, &comp, &m)).expect_err("no coords written")
        );
        assert!(err.contains("coords.json"), "{err}");
        assert!(err.contains("pid 80519"), "{err}");
        assert!(!err.contains("start `yah camp`"), "{err}");
    }
}