Skip to content

dev: unify the module runner in Rust (run_inner) so the in-container log shipper runs #37

Description

@I-am-nothing

Problem

The dev module log pipeline is built and verified end-to-end except the final hop: the runner container never ships module logs to dispatch, so the developer Logcat (/dev/module/<slug>/service) stays empty.

Root cause: there are two parallel module runners, and the container runs the one without shipping.

  • Rust run_inner (src/commands/dev/mod.rs, invoked by mirrorstack dev --all): spawns each module, taps stdout/stderr, and runs the log shipper (log_shipper.rs, feat(dev): ship runner module logs to dispatch (log pipeline) #36) — batch-POSTs JSON lines to $MS_DISPATCH_URL/internal/modules/<id>/logs with X-MS-Service-Secret. Has shipping; lacks hot-reload + proxy.
  • Shell dev-runner.sh (untracked, in the e2e worktree ms-app-modules/scripts/): runs air per module (polling), a dev-proxy.go on :8080, esbuild web watchers + livereload. Has hot-reload + proxy; no shipping.

The e2e compose sets command: sh /modules/scripts/dev-runner.sh, so the container runs the shell runner and the Rust run_inner (with its shipper) never executes. Confirmed: zero ingest hits at dispatch.

Goal

One canonical Rust runner (run_inner) at full feature parity, so the container runs mirrorstack dev --all and dev-runner.sh + dev-proxy.go can be retired. Shipping then works for free.

Parity gap — what run_inner must gain

Today run_inner does plain go run ./<module> + the shipper. To replace dev-runner.sh it must also:

  1. Hot-reload with pollingdev-runner.sh runs air with poll = true, poll_interval = 2000 because OrbStack bind mounts don't deliver inotify; go run never rebuilds. Spawn air per module from Rust (preferred) or a poll→build→restart loop.
  2. Per-module internal ports (18080+) via PORT.
  3. Per-module platform token filesMS_PLATFORM_TOKEN_FILE=/modules/.ms-platform-token-<slug> (dispatch mints a distinct service token per tunnel connection; a shared file 401s all but the first module).
  4. Web watcher + livereload — for modules with web/esbuild.config.mjs: npm install + node esbuild.config.mjs --watch with LR_PORT (8089+).
  5. Dev-proxy on :8080 — route /_m/<slug>/ → the module's internal port. Launch the existing dev-proxy.go from Rust, or port it.
  6. Keep: stdout/stderr tap → log_shipper (already present) + [<slug>] prefixing.

Runner image

The runner image is golang:1.26-alpine + nodejs npm sed grep air (built via dockerfile_inline in the e2e compose). It does not contain the mirrorstack binary. To run mirrorstack dev --all in-container, bake the binary into the image (or mount it). Decide as part of this work.

Implementation plan (mirrorstack-cli)

  1. Refactor run_inner to spawn air per module (mirroring dev-runner.sh's generated .air.<mod>.toml) instead of go run, keeping stdout piped through spawn_forwarderlog_shipper.
  2. Add per-module port assignment + PORT + MS_PLATFORM_TOKEN_FILE.
  3. Add the esbuild web watcher + livereload for modules that have web/esbuild.config.mjs.
  4. Launch/embed the dev-proxy (:8080 route map).
  5. Generate the air configs from Rust (or ship them as templates).

Migration / retirement

  • Runner image: include the mirrorstack binary.
  • e2e ms-app-modules/docker-compose.yml: command: mirrorstack dev --all (replaces sh dev-runner.sh); keep the MS_INTERNAL_SECRET: ${MS_INTERNAL_SECRET:-} forward (already added locally — fixed a separate gap where the minted secret never reached the container).
  • Delete scripts/dev-runner.sh + scripts/dev-proxy.go (untracked e2e scaffolding) once parity is verified.

Risks / open questions

  • OrbStack inotify: hot-reload MUST poll (no inotify across the macOS bind mount).
  • Keep dev-proxy.go as a launched helper vs. port routing into Rust.
  • The shipper's per-module secret must equal the live tunnel session's InternalSecret — both derive from the minted per-session secret; just ensure the container env carries it.
  • Livereload SSE ports (8089+) must stay published in compose.

Acceptance criteria

  • Container runs mirrorstack dev --all (no shell runner).
  • Editing a module .go triggers an air rebuild (polling).
  • /_m/<slug>/ proxy + web livereload still work.
  • A request to a module emits a {"msg":"request",...} access line (SDK app-module-sdk#128, merged) that lands in dispatch's ring and shows in the Logcat.

Context / related

  • feat(dev): ship runner module logs to dispatch (log pipeline) #36 — the Rust log shipper (merged; run_inner only).
  • app-module-sdk#128 — SDK auto access-log so modules emit (merged; oauth-core emit verified).
  • Empty-Logcat diagnosis chain: stale CLI binary → stale SDK checkout → MS_INTERNAL_SECRET compose-forward gap → this runner divergence.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions