Skip to content

Migrate Maestro web control plane to Rust#206

Merged
haasonsaas merged 59 commits into
mainfrom
jonathan/rust-control-plane-maestro
Apr 28, 2026
Merged

Migrate Maestro web control plane to Rust#206
haasonsaas merged 59 commits into
mainfrom
jonathan/rust-control-plane-maestro

Conversation

@haasonsaas

@haasonsaas haasonsaas commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a Rust maestro-control-plane package that serves the web control plane, static UI assets, status/model/chat endpoints, and common local UI APIs
  • split Docker web and Rust build stages so Rust edits do not invalidate Bun/web build caches
  • make the runtime image Alpine + Rust binary + static web assets only, with no Node binary and no Node compatibility proxy

Verification

  • cargo fmt --check
  • cargo check --bin maestro-control-plane
  • cargo test --bin maestro-control-plane
  • cargo clippy --bin maestro-control-plane -- -D warnings
  • docker build -t maestro-rust-control-plane-migrate .
  • container smoke: status/models/model/files/commands/config/usage/run/background/undo/changes/framework/tools/review/context/stats/telemetry/training all 200; unmigrated route 501; no Node binary in runtime

Source provenance

Notes

  • cold Rust graph still compiles through maestro-tui because NativeAgent currently lives there; extracting an agent-core crate is the next build-size reduction.

@cursor

cursor Bot commented Apr 27, 2026

Copy link
Copy Markdown

PR Summary

High Risk
High risk because it changes the production runtime entrypoint from Node to a newly introduced Rust control-plane binary and restructures the container build/publish pipeline, which could impact request handling, auth/CORS behavior, and deployment stability.

Overview
Migrates the web control plane runtime to Rust. The Docker image now builds the web UI assets separately and runs a new maestro-control-plane Rust binary as the container CMD, removing the Node runtime from the final image.

Adds a new Rust control plane package. Introduces packages/control-plane-rs (new Cargo crate + lockfile) including an internal HTTP parsing/response layer (src/http.rs) with request size limits, CORS origin rewriting, and response helpers.

Adjusts build/ops wiring. Updates .github/workflows/ghcr-publish.yml to publish images only on pushes to main (no PR image builds), extends .dockerignore, and adds npm scripts to build/check the Rust control plane plus a web:rust-control runner.

Reviewed by Cursor Bugbot for commit db6705f. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

This PR changes mirrored Maestro source files in the public repo, but it does not link the matching private source-of-truth PR.

Add one of these to the PR body, then re-run the check:

  • https://github.com/evalops/maestro-internal/pull/<number>
  • evalops/maestro-internal#<number>
  • maestro-internal#<number>

Mirrored files touched:

  • .dockerignore
  • Dockerfile
  • package.json
  • packages/control-plane-rs/Cargo.lock
  • packages/control-plane-rs/Cargo.toml
  • packages/control-plane-rs/src/main.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: HEAD response sends incorrect Content-Length of zero
    • The Rust control plane now preserves the GET payload size in Content-Length for HEAD static responses while still omitting the body.
  • ✅ Fixed: Chat SSE stream missing terminal event on channel close
    • The chat SSE handler now emits a fallback error and done event when the agent stream closes before a normal completion event arrives.
Preview (54313ff060)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig openssl-dev openssl-libs-static && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
 
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
+
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
+WORKDIR /app
+
+COPY proto ./proto
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    --mount=type=cache,target=/app/packages/control-plane-rs/target \
+    cd packages/control-plane-rs && \
+    cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
 # ---------- runner ----------
-FROM node:22-alpine AS runner
+FROM alpine:3.23 AS runner
 WORKDIR /app
 
-ENV NODE_ENV=production
-
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates openssl && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.lock
@@ -1,0 +1,3714 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "euclid"
+version = "0.20.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "eventsource-stream"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
+dependencies = [
+ "futures-core",
+ "nom",
+ "pin-project-lite",
... diff truncated: showing 800 of 6558 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b0bc2c6dd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Temp files leak when SSE streaming fails mid-chat
    • Added a Drop cleanup fallback for PreparedAttachments and made explicit cleanup consume the temp dir so attachment files are removed on every exit path.
  • ✅ Fixed: Server exits on transient accept errors
    • Wrapped listener.accept() in a retrying match that logs transient failures, backs off briefly, and continues accepting connections.
Preview (323af4b8a5)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig openssl-dev openssl-libs-static && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
 
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
+
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
+WORKDIR /app
+
+COPY proto ./proto
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    --mount=type=cache,target=/app/packages/control-plane-rs/target \
+    cd packages/control-plane-rs && \
+    cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
 # ---------- runner ----------
-FROM node:22-alpine AS runner
+FROM alpine:3.23 AS runner
 WORKDIR /app
 
-ENV NODE_ENV=production
-
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates openssl && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.lock
@@ -1,0 +1,3714 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "euclid"
+version = "0.20.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "eventsource-stream"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
+dependencies = [
+ "futures-core",
+ "nom",
+ "pin-project-lite",
... diff truncated: showing 800 of 6847 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/main.rs Outdated
@haasonsaas haasonsaas force-pushed the jonathan/rust-control-plane-maestro branch from 5fd4edd to 323af4b Compare April 27, 2026 17:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20726a092a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Git status counts miss staged and renamed files
    • I replaced the narrow porcelain prefix checks with a parser that classifies index/worktree statuses and added coverage for staged, renamed, copied, deleted, conflicted, and untracked entries.
  • ✅ Fixed: Telemetry and training overrides have confusingly inverted boolean semantics
    • I replaced the shared Option<bool> overrides with distinct TelemetryOverride and TrainingOverride enums so each endpoint uses explicit, self-documenting semantics.
Preview (58dfc8e05a)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@ coverage/
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

@@ -12,6 +12,7 @@ coverage/
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@ RUN apk add --no-cache python3 make g++ git
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/

@@ -8,6 +8,7 @@ RUN apk add --no-cache python3 make g++ git
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@ COPY packages/slack-agent-ui/package.json packages/slack-agent-ui/
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig openssl-dev openssl-libs-static && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
 
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
-# ---------- runner ----------
-FROM node:22-alpine AS runner
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
 WORKDIR /app
 
-ENV NODE_ENV=production
+COPY proto ./proto
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    --mount=type=cache,target=/app/packages/control-plane-rs/target \
+    cd packages/control-plane-rs && \
+    cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
+# ---------- runner ----------
+FROM alpine:3.23 AS runner
+WORKDIR /app
 
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates openssl nodejs npm && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

@@ -19,55 +20,80 @@ COPY packages/slack-agent-ui/package.json packages/slack-agent-ui/
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig openssl-dev openssl-libs-static && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
 
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
-# ---------- runner ----------
-FROM node:22-alpine AS runner
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
 WORKDIR /app
 
-ENV NODE_ENV=production
+COPY proto ./proto
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    --mount=type=cache,target=/app/packages/control-plane-rs/target \
+    cd packages/control-plane-rs && \
+    cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
+# ---------- runner ----------
+FROM alpine:3.23 AS runner
+WORKDIR /app
 
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates openssl nodejs npm && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",

@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644

diff --git a/packages/control-plane-rs/Cargo.toml b/packages/control-plane-rs/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "maestro-control-plane"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+description = "Native Rust HTTP control plane for Maestro"
+
+[[bin]]
+name = "maestro-control-plane"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1"
+base64 = "0.22"
+chrono = "0.4"
+maestro-tui = { path = "../tui-rs" }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "net", "process", "sync", "time", "fs"] }

@@ -0,0 +1,19 @@
+[package]
+name = "maestro-control-plane"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+description = "Native Rust HTTP control plane for Maestro"
+
+[[bin]]
+name = "maestro-control-plane"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1"
+base64 = "0.22"
+chrono = "0.4"
+maestro-tui = { path = "../tui-rs" }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "net", "process", "sync", "time", "fs"] }

diff --git a/packages/control-plane-rs/src/main.rs b/packages/control-plane-rs/src/main.rs
new file mode 100644

diff --git a/test/db/migration-files.test.ts b/test/db/migration-files.test.ts
--- a/test/db/migration-files.test.ts
+++ b/test/db/migration-files.test.ts
@@ -70,10 +70,14 @@ describe("database migration packaging", () => {
 		);
 	});
 
-	it("keeps Docker and build scripts wired to ship migrations", () => {
+	it("keeps build scripts wired to ship migrations without the legacy Node image copy", () => {
 		const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8");
+		expect(dockerfile).toContain("RUN bun run build:all");
 		expect(dockerfile).toContain(
-			"COPY --from=builder /app/src/db/migrations ./dist/db/migrations",
+			"COPY --from=web-builder /app/packages/web/dist ./packages/web/dist",
+		);
+		expect(dockerfile).not.toContain(
+			"COPY --from=builder /app/src/db/migrations",
 		);
 
 		const packageJson = JSON.parse(

@@ -70,10 +70,14 @@ describe("database migration packaging", () => {
 		);
 	});
 
-	it("keeps Docker and build scripts wired to ship migrations", () => {
+	it("keeps build scripts wired to ship migrations without the legacy Node image copy", () => {
 		const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8");
+		expect(dockerfile).toContain("RUN bun run build:all");
 		expect(dockerfile).toContain(
-			"COPY --from=builder /app/src/db/migrations ./dist/db/migrations",
+			"COPY --from=web-builder /app/packages/web/dist ./packages/web/dist",
+		);
+		expect(dockerfile).not.toContain(
+			"COPY --from=builder /app/src/db/migrations",
 		);
 
 		const packageJson = JSON.parse(

You can send follow-ups to the cloud agent here.

Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58dfc8e05a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread Dockerfile Outdated
@cursor

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce11f0e44c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread packages/control-plane-rs/src/main.rs Outdated
Comment thread Dockerfile
Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/http.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: HTTP header parsing silently drops duplicate header values
    • parse_request_head now merges duplicate header values instead of overwriting them, with a regression test covering repeated headers.
Preview (296eccc4b0)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/.github/workflows/ghcr-publish.yml b/.github/workflows/ghcr-publish.yml
--- a/.github/workflows/ghcr-publish.yml
+++ b/.github/workflows/ghcr-publish.yml
@@ -2,12 +2,14 @@
 
 on:
   pull_request:
+    branches:
+      - main
   push:
     branches:
       - main
 
 concurrency:
-  group: ghcr-publish-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  group: ghcr-publish-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
 env:
@@ -28,7 +30,7 @@
         uses: docker/setup-buildx-action@v4
 
       - name: Log in to GHCR
-        if: github.event_name != 'pull_request'
+        if: github.event_name == 'push'
         uses: docker/login-action@v4
         with:
           registry: ghcr.io
@@ -42,7 +44,6 @@
           images: ${{ env.IMAGE_NAME }}
           tags: |
             type=ref,event=branch
-            type=ref,event=pr
             type=sha,prefix=sha-
             type=raw,value=latest,enable={{is_default_branch}}
 
@@ -52,7 +53,7 @@
           context: .
           file: ./Dockerfile
           platforms: linux/amd64
-          push: ${{ github.event_name != 'pull_request' }}
+          push: ${{ github.event_name == 'push' }}
           load: ${{ github.event_name == 'pull_request' }}
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
 
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
+
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
+WORKDIR /app
+
+COPY proto ./proto
+COPY --from=web-builder /app/packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    cd packages/control-plane-rs && \
+    CARGO_TARGET_DIR=/app/rust-target cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp /app/rust-target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
 # ---------- runner ----------
-FROM node:22-alpine AS runner
+FROM alpine:3.23 AS runner
 WORKDIR /app
 
-ENV NODE_ENV=production
-
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates libstdc++ && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/docs/MODELS.md b/docs/MODELS.md
--- a/docs/MODELS.md
+++ b/docs/MODELS.md
@@ -23,6 +23,19 @@
 
 Paths are read in that order, later entries overriding earlier ones.
 
+The Rust control plane keeps the same local fallback, and can also hydrate
+`GET /api/models` from the llm-gateway model catalog before applying local
+Maestro overrides:
+
+- `MAESTRO_LLM_GATEWAY_MODELS_URL` points directly at the catalog endpoint.
+- `MAESTRO_LLM_GATEWAY_URL` derives the catalog URL as `<base>/v1/models`.
+- `MAESTRO_LLM_GATEWAY_TOKEN` is sent as a bearer token when set.
+- `MAESTRO_LLM_GATEWAY_ORG_ID` is sent as `X-Organization-ID` when set.
+- `MAESTRO_LLM_GATEWAY_TIMEOUT_MS` defaults to `2500`.
+
+If the gateway URL is unset, unavailable, or returns invalid JSON, Maestro falls
+back to the built-in models and `~/.maestro/models.json`.
+
 ## Format
 
 Custom config files accept:

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.lock
@@ -1,0 +1,3665 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
... diff truncated: showing 800 of 12019 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/control-plane-rs/src/http.rs Outdated
@cursor

cursor Bot commented Apr 27, 2026

Copy link
Copy Markdown

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: OPTIONS preflight requests return 404 instead of CORS headers
    • handle_local_endpoint now returns an immediate 204 response for /api/* OPTIONS requests and a regression test covers the preflight path.
  • ✅ Fixed: Duplicate percent-decoding functions with near-identical logic
    • Query parsing now reuses percent_decode_component, removing the duplicate decoder so future fixes stay centralized.
Preview (89a9a66c45)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/.github/workflows/ghcr-publish.yml b/.github/workflows/ghcr-publish.yml
--- a/.github/workflows/ghcr-publish.yml
+++ b/.github/workflows/ghcr-publish.yml
@@ -2,12 +2,14 @@
 
 on:
   pull_request:
+    branches:
+      - main
   push:
     branches:
       - main
 
 concurrency:
-  group: ghcr-publish-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  group: ghcr-publish-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
 env:
@@ -28,7 +30,7 @@
         uses: docker/setup-buildx-action@v4
 
       - name: Log in to GHCR
-        if: github.event_name != 'pull_request'
+        if: github.event_name == 'push'
         uses: docker/login-action@v4
         with:
           registry: ghcr.io
@@ -42,7 +44,6 @@
           images: ${{ env.IMAGE_NAME }}
           tags: |
             type=ref,event=branch
-            type=ref,event=pr
             type=sha,prefix=sha-
             type=raw,value=latest,enable={{is_default_branch}}
 
@@ -52,7 +53,7 @@
           context: .
           file: ./Dockerfile
           platforms: linux/amd64
-          push: ${{ github.event_name != 'pull_request' }}
+          push: ${{ github.event_name == 'push' }}
           load: ${{ github.event_name == 'pull_request' }}
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
 
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
+
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
+WORKDIR /app
+
+COPY proto ./proto
+COPY --from=web-builder /app/packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    cd packages/control-plane-rs && \
+    CARGO_TARGET_DIR=/app/rust-target cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp /app/rust-target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
 # ---------- runner ----------
-FROM node:22-alpine AS runner
+FROM alpine:3.23 AS runner
 WORKDIR /app
 
-ENV NODE_ENV=production
-
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates libstdc++ && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/docs/MODELS.md b/docs/MODELS.md
--- a/docs/MODELS.md
+++ b/docs/MODELS.md
@@ -23,6 +23,19 @@
 
 Paths are read in that order, later entries overriding earlier ones.
 
+The Rust control plane keeps the same local fallback, and can also hydrate
+`GET /api/models` from the llm-gateway model catalog before applying local
+Maestro overrides:
+
+- `MAESTRO_LLM_GATEWAY_MODELS_URL` points directly at the catalog endpoint.
+- `MAESTRO_LLM_GATEWAY_URL` derives the catalog URL as `<base>/v1/models`.
+- `MAESTRO_LLM_GATEWAY_TOKEN` is sent as a bearer token when set.
+- `MAESTRO_LLM_GATEWAY_ORG_ID` is sent as `X-Organization-ID` when set.
+- `MAESTRO_LLM_GATEWAY_TIMEOUT_MS` defaults to `2500`.
+
+If the gateway URL is unset, unavailable, or returns invalid JSON, Maestro falls
+back to the built-in models and `~/.maestro/models.json`.
+
 ## Format
 
 Custom config files accept:

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.lock
@@ -1,0 +1,3665 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
... diff truncated: showing 800 of 11990 lines

You can send follow-ups to the cloud agent here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 296eccc4b0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/main.rs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Query flag treats present-but-valueless params as false
    • The branch now treats present-but-valueless query params as enabled in query_flag, with regression coverage for bare and explicit false values.
Preview (45b0f532ad)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/.github/workflows/ghcr-publish.yml b/.github/workflows/ghcr-publish.yml
--- a/.github/workflows/ghcr-publish.yml
+++ b/.github/workflows/ghcr-publish.yml
@@ -2,12 +2,14 @@
 
 on:
   pull_request:
+    branches:
+      - main
   push:
     branches:
       - main
 
 concurrency:
-  group: ghcr-publish-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  group: ghcr-publish-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
 env:
@@ -28,7 +30,7 @@
         uses: docker/setup-buildx-action@v4
 
       - name: Log in to GHCR
-        if: github.event_name != 'pull_request'
+        if: github.event_name == 'push'
         uses: docker/login-action@v4
         with:
           registry: ghcr.io
@@ -42,7 +44,6 @@
           images: ${{ env.IMAGE_NAME }}
           tags: |
             type=ref,event=branch
-            type=ref,event=pr
             type=sha,prefix=sha-
             type=raw,value=latest,enable={{is_default_branch}}
 
@@ -52,7 +53,7 @@
           context: .
           file: ./Dockerfile
           platforms: linux/amd64
-          push: ${{ github.event_name != 'pull_request' }}
+          push: ${{ github.event_name == 'push' }}
           load: ${{ github.event_name == 'pull_request' }}
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
 
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
+
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
+WORKDIR /app
+
+COPY proto ./proto
+COPY --from=web-builder /app/packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    cd packages/control-plane-rs && \
+    CARGO_TARGET_DIR=/app/rust-target cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp /app/rust-target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
 # ---------- runner ----------
-FROM node:22-alpine AS runner
+FROM alpine:3.23 AS runner
 WORKDIR /app
 
-ENV NODE_ENV=production
-
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates libstdc++ && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/docs/MODELS.md b/docs/MODELS.md
--- a/docs/MODELS.md
+++ b/docs/MODELS.md
@@ -23,6 +23,19 @@
 
 Paths are read in that order, later entries overriding earlier ones.
 
+The Rust control plane keeps the same local fallback, and can also hydrate
+`GET /api/models` from the llm-gateway model catalog before applying local
+Maestro overrides:
+
+- `MAESTRO_LLM_GATEWAY_MODELS_URL` points directly at the catalog endpoint.
+- `MAESTRO_LLM_GATEWAY_URL` derives the catalog URL as `<base>/v1/models`.
+- `MAESTRO_LLM_GATEWAY_TOKEN` is sent as a bearer token when set.
+- `MAESTRO_LLM_GATEWAY_ORG_ID` is sent as `X-Organization-ID` when set.
+- `MAESTRO_LLM_GATEWAY_TIMEOUT_MS` defaults to `2500`.
+
+If the gateway URL is unset, unavailable, or returns invalid JSON, Maestro falls
+back to the built-in models and `~/.maestro/models.json`.
+
 ## Format
 
 Custom config files accept:

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.lock
@@ -1,0 +1,3665 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
... diff truncated: showing 800 of 12382 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/control-plane-rs/src/http.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45b0f532ad

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/main.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Content-Length/body size mismatch possible in response helpers
    • Added an assertion that rejects non-empty responses whose explicit Content-Length does not match the body bytes, while preserving empty-body HEAD/204-style responses.
Preview (7750b435d0)
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/.github/workflows/ghcr-publish.yml b/.github/workflows/ghcr-publish.yml
--- a/.github/workflows/ghcr-publish.yml
+++ b/.github/workflows/ghcr-publish.yml
@@ -2,12 +2,14 @@
 
 on:
   pull_request:
+    branches:
+      - main
   push:
     branches:
       - main
 
 concurrency:
-  group: ghcr-publish-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  group: ghcr-publish-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
 env:
@@ -21,14 +23,20 @@
       packages: write
 
     steps:
+      - name: Validate publish gate
+        if: github.event_name == 'pull_request'
+        run: echo "Image publishing runs only for pushes to main."
+
       - name: Checkout repository
+        if: github.event_name == 'push'
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
 
       - name: Set up Docker Buildx
+        if: github.event_name == 'push'
         uses: docker/setup-buildx-action@v4
 
       - name: Log in to GHCR
-        if: github.event_name != 'pull_request'
+        if: github.event_name == 'push'
         uses: docker/login-action@v4
         with:
           registry: ghcr.io
@@ -36,24 +44,24 @@
           password: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Extract image metadata
+        if: github.event_name == 'push'
         id: meta
         uses: docker/metadata-action@v6
         with:
           images: ${{ env.IMAGE_NAME }}
           tags: |
             type=ref,event=branch
-            type=ref,event=pr
             type=sha,prefix=sha-
             type=raw,value=latest,enable={{is_default_branch}}
 
       - name: Build image
+        if: github.event_name == 'push'
         uses: docker/build-push-action@v7
         with:
           context: .
           file: ./Dockerfile
           platforms: linux/amd64
-          push: ${{ github.event_name != 'pull_request' }}
-          load: ${{ github.event_name == 'pull_request' }}
+          push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
           cache-from: type=gha

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
 
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
+
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
+WORKDIR /app
+
+COPY proto ./proto
+COPY --from=web-builder /app/packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    cd packages/control-plane-rs && \
+    CARGO_TARGET_DIR=/app/rust-target cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp /app/rust-target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
 # ---------- runner ----------
-FROM node:22-alpine AS runner
+FROM alpine:3.23 AS runner
 WORKDIR /app
 
-ENV NODE_ENV=production
-
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates libstdc++ && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/docs/MODELS.md b/docs/MODELS.md
--- a/docs/MODELS.md
+++ b/docs/MODELS.md
@@ -23,6 +23,19 @@
 
 Paths are read in that order, later entries overriding earlier ones.
 
+The Rust control plane keeps the same local fallback, and can also hydrate
+`GET /api/models` from the llm-gateway model catalog before applying local
+Maestro overrides:
+
+- `MAESTRO_LLM_GATEWAY_MODELS_URL` points directly at the catalog endpoint.
+- `MAESTRO_LLM_GATEWAY_URL` derives the catalog URL as `<base>/v1/models`.
+- `MAESTRO_LLM_GATEWAY_TOKEN` is sent as a bearer token when set.
+- `MAESTRO_LLM_GATEWAY_ORG_ID` is sent as `X-Organization-ID` when set.
+- `MAESTRO_LLM_GATEWAY_TIMEOUT_MS` defaults to `2500`.
+
+If the gateway URL is unset, unavailable, or returns invalid JSON, Maestro falls
+back to the built-in models and `~/.maestro/models.json`.
+
 ## Format
 
 Custom config files accept:

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.lock
@@ -1,0 +1,3665 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "adobe-cmap-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3"
+dependencies = [
+ "pom",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cassowary"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "compact_str"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "futures-core",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
... diff truncated: showing 800 of 12761 lines

You can send follow-ups to the cloud agent here.

Comment thread packages/control-plane-rs/src/http.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7750b435d0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/control-plane-rs/src/main.rs
Comment thread packages/control-plane-rs/src/main.rs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing 409 status maps to wrong reason phrase
    • Added the missing 409-to-Conflict mapping so pending request resume conflicts now emit the correct HTTP reason phrase.
Preview
diff --git a/.dockerignore b/.dockerignore
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,7 @@ coverage/
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

@@ -12,6 +12,7 @@ coverage/
 .DS_Store
 .external/
 packages/tui-rs/target
+packages/control-plane-rs/target
 packages/desktop
 packages/jetbrains-plugin
 packages/vscode-extension

diff --git a/.github/workflows/ghcr-publish.yml b/.github/workflows/ghcr-publish.yml
--- a/.github/workflows/ghcr-publish.yml
+++ b/.github/workflows/ghcr-publish.yml
@@ -2,12 +2,14 @@ name: GHCR Publish
 
 on:
   pull_request:
+    branches:
+      - main
   push:
     branches:
       - main
 
 concurrency:
-  group: ghcr-publish-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  group: ghcr-publish-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
 env:

@@ -2,12 +2,14 @@ name: GHCR Publish
 
 on:
   pull_request:
+    branches:
+      - main
   push:
     branches:
       - main
 
 concurrency:
-  group: ghcr-publish-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  group: ghcr-publish-${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
 env:
@@ -21,39 +23,45 @@ jobs:
       packages: write
 
     steps:
+      - name: Validate publish gate
+        if: github.event_name == 'pull_request'
+        run: echo "Image publishing runs only for pushes to main."
+
       - name: Checkout repository
+        if: github.event_name == 'push'
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
 
       - name: Set up Docker Buildx
+        if: github.event_name == 'push'
         uses: docker/setup-buildx-action@v4
 
       - name: Log in to GHCR
-        if: github.event_name != 'pull_request'
+        if: github.event_name == 'push'
         uses: docker/login-action@v4
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Extract image metadata
+        if: github.event_name == 'push'
         id: meta
         uses: docker/metadata-action@v6
         with:
           images: ${{ env.IMAGE_NAME }}
           tags: |
             type=ref,event=branch
-            type=ref,event=pr
             type=sha,prefix=sha-
             type=raw,value=latest,enable={{is_default_branch}}
 
       - name: Build image
+        if: github.event_name == 'push'
         uses: docker/build-push-action@v7
         with:
           context: .
           file: ./Dockerfile
           platforms: linux/amd64
-          push: ${{ github.event_name != 'pull_request' }}
-          load: ${{ github.event_name == 'pull_request' }}
+          push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
           cache-from: type=gha

@@ -21,39 +23,45 @@ jobs:
       packages: write
 
     steps:
+      - name: Validate publish gate
+        if: github.event_name == 'pull_request'
+        run: echo "Image publishing runs only for pushes to main."
+
       - name: Checkout repository
+        if: github.event_name == 'push'
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
 
       - name: Set up Docker Buildx
+        if: github.event_name == 'push'
         uses: docker/setup-buildx-action@v4
 
       - name: Log in to GHCR
-        if: github.event_name != 'pull_request'
+        if: github.event_name == 'push'
         uses: docker/login-action@v4
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Extract image metadata
+        if: github.event_name == 'push'
         id: meta
         uses: docker/metadata-action@v6
         with:
           images: ${{ env.IMAGE_NAME }}
           tags: |
             type=ref,event=branch
-            type=ref,event=pr
             type=sha,prefix=sha-
             type=raw,value=latest,enable={{is_default_branch}}
 
       - name: Build image
+        if: github.event_name == 'push'
         uses: docker/build-push-action@v7
         with:
           context: .
           file: ./Dockerfile
           platforms: linux/amd64
-          push: ${{ github.event_name != 'pull_request' }}
-          load: ${{ github.event_name == 'pull_request' }}
+          push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
           cache-from: type=gha

diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,6 +8,7 @@ RUN apk add --no-cache python3 make g++ git
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/

@@ -8,6 +8,7 @@ RUN apk add --no-cache python3 make g++ git
 
 COPY package.json bun.lockb ./
 COPY packages/ai/package.json packages/ai/
+COPY packages/consumer-sdk/package.json packages/consumer-sdk/
 COPY packages/contracts/package.json packages/contracts/
 COPY packages/core/package.json packages/core/
 COPY packages/github-agent/package.json packages/github-agent/
@@ -19,55 +20,80 @@ COPY packages/slack-agent-ui/package.json packages/slack-agent-ui/
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
 
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
-# ---------- runner ----------
-FROM node:22-alpine AS runner
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
 WORKDIR /app
 
-ENV NODE_ENV=production
+COPY proto ./proto
+COPY --from=web-builder /app/packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    cd packages/control-plane-rs && \
+    CARGO_TARGET_DIR=/app/rust-target cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp /app/rust-target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
+# ---------- runner ----------
+FROM alpine:3.23 AS runner
+WORKDIR /app
 
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates libstdc++ && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

@@ -19,55 +20,80 @@ COPY packages/slack-agent-ui/package.json packages/slack-agent-ui/
 COPY packages/tui/package.json packages/tui/
 COPY packages/web/package.json packages/web/
 
-# ambient-agent-rs and tui-rs are pure Rust (no package.json)
+# ambient-agent-rs, control-plane-rs, and tui-rs are pure Rust (no package.json)
 # desktop, jetbrains-plugin, vscode-extension excluded via .dockerignore
 RUN bun install --no-frozen-lockfile
 
-# ---------- builder ----------
-FROM oven/bun:1.3-alpine AS builder
+# ---------- builder base ----------
+FROM oven/bun:1.3-alpine AS builder-base
 WORKDIR /app
 
 # contracts build generates Rust protocol files and formats with rustfmt
-RUN apk add --no-cache python3 make g++ git nodejs && \
+RUN apk add --no-cache python3 make g++ git nodejs pkgconfig && \
     wget -qO- https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c rustfmt && \
     ln -s /root/.cargo/bin/rustfmt /usr/local/bin/rustfmt
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# ---------- web builder ----------
+FROM builder-base AS web-builder
+WORKDIR /app
 
 COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json bun.lockb ./
+COPY biome.json buf.gen.yaml buf.yaml drizzle.config.ts nx.json openapi.json project.json ./
+COPY tsconfig.base.json tsconfig.build.json tsconfig.json vitest.config.ts ./
+COPY proto ./proto
+COPY scripts ./scripts
+COPY skills ./skills
+COPY src ./src
+COPY types ./types
+COPY packages/ai ./packages/ai
+COPY packages/consumer-sdk ./packages/consumer-sdk
+COPY packages/contracts ./packages/contracts
+COPY packages/core ./packages/core
+COPY packages/github-agent ./packages/github-agent
+COPY packages/governance ./packages/governance
+COPY packages/governance-mcp-server ./packages/governance-mcp-server
+COPY packages/memory ./packages/memory
+COPY packages/slack-agent ./packages/slack-agent
+COPY packages/slack-agent-ui ./packages/slack-agent-ui
+COPY packages/tui ./packages/tui
+COPY packages/tui-rs ./packages/tui-rs
+COPY packages/web ./packages/web
 
 # Write lockfile hash stamp so ensure-deps.js skips re-install
 RUN node -e "const c=require('crypto'),f=require('fs');const h=c.createHash('sha256').update(f.readFileSync('bun.lockb')).digest('hex');f.mkdirSync('node_modules',{recursive:true});f.writeFileSync('node_modules/.bun-lockb.sha256',h);"
 
 RUN bun run build:all
 
-# ---------- runner ----------
-FROM node:22-alpine AS runner
+# ---------- rust builder ----------
+FROM builder-base AS rust-builder
 WORKDIR /app
 
-ENV NODE_ENV=production
+COPY proto ./proto
+COPY --from=web-builder /app/packages/tui-rs ./packages/tui-rs
+COPY packages/control-plane-rs ./packages/control-plane-rs
+RUN --mount=type=cache,target=/root/.cargo/registry \
+    --mount=type=cache,target=/root/.cargo/git \
+    cd packages/control-plane-rs && \
+    CARGO_TARGET_DIR=/app/rust-target cargo build --release --bin maestro-control-plane && \
+    mkdir -p /app/target-bin && \
+    cp /app/rust-target/release/maestro-control-plane /app/target-bin/maestro-control-plane
+
+# ---------- runner ----------
+FROM alpine:3.23 AS runner
+WORKDIR /app
 
-RUN apk add --no-cache tini git && \
+RUN apk add --no-cache tini git ca-certificates libstdc++ && \
     addgroup --system --gid 1001 appgroup && \
     adduser --system --uid 1001 appuser
 
-COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/src/db/migrations ./dist/db/migrations
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./
-COPY --from=builder /app/skills ./skills
-COPY --from=builder /app/packages/contracts/dist ./packages/contracts/dist
-COPY --from=builder /app/packages/contracts/package.json ./packages/contracts/
-COPY --from=builder /app/packages/memory ./packages/memory
-COPY --from=builder /app/packages/tui/dist ./packages/tui/dist
-COPY --from=builder /app/packages/tui/package.json ./packages/tui/
-COPY --from=builder /app/packages/web/dist ./packages/web/dist
-COPY --from=builder /app/packages/web/package.json ./packages/web/
-COPY --from=builder /app/packages/ai/dist ./packages/ai/dist
-COPY --from=builder /app/packages/ai/package.json ./packages/ai/
+COPY --from=web-builder /app/packages/web/dist ./packages/web/dist
+COPY --from=rust-builder /app/target-bin/maestro-control-plane ./bin/maestro-control-plane
 
 USER appuser
 
 EXPOSE 8080
 
 ENTRYPOINT ["tini", "--"]
-CMD ["node", "--enable-source-maps", "dist/web-server.js"]
+CMD ["./bin/maestro-control-plane"]

diff --git a/docs/MODELS.md b/docs/MODELS.md
--- a/docs/MODELS.md
+++ b/docs/MODELS.md
@@ -23,6 +23,19 @@ order and how to customize providers.
 
 Paths are read in that order, later entries overriding earlier ones.
 
+The Rust control plane keeps the same local fallback, and can also hydrate
+`GET /api/models` from the llm-gateway model catalog before applying local
+Maestro overrides:
+
+- `MAESTRO_LLM_GATEWAY_MODELS_URL` points directly at the catalog endpoint.
+- `MAESTRO_LLM_GATEWAY_URL` derives the catalog URL as `<base>/v1/models`.
+- `MAESTRO_LLM_GATEWAY_TOKEN` is sent as a bearer token when set.
+- `MAESTRO_LLM_GATEWAY_ORG_ID` is sent as `X-Organization-ID` when set.
+- `MAESTRO_LLM_GATEWAY_TIMEOUT_MS` defaults to `2500`.
+
+If the gateway URL is unset, unavailable, or returns invalid JSON, Maestro falls
+back to the built-in models and `~/.maestro/models.json`.
+
 ## Format
 
 Custom config files accept:

@@ -23,6 +23,19 @@ order and how to customize providers.
 
 Paths are read in that order, later entries overriding earlier ones.
 
+The Rust control plane keeps the same local fallback, and can also hydrate
+`GET /api/models` from the llm-gateway model catalog before applying local
+Maestro overrides:
+
+- `MAESTRO_LLM_GATEWAY_MODELS_URL` points directly at the catalog endpoint.
+- `MAESTRO_LLM_GATEWAY_URL` derives the catalog URL as `<base>/v1/models`.
+- `MAESTRO_LLM_GATEWAY_TOKEN` is sent as a bearer token when set.
+- `MAESTRO_LLM_GATEWAY_ORG_ID` is sent as `X-Organization-ID` when set.
+- `MAESTRO_LLM_GATEWAY_TIMEOUT_MS` defaults to `2500`.
+
+If the gateway URL is unset, unavailable, or returns invalid JSON, Maestro falls
+back to the built-in models and `~/.maestro/models.json`.
+
 ## Format
 
 Custom config files accept:

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",

@@ -57,6 +57,7 @@
     "bun:cli": "bun run ./src/cli.ts",
     "bun:watch": "bun run --watch ./src/cli.ts",
     "web": "node dist/web-server.js",
+    "web:rust-control": "packages/control-plane-rs/target/release/maestro-control-plane",
     "web:dev": "concurrently --names \"server,ui\" --prefix-colors \"blue,green\" \"tsx --watch src/web-server.ts\" \"bun run dev:web\"",
     "bun:lint": "bunx biome check . && bun run lint:evals",
     "bun:test": "node ./scripts/run-vitest.js --run",
@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

@@ -109,6 +110,8 @@
     "vscode:package": "npx nx run vscode-extension:package",
     "vscode:publish": "npx nx run vscode-extension:publish",
     "tui-rs:build": "cd packages/tui-rs && cargo build --release",
+    "control-plane-rs:build": "cd packages/control-plane-rs && cargo build --release --bin maestro-control-plane",
+    "control-plane-rs:check": "cd packages/control-plane-rs && cargo check --bin maestro-control-plane",
     "tui-rs:build:debug": "cd packages/tui-rs && cargo build",
     "tui-rs:check": "cd packages/tui-rs && cargo check",
     "tui-rs:test": "cd packages/tui-rs && cargo test"

diff --git a/packages/control-plane-rs/Cargo.lock b/packages/control-plane-rs/Cargo.lock
new file mode 100644

diff --git a/packages/control-plane-rs/Cargo.toml b/packages/control-plane-rs/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "maestro-control-plane"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+description = "Native Rust HTTP control plane for Maestro"
+
+[[bin]]
+name = "maestro-control-plane"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1"
+base64 = "0.22"
+chrono = "0.4"
+getrandom = "0.3.4"
+hmac = "0.12"
+jsonwebtoken = "9"
+maestro-tui = { path = "../tui-rs" }
+pdf-extract = "0.7"
+reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+sha1 = "0.10"
+sha2 = "0.10"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "net", "process", "sync", "time", "fs"] }
+zip = "2"

@@ -0,0 +1,27 @@
+[package]
+name = "maestro-control-plane"
+version = "0.1.0"
+edition = "2021"
+license = "MIT"
+description = "Native Rust HTTP control plane for Maestro"
+
+[[bin]]
+name = "maestro-control-plane"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1"
+base64 = "0.22"
+chrono = "0.4"
+getrandom = "0.3.4"
+hmac = "0.12"
+jsonwebtoken = "9"
+maestro-tui = { path = "../tui-rs" }
+pdf-extract = "0.7"
+reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+sha1 = "0.10"
+sha2 = "0.10"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "net", "process", "sync", "time", "fs"] }
+zip = "2"

diff --git a/packages/control-plane-rs/src/http.rs b/packages/control-plane-rs/src/http.rs
new file mode 100644
--- /dev/null
+++ b/packages/control-plane-rs/src/http.rs
@@ -0,0 +1,449 @@
+use serde::Serialize;
+use std::collections::HashMap;
+use std::env;
+use tokio::io::AsyncReadExt;
+use tokio::net::TcpStream;
+
+const MAX_HEADER_BYTES: usize = 64 * 1024;
+pub(crate) const MAX_JSON_BODY_BYTES: usize = 32 * 1024 * 1024;
+const CORS_ALLOW_HEADERS: &str = "authorization,content-type,x-composer-artifact-access,x-composer-api-key,x-composer-approval-mode,x-composer-client,x-composer-client-tools,x-composer-csrf,x-composer-agent-id,x-composer-slim-events,x-composer-workspace,x-composer-workspace-id,x-maestro-artifact-access,x-maestro-api-key,x-maestro-approval-mode,x-maestro-agent-id,x-maestro-client,x-maestro-client-tools,x-maestro-csrf,x-maestro-slim-events,x-maestro-workspace,x-maestro-workspace-id,x-csrf-token,x-xsrf-token";
+
+#[derive(Debug)]
+pub(crate) struct RequestHead {
+    pub(crate) method: String,
+    pub(crate) path: String,
+    pub(crate) query: HashMap<String, String>,
+    pub(crate) headers: HashMap<String, String>,
+}
+
+pub(crate) async fn read_request_head(
+    stream: &mut TcpStream,
+    initial: &mut Vec<u8>,
+) -> Result<RequestHead, String> {
+    let mut chunk = [0_u8; 4096];
+    loop {
+        let read = stream
+            .read(&mut chunk)
+            .await
+            .map_err(|error| error.to_string())?;
+        if read == 0 {
+            return Err("connection closed before request headers".into());
+        }
+        initial.extend_from_slice(&chunk[..read]);
+        if initial.windows(4).any(|window| window == b"\r\n\r\n") {
+            break;
+        }
+        if initial.len() > MAX_HEADER_BYTES {
+            return Err("request headers exceeded limit".into());
+        }
+    }
+    parse_request_head(initial)
+}
+
+pub(crate) async fn read_request_body(
+    stream: &mut TcpStream,
+    initial: &mut Vec<u8>,
+    head: &RequestHead,
+) -> Result<Vec<u8>, String> {
+    read_request_body_with_limit(stream, initial, head, MAX_JSON_BODY_BYTES).await
+}
+
+pub(crate) async fn read_request_body_with_limit(
+    stream: &mut TcpStream,
+    initial: &mut Vec<u8>,
+    head: &RequestHead,
+    max_body_bytes: usize,
+) -> Result<Vec<u8>, String> {
+    let header_end = header_end(initial)?;
+    let body_start = header_end + 4;
+    let content_length = head
+        .headers
+        .get("content-length")
+        .and_then(|value| parse_content_length(value))
+        .ok_or_else(|| "content-length is required".to_string())?;
+    if content_length > max_body_bytes {
+        return Err(format!(
+            "request body exceeded limit: {content_length} > {max_body_bytes}"
+        ));
+    }
+
+    while initial.len().saturating_sub(body_start) < content_length {
+        let mut chunk = [0_u8; 8192];
+        let read = stream
+            .read(&mut chunk)
+            .await
+            .map_err(|error| error.to_string())?;
+        if read == 0 {
+            return Err("connection closed before request body completed".into());
+        }
+        initial.extend_from_slice(&chunk[..read]);
+        if initial.len().saturating_sub(body_start) > max_body_bytes {
+            return Err("request body exceeded limit".into());
+        }
+    }
+
+    Ok(initial[body_start..body_start + content_length].to_vec())
+}
+
+pub(crate) fn parse_request_head(initial: &[u8]) -> Result<RequestHead, String> {
+    let header_end = header_end(initial)?;
+    let header_text = std::str::from_utf8(&initial[..header_end])
+        .map_err(|error| format!("request headers are not utf-8: {error}"))?;
+    let mut lines = header_text.split("\r\n");
+    let request_line = lines
+        .next()
+        .ok_or_else(|| "request line missing".to_string())?;
+    let mut parts = request_line.split_whitespace();
+    let method = parts
+        .next()
+        .ok_or_else(|| "request method missing".to_string())?
+        .to_uppercase();
+    let raw_target = parts
+        .next()
+        .ok_or_else(|| "request target missing".to_string())?;
+    let (path, query) = raw_target
+        .split_once('?')
+        .map(|(path, query)| (path.to_string(), parse_query(query)))
+        .unwrap_or_else(|| (raw_target.to_string(), HashMap::new()));
+    let mut headers = HashMap::new();
+    for (name, value) in lines
+        .filter_map(|line| line.split_once(':'))
+        .map(|(name, value)| (name.trim().to_lowercase(), value.trim().to_string()))
+    {
+        headers
+            .entry(name)
+            .and_modify(|existing: &mut String| {
+                existing.push_str(", ");
+                existing.push_str(&value);
+            })
+            .or_insert(value);
+    }
+    Ok(RequestHead {
+        method,
+        path,
+        query,
+        headers,
+    })
+}
+
+fn parse_content_length(value: &str) -> Option<usize> {
+    let mut lengths = value
+        .split(',')
+        .map(|part| part.trim().parse::<usize>().ok());
+    let first = lengths.next()??;
+    lengths.all(|length| length == Some(first)).then_some(first)
+}
+
+fn parse_query(query: &str) -> HashMap<String, String> {
+    query
+        .split('&')
+        .filter(|part| !part.is_empty())
+        .filter_map(|part| {
+            let (key, value) = part.split_once('=').unwrap_or((part, ""));
+            let key = percent_decode_component(key);
+            if key.is_empty() {
+                None
+            } else {
+                Some((key, percent_decode_component(value)))
+            }
+        })
+        .collect()
+}
+
+pub(crate) fn query_flag(head: &RequestHead, name: &str) -> bool {
+    head.query
+        .get(name)
+        .map(|value| !matches!(value.as_str(), "0" | "false" | "off"))
+        .unwrap_or(false)
+}
+
+pub(crate) fn header_end(initial: &[u8]) -> Result<usize, String> {
+    initial
+        .windows(4)
+        .position(|window| window == b"\r\n\r\n")
+        .ok_or_else(|| "request header terminator not found".to_string())
+}
+
+pub(crate) fn text_response(status: u16, body: &str) -> Vec<u8> {
+    response(status, "text/plain; charset=utf-8", body.as_bytes())
+}
+
+pub(crate) fn json_response<T: Serialize>(status: u16, value: &T) -> Vec<u8> {
+    let body = serde_json::to_vec(value)
+        .unwrap_or_else(|_| br#"{"error":"failed to serialize response"}"#.to_vec());
+    response(status, "application/json", &body)
+}
+
+pub(crate) fn response(status: u16, content_type: &str, body: &[u8]) -> Vec<u8> {
+    response_with_extra_headers(status, content_type, body, "")
+}
+
+pub(crate) fn response_with_cache(
+    status: u16,
+    content_type: &str,
+    body: &[u8],
+    cache_seconds: u64,
+) -> Vec<u8> {
+    response_with_extra_headers_and_length(
+        status,
+        content_type,
+        body,
+        &format!("Cache-Control: public, max-age={cache_seconds}\r\n"),
+        body.len(),
+    )
+}
+
+pub(crate) fn response_with_cache_and_length(
+    status: u16,
+    content_type: &str,
+    body: &[u8],
+    cache_seconds: u64,
+    content_length: usize,
+) -> Vec<u8> {
+    response_with_extra_headers_and_length(
+        status,
+        content_type,
+        body,
+        &format!("Cache-Control: public, max-age={cache_seconds}\r\n"),
+        content_length,
+    )
+}
+
+pub(crate) fn response_with_no_store(status: u16, content_type: &str, body: &[u8]) -> Vec<u8> {
+    response_with_no_store_and_length(status, content_type, body, body.len())
+}
+
+pub(crate) fn response_with_no_store_and_length(
+    status: u16,
+    content_type: &str,
+    body: &[u8],
+    content_length: usize,
+) -> Vec<u8> {
+    response_with_extra_headers_and_length(
+        status,
+        content_type,
+        body,
+        "Cache-Control: no-store, no-cache, must-revalidate\r\n",
+        content_length,
+    )
+}
+
+pub(crate) fn response_with_extra_headers(
+    status: u16,
+    content_type: &str,
+    body: &[u8],
+    extra_headers: &str,
+) -> Vec<u8> {
+    response_with_extra_headers_and_length(status, content_type, body, extra_headers, body.len())
+}
+
+pub(crate) fn response_with_extra_headers_and_length(
+    status: u16,
+    content_type: &str,
+    body: &[u8],
+    extra_headers: &str,
+    content_length: usize,
+) -> Vec<u8> {
+    assert!(
+        body.is_empty() || content_length == body.len(),
+        "response body length must match Content-Length unless body is empty"
+    );
+    let reason = match status {
+        200 => "OK",
+        204 => "No Content",
+        400 => "Bad Request",
... diff truncated: showing 800 of 2687 lines

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit db6705f. Configure here.

Comment thread packages/control-plane-rs/src/http.rs
@haasonsaas haasonsaas merged commit 509b8d6 into main Apr 28, 2026
13 checks passed
@haasonsaas haasonsaas deleted the jonathan/rust-control-plane-maestro branch April 28, 2026 00:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d2843d74f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread Dockerfile
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants