Skip to content

Refactor and Gateway Config UI - #8

Merged
vinniefalco merged 104 commits into
cppalliance:masterfrom
vinniefalco:master
Aug 31, 2026
Merged

Refactor and Gateway Config UI#8
vinniefalco merged 104 commits into
cppalliance:masterfrom
vinniefalco:master

Conversation

@vinniefalco

Copy link
Copy Markdown
Member

bunch of stuff.. check commit messages

Profile switches re-hashed every sha256-pinned GGUF on every
cache hit: ensure_blob verified the cached file unconditionally,
and the path-source branch of ensure_model did the same. With
local models at 19-29 GB, that hash pass dominated switch time
(a live spike measured roughly eight minutes of provisioning
before llama-server even started loading weights).

A new verified.rs submodule records a marker after any
successful hash: the pinned digest, the file size, and the
modification time. verify_blob consults the marker first and
returns MarkerHit with zero disk reads when all three match;
anything missing, stale, corrupt, or mis-pinned falls back to a
real hash, which refreshes the marker on success and deletes it
on mismatch. URL-source blobs keep the marker beside the file
under the existing artifact lock; path sources, which live
outside the cache, key a marker under the cache's markers
directory by source_cache_key.

- Trust tradeoff, documented in the module docs: size+mtime is
  spoofable by anyone who can write the cache, but the cache
  root is already operator-trusted, and a marker is only ever
  written after a real hash match, so the pin still fully
  guards downloads.

Six tests cover first-hash marker write, marker-hit short
circuit, content-change re-hash and mismatch, corrupt-marker
fallback, post-download marker write, and the path-source
second call. Focused: cargo test --locked -p promptforge-gateway
local::artifacts (34 passed, 0 failed); fmt and clippy
-D warnings clean.
Weight loading and inference in a llama-server child compete
with the interactive desktop for CPU and I/O scheduling. The
production spawn path now builds its Command through a
production_command helper that sets the Win32
BELOW_NORMAL_PRIORITY_CLASS creation flag on Windows, so a
profile switch's model load yields to foreground rendering and
input handling. The constant is spelled as its raw stable-ABI
value to avoid a windows-sys dependency in the main build.
Respawns inherit the flag through the same ChildSpawner.
Non-Windows builds are a documented no-op; a nice port would
need libc and pre_exec unsafe and is deferred.

- Test deviation from the plan: the workspace forbids
  unsafe_code at the unoverridable forbid level, so the planned
  windows-sys GetPriorityClass probe cannot compile here. The
  test instead spawns a PowerShell child that self-reports
  (Get-Process -Id $PID).PriorityClass and asserts BelowNormal -
  same behavioral coverage, no unsafe, no new dependency, no
  lockfile change.

Focused: cargo test -p promptforge-gateway local::server
(24 passed, 0 failed); fmt and clippy -D warnings clean.
Switching to a Gemma-4 profile failed at start-local with "no
tool dialect matched the provided evidence". The Gemma-4 chat
template uses pipe-wrapped markers (<|tool_call|>,
<|tool_response|>, <|turn|>), so the ChatML conjunction in
openai_score missed it, and the gemma3 fingerprint missed too:
the template has no <start_of_turn> and this server's /props
carries no default_generation_settings.model id. Meanwhile the
capability evidence that would have settled it -
chat_template_caps.supports_tool_calls in /props - was never
read; only /v1/models' has_tool_call_capability was probed.

fetch_props_evidence now reads the /props chat_template_caps
field first, falling back to the /v1/models probe only when it
is absent, consistent with the file's props-first precedence.
openai_score gains the Gemma-4 conjunction, scoring like the
ChatML and Mistral conjunctions, so a tool-capable Gemma-4
template resolves to the native openai dialect even when both
capability probes are silent. gemma3_tool_code scoring is
unchanged; legacy Gemma-3 evidence still resolves to the
emulated dialect, pinned by the pre-existing test.

Five new tests cover the marker conjunction, precedence over
the gemma fingerprint, absent-vs-false capability parsing, the
props capability path, and the unreliable-caps-false fall
through. Focused: cargo test --locked -p promptforge-gateway
local::dialect (15 passed, 0 failed); fmt and clippy
-D warnings clean.
Native compilation inside a running gateway would make ordinary startup depend on developer toolchains, permit long and failure-prone build processes inside the serving lifecycle, and complicate installer security. The change adds one rule to `AGENTS.md`: runtime and serve paths never compile native dependencies or invoke compilers or build tools. Compilation belongs to the Cargo build or packaging process; runtime may only verify, stage, and launch build-produced artifacts.
A marker-write failure after a successful digest match failed verification and download publication, even though the marker only skips a future re-hash. This change adds `write_marker_best_effort`, which wraps `write_marker` and degrades a persistence failure to a `tracing::warn!` log. `verify_blob` and the post-download publish path in `artifacts.rs` now call it.

- Confinement stays a hard error: `validate_cache_path` still runs before the marker write, and `LocalError::UnsafeCachePath` and `LocalError::DigestMismatch` remain failures.
- New tests `marker_persistence_failure_still_verifies` and `post_download_marker_persistence_failure_still_publishes` use `with_readonly_file` to block the marker path and pin that the operation succeeds while the stale marker stays untouched.
Give built-in tools, web providers, and a future addon host one stable tool vocabulary that does not depend on the parser, the Lua runtime, the executor, or HTTP clients. Move `Tool`, `ToolCatalog`, `ToolId`, `ToolOutput`, `OutputTrust`, `ToolError`, and the contract errors out of `promptforge-core` into the new publishable `promptforge-tools` crate. `promptforge-core` re-exports the vocabulary under `promptforge_core::tools`, so existing paths keep working.

- `promptforge-webfetch` now depends on `promptforge-tools` instead of `promptforge-core`; the concrete `WebSearch` tool remains in `promptforge-core`.
- `ToolId::from_validated` becomes `#[doc(hidden)] pub` so `promptforge-core` can construct validated identities across the crate boundary, and `NearDuplicateDiagnostic` stays crate-internal to `promptforge-core`.
- Because `OutputTrust` is `#[non_exhaustive]` outside its defining crate, `tool_loop.rs` now sends any unknown future variant through `untrusted::wrap` as untrusted output.
- The contract tests move with the code into `promptforge-tools`; `promptforge-core` keeps only regression tests that pin re-export identity and dynamic dispatch.
`WebSearch` performs network I/O and owns credentials, deadlines, and response decoding, so it does not belong in the execution core. The concrete provider moves from `crates/promptforge-core/src/tools/web_search.rs` into the new publishable `promptforge-web-search` crate, which keeps the vendor credential behind one review boundary. `promptforge-core` re-exports `WebSearch` under its historical `promptforge_core::tools` path so existing callers keep working.

- The new crate takes its `Tool` vocabulary from `promptforge-tools` and carries crate-private `Endpoint` and `Token` types, so it never depends on `promptforge-core` or the gateway; `promptforge-cli`, `promptforge-dev`, and `promptforge-mcp-server` now import `WebSearch` from `promptforge_web_search` directly.
- Constructor failures now wrap the endpoint or token error with `ToolError::with_source` instead of flattening it into the message, preserving the underlying cause as the error source.
- The HTTP test suite moves with the provider into `crates/promptforge-web-search/src/web_search/tests.rs`; new tests pin source-preserving constructor errors that do not echo embedded credentials, and `reexported_web_search_is_the_provider_type` proves at compile time that the core re-export names the provider type.
Whisper inference is a compute subsystem with worker ownership and its own CUDA dependency, so it moves out of the HTTP server to keep voice CUDA distinct from llama CUDA and to reduce server dependency weight. The `transcribe` module and `segment.rs` move from `promptforge-ws-server` into the new `promptforge-transcribe` crate; the server keeps the voice WebSocket session, route state, and activation, and constructs the engine through the new plain-value `EngineConfig`.

- `VoiceEngine::new` now takes `&EngineConfig`; `From<&VoiceConfig> for promptforge_transcribe::EngineConfig` in `config.rs` is the only seam, and an empty `final_model` maps to `None`, which disables the final pass.
- The server's GPU feature is renamed to `voice-cuda`, which forwards to `promptforge-transcribe/cuda`; `cuda` remains as a compatibility alias.
- `final_pass_absent_for_test` is now gated on the `test-fixtures` feature instead of `cfg(test)`, and `SILENCE_RMS` and `rms` become crate-private.
- The moved engine code carries no new tests; the only added test is `voice_config_maps_into_engine_config`, which pins the `VoiceConfig` mapping including the empty-`final_model` case.
Windowing and the Windows WebView2 bridge are platform infrastructure, not application lifecycle orchestration, so they move out of the `promptforge-ws` binary into an unpublished crate. The new crate owns `window.rs`, `file_drop.rs`, the icon assets, and the tao/wry event loop behind one public `run` entry point. `promptforge-ws` keeps configuration discovery, gateway start, the health wait, and shutdown, and opens the window through `promptforge_desktop_shell::run`.

- `promptforge-desktop-shell` mirrors the workspace lint set with `unsafe_code` lowered to deny, which `file_drop.rs` alone opts out of; `promptforge-ws` returns to `workspace = true` lints.
- `window::run` widens from `pub(crate)` to `pub` as the crate's single documented entry point.
- CI excludes `promptforge-desktop-shell` from the default workspace clippy, test, doctest, doc, and MSRV jobs and adds it to the workshop clippy and test jobs.
- `window.rs` and `file_drop.rs` move without behavior changes; the only added test, `run_is_the_single_narrow_entry_point` in `tests/it/main.rs`, pins the `run` signature, and the moved windowing code gains no other new tests.
On Windows the gateway hard-codes a Vulkan llama.cpp archive even on NVIDIA systems. The new `llama-cuda` feature compiles the pinned `third_party/llama.cpp` submodule during the Cargo build into a host-native CUDA `llama-server` and embeds the bundle in the gateway binary. The unpublished `promptforge-gateway-build` crate runs the pipeline behind the `Probe` command seam; the gateway `build.rs` calls `build()` and includes the generated `llama_cuda_bundle` module.

- Build logic lives in `promptforge-gateway-build` (`publish = false`), not in the gateway library, and `crates/promptforge-gateway/AGENTS.md` limits runtime code to verifying, staging, and launching build-produced bundles.
- The submodule is pinned to `PINNED_COMMIT` (tag `b10082`); `submodule::verify` reads HEAD without invoking git and fails the build on absence or drift, naming both commits.
- The build applies only to native Windows x86-64: `require_native` rejects cross-compilation, every other target returns `built: false`, and `workshop-cuda` now implies `llama-cuda`.
- The pipeline requires CUDA Toolkit >= `MIN_TOOLKIT` (12.8), detects visible GPUs through `nvidia-smi`, compiles only the `llama-server` target for the normalized `CMAKE_CUDA_ARCHITECTURES` list, and smoke-checks `llama-server --list-devices` for a CUDA device.
- `dumpbin /dependents` accounting keeps Windows system and CUDA Toolkit DLLs external under `LINKAGE_POLICY` `static-project-external-cuda`; an unknown import absent from the bundle fails the build.
- All output stays under `OUT_DIR`: a canonical `llama-cuda-manifest.json` (`BUNDLE_FORMAT_VERSION` 1, SHA-256 per file) and a generated `llama_cuda_bundle.rs` embedding `MANIFEST` and `FILES` with `include_str!` and `include_bytes!`.
- The `llama_cuda_bundle` module is dormant under `#[expect(dead_code)]`; no runtime code consumes the embedded bundle yet. Tests use synthetic trees and `FakeProbe`; no test invokes real CMake or the network.
A `llama-cuda` Windows x86-64 build now verifies and publishes its embedded CUDA bundle into the operator cache instead of downloading the Vulkan archive. The new `cuda_bundle` module decodes the embedded manifest through a narrow runtime-side schema, validates the payload, checks the declared external CUDA Toolkit DLLs, and stages the files through the shared lock, private staging directory, tree digest, install marker, and atomic rename. `provision_llama_server` returns the new `ProvisionedServer`, whose `path_prefix` `production_command` prepends to the child `PATH`.

- `build.rs` emits the `llama_cuda_embedded` cfg from Cargo target environment variables, so runtime code gates on one name and a cross-compile cannot claim an embedded bundle it did not produce.
- The runtime decodes the manifest into its own `RuntimeManifest` and reports failures through `BundleError` wrapped by `LocalError::CudaBundle`; it never depends on the build-support crate.
- The toolkit runtime directory resolves from `CUDA_PATH_V13_3`-style versioned variables with `CUDA_PATH` as fallback, and each external DLL must exist in that `bin` directory or in `System32`.
- Payload validation completes before the cache is consulted, so tampered embedded bytes fail even when a valid installation exists; a valid matching installation returns without restaging.
- `child_path_with_prefix` sets the prefixed `PATH` only on the child environment; the process environment is never mutated, and an empty prefix leaves the inherited `PATH` untouched.
- `stage_embedded`, the consumer of `crate::llama_cuda_bundle`, is not covered by tests; the tests exercise `stage_bundle` with synthetic payloads.
The bundle build failed on Visual Studio generators because `parse_cache` required `CMAKE_CXX_COMPILER` and `CMAKE_CXX_COMPILER_VERSION` entries in `CMakeCache.txt`, which those generators never write: the toolset fixes the compiler, so the cache carries only the generator. `parse_generator` now reads the generator alone, and the new `parse_compiler_cmake` and `compiler_cmake_path` recover the compiler path and version from `CMakeFiles/<version>/CMakeCXXCompiler.cmake`.

- `build_with` in `bundle.rs` now assembles `CacheIdentity` itself from the `parse_generator` and `parse_compiler_cmake` results; `cmake.rs` no longer owns cache-wide parsing.
- `compiler_cmake_path` sorts the `CMakeFiles` subdirectories and returns the first that contains `CMakeCXXCompiler.cmake`; a missing file fails the build with an error that names `CMakeCXXCompiler.cmake`.
- The new test `missing_compiler_identity_fails_the_build` pins that failure, and the synthetic host now writes a `CMakeFiles/4.4.2/CMakeCXXCompiler.cmake` fixture while the manifest test asserts the `msvc` path and version.
When `llama_cuda_embedded` is set, provisioning stages an embedded bundle and never reads archives, so `require_executable`, four `LocalError` variants, and the `LocalError` import in `cuda_bundle.rs` had no users. The dead code and the unused import fail lint runs that deny warnings. This change gates each item behind `cfg(not(llama_cuda_embedded))`, removes the import, and points the doc links at the full path `crate::local::error::LocalError`.

- The variants `Archive`, `MissingExecutable`, and `DuplicateExecutable` use `cfg(any(not(llama_cuda_embedded), test))` so the archive tests still compile; `UnsupportedPlatform` has no test use and is fully gated out.
- In `artifacts.rs`, the `llama_cuda_embedded` branch drops its `return` for a tail expression, because the trailing `not(llama_cuda_embedded)` block compiles away and leaves a needless `return`.
- No test assertions change; the only test edit adds the `LocalError` import to `cuda_bundle/tests.rs`.
Chat local models need an explicit MTP drafter and multimodal projector declared next to the main model, so those artifacts stay reproducible and pinned without repository-name heuristics. A new `companion` module in `promptforge-gateway-config` parses and validates a `[local_model.speculative]` sub-table and a `[local_model.multimodal_projector]` sub-table into `SpeculativeConfig` and `MultimodalProjectorConfig`, exposed as optional `speculative` and `multimodal_projector` fields on `LocalModelConfig` and re-exported through `lib.rs`.

- The shared `validate_artifact_source` gate in `companion.rs` now holds the artifact source rules: non-empty, `https` or local path, remote sources pinned by `sha256`, and a 64-character lowercase hex check. `validate.rs` calls it for the main model source in place of its inline checks, and `validate_http_url` is now `pub(super)` to permit that reuse.
- `DraftTokenMax` bounds `draft_max` to `1..=16` and `SpeculationType` supports only `draft-mtp`, so an unknown speculation type fails at deserialize time. Both companion types use `deny_unknown_fields`, and `validate_kind_scope` rejects them on any non-chat model kind.
- The companions are parsed, validated, and re-exported only; no code outside `promptforge-gateway-config` consumes them.
Declared companions must reach every launch and respawn as the same verified paths and arguments. `LocalRuntime::start` now calls `provision_companions` before `ServerGuard::start`, which resolves the speculative drafter and multimodal projector through `store.ensure_model` and stores the owned results in `LaunchOptions` as `speculative` and `multimodal_projector`. `server_args` emits `--spec-draft-model`, `--spec-type draft-mtp`, `--spec-draft-n-max`, and `--mmproj` when those fields are set.

- Each companion resolves through `ensure_model` under its own source identity and pin, so the drafter and projector share the main model's integrity and cache behavior.
- The new `SpeculativeLaunch` struct owns the resolved drafter path and draft maximum inside `LaunchOptions`, so a respawn re-emits the exact verified artifact without re-resolving external state.
- The companion flag spellings are pinned to the bundled server (`third_party/llama.cpp` @ `fb0e6b6`, `common/arg.cpp`); the legacy `--draft` and `--draft-max` flags were removed at that pin.
- A provisioning failure returns `LocalError` before the child spawns, so a bad companion never becomes a spawned-then-failing server.
- A model without companions leaves `LaunchOptions` untouched, preserving the exact command line from before companions existed.
CUDA offload, MTP drafting, projector input, and cache reuse can all compile and still fail only on real hardware. This adds an ignored, opt-in integration test in `tests/it/cuda.rs` that proves the full path on a CUDA host, plus a public `Gateway::local_diagnostics` that lets an embedding host read bounded, credential-redacted tails of each child's captured output. The READMEs and the gateway guide now document the CUDA build requirements, companion syntax, and diagnostics.

- `LocalRuntime` keeps its `LocalUpstream` values un-erased in a new `upstreams` field so `diagnostics()` can reach each child's captured output; `LocalUpstream` gains `model_name()` and `diagnostics()`.
- `live_cuda_mtp_multimodal_end_to_end` is `#[ignore]`d, gated on `PROMPTFORGE_LIVE_CUDA=1`, serialized by the `LIVE_CUDA` mutex, and checks embedded-bundle staging, a CUDA device report, GPU-layer offload, digest markers, MTP `timings`, cache reuse, a tool call, and an image completion.
- `png` joins the gateway's dev-dependencies to encode the generated red-and-blue test image.
- The live proof runs only with `--features llama-cuda -- --ignored live_cuda` and the environment variable set, so default test runs never exercise it; unit tests pin only the empty-diagnostics cases.
Documentation built with warnings denied failed because the doc comments on `StateError` and `router` linked to private items that rustdoc cannot resolve. The change replaces the intra-doc link brackets with plain code spans for `crate::error::AppError`, `crate::routes`, `crate::cross_site`, `crate::deadline`, and `routes::chat`. The rendered text stays the same.

- Only doc comments in `app.rs` change; no code, tests, or public API change.
Running the live CUDA test exposed four defects: the toolkit probe resolved only `bin`, which is DLL-less on CUDA 13; the system probe rejected UCRT API-set stubs (`api-ms-win-crt-*`) that Windows 11 ships only in `System32\downlevel`; phase failures printed only the outer message of opaque error wrappers; and the diagnostics matcher waited for log lines the pinned server never emits. `toolkit_bin_dir` now probes `bin\x64` before `bin`, `require_external_dlls` probes `System32` and its `downlevel` subdirectory, and the test walks the error `source` chain via `error_chain` and matches `using device CUDA0` plus one `offloaded ` line per model.

- `server_args` now passes `-lv 4`: the pinned server maps INFO messages, including device reports and `load_tensors` offload lines, to trace verbosity, so the default threshold hid the evidence the captured diagnostics exist for.
- The `MissingToolkitDependency` error text now says 'system directories' to match the widened probe.
- New unit tests pin the `bin\x64` preference and the `downlevel` probe against synthetic hosts.
The gateway launches `llama-server` with mixed KV cache types (`--cache-type-k q8_0 --cache-type-v q4_0`), and the CUDA flash attention kernel rejects mixed K/V quant types unless every quant combination is compiled in. Without this option, `FLASH_ATTN_EXT` fell back to the CPU backend on every layer. `configure_options` in `crates/promptforge-gateway-build/src/cmake.rs` now passes `-DGGML_CUDA_FA_ALL_QUANTS=ON` to CMake.

- The new test `configure_options_enable_all_flash_attention_quants` fails if `-DGGML_CUDA_FA_ALL_QUANTS=ON` is dropped, and the existing required-options test now also pins `-DGGML_CUDA_FA=ON` and `-DGGML_CUDA_FA_ALL_QUANTS=ON`.
A local model with a configured `[local_model.multimodal_projector]` launched image-capable through `--mmproj`, but the catalog still reported the `images` default of false. The new `Config::imply_projector_images` sets `capabilities.images = true` for every local model whose `multimodal_projector` is present. `Config` loading in `imp.rs` now calls it between `apply_model_allowlist` and `validate`.

- `imply_projector_images` lives in `validate.rs` and runs after `apply_model_allowlist` and before `validate`, so downstream code reads the resolved capability verbatim.
- The `images` flag is a plain `bool`, so an explicit `images = false` cannot be told apart from an absent one; the projector wins either way because the model does accept images.
- New tests `projector_implies_images_capability` and `no_projector_keeps_images_default` in `companion.rs` pin both directions of the implication.
- The `Capabilities::images` doc comment in `config.rs`, the gateway `README.md`, and `user-guide-promptforge-gateway.md` now state that a `[local_model.multimodal_projector]` companion implies `images = true`.
The blanket `--all-features` commands in `ci.yml` enabled `workshop-cuda` on `promptforge-gateway`, which requires a CUDA Toolkit that the GitHub-hosted runners do not have, so the CI jobs failed. The blanket `Clippy`, `Test`, `Doctests`, `Docs`, and MSRV commands now use `--exclude promptforge-gateway`, and new `Clippy (gateway)`, `Test (gateway)`, and `Build and test gateway on MSRV` steps exercise the gateway with `--features workshop` instead. A new `cuda.yml` workflow runs the CUDA build and tests on a self-hosted runner.

- The gateway steps run with `--features workshop` rather than `--all-features`, so the non-CUDA gateway surface stays under test on the standard runners.
- Each job that builds the `workshop` feature now runs `actions/setup-node@v4` and `npm ci` in `crates/promptforge-ws-server/ui`, because the feature's build script bundles the UI with esbuild.
- `cuda.yml` checks out with `submodules: true`, runs `cargo build --locked -p promptforge-ws`, then `cargo test --locked -p promptforge-gateway --features llama-cuda`, then the ignored `live_cuda` test with `PROMPTFORGE_LIVE_CUDA` set.
- `cuda.yml` triggers only on `workflow_dispatch`; its nightly `schedule` is commented out until a `[self-hosted, windows, cuda]` runner is registered, so no CI job currently exercises `llama-cuda` automatically.
The blanket `--all-features` commands still enabled the `cuda` feature of `promptforge-transcribe`, which needs a CUDA Toolkit the runners do not have. The `Clippy`, `Test`, `Doctests`, and `Docs` steps and the MSRV build and test commands now pass `--exclude promptforge-transcribe`. New dedicated steps build, lint, and test `promptforge-transcribe` without that feature.

- The dedicated `Clippy (transcribe)`, `Test (transcribe)`, and `Build and test transcribe on MSRV` steps run the crate with its default features, so `whisper` builds its CPU backend, which needs no toolkit.
- The `msrv` job toolchain step now installs the `rustfmt` component.
- `Docs (transcribe)` keeps the warnings-denied doc build for the crate after the blanket `Docs` exclusion.
The menubar did not follow the pointer: with a menu open, hovering another button did nothing, and only a click moved the open menu. `setupWindowMenus` now registers a `pointerenter` listener on each menu button that calls `openMenu(handle.id, false)` when a menu is open and the hovered button is a different one.

- Rollover passes `false` to `openMenu`, the same as the click path, so the switch opens the menu without moving focus into it.
- The guard `openId !== handle.id` makes hovering the open menu's own button a no-op, so its rows are not rebuilt.
- New cases in `crates/promptforge-ws-server/ui/test/window-menu.mjs` pin that hover with no menu open opens nothing, that hover switches the open menu and its `aria-expanded` state, and that hovering the open menu's own button keeps it open without rebuilding rows.
A menu left open survived Alt+Tab and taskbar clicks, covering the returned window with a stale popover. `setupWindowMenus` now listens for `blur` on `window` and calls `closeMenu`.
Move the OpenAI wire types, the `Upstream` trait, and the bounded HTTP helpers out of `promptforge-gateway` into a new `promptforge-gateway-protocol` crate, so the gateway, its local inference subsystem, and external clients share one protocol contract. The gateway re-exports `wire`, `upstream`, and `http_util` from the crate, so existing `crate::wire::*` paths resolve unchanged.

- `Upstream::shutdown` now returns the crate's own `ShutdownError` instead of `LocalError`, so no type edge points from the protocol crate back into gateway code; `LocalUpstream` maps teardown failures through `ShutdownError::teardown`, and `LocalRuntime`'s shutdown returns `ShutdownError`.
- `GatewayError` replaces its `ModelUnavailable`, `UpstreamTransport`, `UpstreamConnect`, `UpstreamProtocol`, and `UpstreamStatus` variants with a transparent `Protocol` wrapper over `ProtocolError`; the classification table and the OpenAI error envelope mapping move to `ProtocolError::classify` and `ProtocolError::envelope`.
- The crate ships its own tests for wire validation, capped body reads, SSE chunk parsing, and error classification; gateway tests adopt the new error types.
Move the gateway model client and the model catalog and binding vocabulary out of `promptforge-core` into a new `promptforge-gateway-client` crate. The crate holds `GatewayClient`, the wire types, `ModelCatalog`, and the `ModelBinding`/`ModelSet`/`ModelView` vocabulary. `promptforge-core` re-exports the moved items under `promptforge_core::client` and `promptforge_core::model`, so existing paths keep working.

- The crate owns its error substrate: `promptforge_gateway_client::Error` is `#[doc(hidden)]` and not `#[non_exhaustive]`, so the `From` impl in `promptforge-core` maps it onto the core substrate variant for variant and the match stays total.
- The `normalize` module moved with its only consumer, the client transport, instead of an injection or a duplicate.
- Items that were `pub(crate)` now cross the crate boundary as `pub` items marked `#[doc(hidden)]`, including `ToolSchemaError`, the `ToolCall` and `Completion` fields, `ModelCatalog::filtered`, and `ModelId::from_validated`.
- `CompletionResult` is `#[non_exhaustive]` across the boundary, so `execute/tool_loop.rs` and `execute/tools.rs` add wildcard arms that fail an unrecognized outcome, one with `Error::Internal`.
- The catalog and binding unit tests (for example `context_filter_drops_small_windows`) moved from `promptforge-core/src/model_tests/mod.rs` into the new crate; core fixtures now build options through `CompletionOptions::new` and test resolvers return `GatewayClientError`.
The workshop server kept its own `ChatRequest` copy of the gateway wire type, and the two definitions could drift apart. `protocol.rs` now re-exports `ChatRequest` from `promptforge_gateway_protocol::wire` and parses inbound bodies with the new `parse_chat_request` function. Both receive paths, the `/chat` handler in `relay.rs` and the `/ws` handler in `chat_ws.rs`, parse through it.

- The re-export keeps the `crate::protocol::ChatRequest` path alive, so existing `protocol` importers need no changes.
- `parse_chat_request` removes a caller-sent `stream` field before deserialization and clears the wire type's `rest` passthrough after, so the body relayed upstream carries only `model` and `messages`; the workshop, not the caller, chooses streaming.
- The shared `ChatRequest` adds `stream` and `rest` fields, and the `gateway.rs` tests now construct it with `stream: false` and an empty `rest`; the new test `a_chat_request_drops_the_stream_flag_and_unnamed_fields` pins the drop behavior.
The run-scoped virtual filesystem imports nothing from the rest of core, so it moves into its own crate where consumers can reuse and test it alone. The change moves `store.rs` and the `store/` module into the new `promptforge-store` crate and leaves `crates/promptforge-core/src/store.rs` as verbatim re-exports, so every `promptforge_core::store::*` path keeps working.

- `WriteScope`, `StoreRef::next_write_token`, and `StoreRef::write_scoped` are now `pub` under `#[doc(hidden)]`: cross-crate seams for core's fanout machinery, not host API. The core shim re-exports `WriteScope` as `pub(crate)`.
- New `#[doc(hidden)]` constructors `StoreError::not_found` and `StoreError::invalid_range` let `host.rs` and `lua/tests.rs` build `#[non_exhaustive]` variants they can no longer construct across the crate boundary.
- The new crate's `AGENTS.md` sets the boundary: virtual filesystem only, no executor, Lua, or tool dependencies.
- The moved `tests.rs` suite is unchanged and no new tests were added; the `tempfile` dev-dependency moved with the crate.
Local inference is a large subsystem with heavy archive and process dependencies; extraction keeps headless gateway builds lean. The change moves the `artifacts`, `cache`, `runtime`, `server`, `sidecar`, and `upstream` modules and `build.rs` into the new `promptforge-gateway-local` crate behind the additive default-on `local` feature, and moves the shared routing vocabulary into the new `promptforge-gateway-routing` crate.

- `promptforge-gateway-routing` owns `Model`, `Endpoint`, and `dominion_queues` so the gateway routing table and the local inference crate both build on one data plane; its `test-helpers` feature exposes the `DominionQueue` seams `waiter_count` and `distinct_clients` to downstream test suites.
- `llama-cuda` now implies `local`, and the gateway crate no longer declares `async-trait`, `flate2`, `indicatif`, `tar`, `zip`, `rand`, the `reqwest` `blocking` feature, or the `promptforge-gateway-build` build-dependency.
- A build without `local` refuses a configuration declaring `[[local_model]]` at startup with `StartupErrorKind::Provisioning` and on profile switch with a terminal error event naming the `start-local` stage; the switch stream carries only the `loading-profile` stage, no `/v1/cache` routes are mounted, and `/admin/status` reports `local_children` as 0.
- A catch-all maps any future `AdmitError` variant to `GatewayError::QueueFull` because the type is non-exhaustive across the crate boundary.
- New tests `headless_boot_refuses_a_config_declaring_local_models`, `headless_switch_streams_loading_stage_then_ready`, `headless_status_reports_zero_local_children_and_no_cache_routes`, and `headless_switch_refuses_profile_declaring_local_models` pin the headless behavior; `switch_profile_streams_stages_in_order_then_ready` and the pinned model URL and digest constants in `support.rs` are now gated behind `cfg(feature = "local")`.
Move the sandboxed Lua runtime and the shared host-support primitives out of `promptforge-core` into two new crates, so the Lua sandbox and its `mlua` dependency build apart from the executor. `promptforge-lua` takes the section VM, the coroutine protocol vocabulary, the host tables, `LuaProgram`, and the model-binding layer; `promptforge-core-support` takes `untrusted`, `cancel`, and `observe`. Compatibility shims in core re-export the moved items, so the existing `promptforge_core::lua`, `promptforge_core::observe`, `promptforge_core::cancel`, and crate-root `CancelHandle` paths keep working.

- `Answer` becomes generic over the driver error type as `Answer<E>` with `map_error`, and `resume_block_coro_answer` accepts the caller's error type; the scheduler maps back with `Error::from`, so the protocol vocabulary never imports core's `Error`.
- `Observation` and `NullObserver` are `#[non_exhaustive]`, and `NullObserver` is constructed through `NullObserver::default()`.
- The coroutine shim integration tests stay in core as `coro_tests.rs`, beside the `section_vm` setup composition they drive; `section_vm` itself stays with the executor.
- Each new crate carries an `AGENTS.md` boundary file.
- The `LuaProgram::compile` doc example is now a compiled doctest instead of a `text` block and passes `NonZeroU32::MIN` as `source_line`; the duplicate `promptforge-core-support` dev-dependency is removed from `promptforge-lua`, which already lists it as a regular dependency.
- The dyn-compatibility test in `observe.rs` now sends `Observation::SectionFinished`.
The config UI gains .env secret management, a pending-vs-running
diff behind the banner's Review action, and an in-place editor for
the profile's model allowlist. The new `secrets-view.ts` lists both
env files as masked rows with an HF token card, backed by a widened
`PUT /admin/env` that accepts `scope=boot` and a `references` map in
the `GET /admin/env` reply. The new `pending_var_references` in
`promptforge-gateway-config` scans the pending chain before
interpolation, because a loaded config interpolates every `${VAR}`
away and redacts secrets.

- `${VAR}` reference labels are computed server-side by
 `pending_var_references`; only variable names and field labels
 enter the reply, never values.
- `ConfigStore.pendingDiff` diffs keyed arrays by entry identity
 (`endpoint[new-ep].api_key`) and other arrays wholesale as one
 row; secrets stay `***` on both sides, and the dialog states that
 changed secrets and staged .env edits show no row.
- The allowlist popover saves through the normal profile save path;
 zero chips deletes the top-level `models` key, and a null
 allowlist means every model is exposed.
- `PUT /admin/env?scope=boot` stages `gateway.env.next`; an unknown
 scope is refused before any write, and a corrupt chain file fails
 `GET /admin/env` loudly instead of returning no references.
- The HF Test Connection probes through `GET /admin/hf/search`, so
 it tests the token the running gateway holds, not a staged edit.
- `GET /admin/env` serves plaintext values; the loopback wall in
 `build_router` restricts the route to local callers, and a stale
 comment in `env_file.rs` still describes the guard as pending.
The workshop chat UI held twelve hand-pasted SVG string constants in `utils/icons.ts`. The module now imports each icon from the new `lucide` dependency and serializes it at module load with `createElement`, so the exported names and the string-valued API do not change. A new test pins the serialized output, and `PROVENANCE.md` records the divergence from the vendored upstream.

- A local `svg` helper overrides `width` and `height` on each serialized icon to keep the pixel size of the removed hand-pasted string; consumer CSS sizes against these attributes.
- `ICON_CHECK` keeps its stroke of `var(--mur-success)`; the other eleven icons stroke with `currentColor`.
- The test, `test/icons.mjs`, bundles the module with `esbuild`, loads it under `jsdom`, and asserts the twelve export names plus each icon's dimensions, viewBox, fill, and stroke attributes; run it with `node test/icons.mjs`.
- The test does not compare path geometry, so a lucide drawing that differs from the removed hand-pasted paths goes undetected.
The module doc described the loopback guard on the env routes as
pending. The guard is applied in `build_router`, so the doc now
states that these routes sit behind the shared loopback wall in
every build. Only the comment text changes; no executable code or
tests change.
This moves the workshop chrome to the palette of the gateway config UI: an orange accent replaces indigo, and neutral near-black surfaces replace blue-tinted darks. The change redefines the color tokens in `style.css`, updates every matching fallback literal in the component CSS files, and recolors the CodeMirror theme and syntax highlight styles in `editor-surface.ts`. The `--radius` token grows from `6px` to `8px`.

- Comments record the contrast constraints behind the values: `--text-muted` is `#909090` because `#888888` would fail 4.5:1 on hovered surfaces, and `--danger-text` `#e4606d` holds 4.5:1 on every surface.
- The CodeMirror active line moves from `--bg-hover` to a fixed `rgba(255, 255, 255, 0.04)` wash; the new `#252525` hover color drops accent-colored syntax tokens to 4.14:1, under the 4.5:1 floor.
- No tests change with the recolor; the contrast ratios exist only as code comments, with no automated check.
The workshop opens the config SPA in an iframe inside a dockview
panel, from a new `Gateway Config` item in the Window menu. In panel
mode each gateway call becomes a postMessage to the workshop page,
which forwards it through the new `/gateway/api/{*path}` server route
with the bearer key attached. The key stays in the workshop server
process and never enters a browser context.

- Origins are pinned in both directions: `parseBridgeOrigin` accepts
 only a loopback http(s) origin from the iframe URL's `bridge`
 parameter, both message listeners drop events whose origin differs,
 and no post targets `"*"`.
- The proxy forwards only paths on the `FORWARD_EXACT` and
 `FORWARD_PREFIX` allowlists and refuses dot segments and
 backslashes before any dial; a refused path answers 403 with the
 new `forward_denied` code (`AppError::ForwardDenied`).
- Panel mode hands `GatewayApi` an in-memory `Storage`
 (`memoryStorage`) so no key can reach `sessionStorage`, and it
 never subscribes to the progress stream; the shell announces
 `apply`, `revert`, and `download-started` to the parent, which
 shows them on the status bar.
- Bridged calls time out after `DEFAULT_TIMEOUT_MS` (30 seconds);
 `POST /v1/cache` gets `CACHE_TIMEOUT_MS` (30 minutes) because
 `forward` buffers the whole response body - no streaming relay
 exists.
- The iframe sandbox grants only `allow-scripts allow-same-origin`,
 and the `/gateway/api/{*path}` route mounts without the default
 deadline, because a forwarded cache download or profile switch can
 run for minutes.
- The workshop's context reply is the constant `PANEL_CONTEXT`
 (theme `dark`, route `#/models`), so no live theme selection
 reaches the panel yet.
The config UI now ships in both binaries by default instead of behind an opt-in flag. The gateway's `default` feature list gains `config-ui`, the workshop dependency names the feature explicitly, and a new `AGENTS.md` section states the feature policy.

- `crates/promptforge-workshop/Cargo.toml` names `config-ui` even though the gateway default already carries it, so the desktop exe keeps the config UI if the default edge is ever severed.
- The `AGENTS.md` policy limits Cargo features to real constraints, such as the `cuda` toolchain requirement or a heavy native build, and forbids `workshop` as a gateway default.
- A default gateway build now requires Node 22 because the config-ui `build.rs` runs esbuild; a `--no-default-features` build skips the SPA crate and needs no Node.
- `cargo check -p promptforge-gateway --no-default-features` must stay green as the gate that catches optional-feature types leaking into core paths.
- No source code or tests change; the diff touches feature wiring, `AGENTS.md`, and `README.md` only.
Granted roots had a grant route but no removal path. `Workspace::revoke` removes a root from the grant set by exact canonical match, and the new `POST /workspace/revoke` route exposes it through the `RevokeRequest` and `RevokeResponse` JSON shapes. The new `WorkspaceError::NotGranted` variant maps to HTTP 404 with wire code `not_granted`.

- A root deleted from disk no longer canonicalizes, so `revoke` falls back to the literal request path, which matches the stored canonical key; `a_deleted_root_can_still_be_revoked` pins this.
- Nested grants are independent: `a_nested_grant_survives_its_parents_revoke` shows a separately granted child stays readable while the parent's own files answer `OutsideGrants`.
- Reads and writes under a revoked root fail with `OutsideGrants` on their next operation; route tests pin the success body, the 404 `not_granted` envelope, and a 400 answer for a malformed body.
The page can ask the shell for a native folder dialog instead of a
typed path. The page posts the `workspace-pick-folder` message on the
WebView2 channel, the shell shows the `rfd` dialog modal to the
window, and a chosen path returns to the page as a
`promptforge:folder-picked` event.

- `parse_web_message` wraps `parse_window_command`, so the picker
 request and the title-bar envelopes route through one parser; the
 IPC handler forwards the parsed `ShellEvent` through the
 `EventLoopProxy`, so the synchronous dialog blocks one event loop
 iteration but never a webview callback.
- `rfd` is a Windows-only dependency with default features off, which
 drops the Linux xdg-portal and wayland backends; on other platforms
 `pick_folder` logs the unexpected request and answers as a cancel.
- A cancelled pick dispatches no event, the same contract as an empty
 file drop; a chosen path goes through `normalize_dropped_path` and
 is JSON-encoded into the event's `path` detail.
- Tests pin the message parsing, the cancel path, and the payload
 round-trip; the Windows `pick_folder` dialog call itself has no
 test.
Before this change, only a folder drop granted a workspace root. The Workshop tree now manages grants itself: a root row's context menu removes the root through `revokeRoot`, and a header button or the empty-space menu adds a folder through the desktop shell's native picker or, in a plain browser, a typed-path dialog. The server's `TreeEntry` gains an `exists` flag, and the panel renders a granted root deleted from disk with a strikethrough, the danger color, and a "missing" text label.

- `showPanelDialog` gains an optional labeled text field. A button marked `requiresValue` stays disabled while the trimmed value is empty, and Enter submits through the first such button.
- A new `PanelServices` seam carries the status bar from `main.ts` through `createPanelComponent` into `WorkshopTreePanel`; grant and revoke outcomes paint through `TreeStatusSink.showLocal`. `grantPath` in `workspace-drops.ts` is now exported and shared with the drop flow.
- `module-ceilings.toml` raises the `workspace.rs` ceiling from 1238 to 1272 for the `exists` flag and its listing tests.
- `parseEntry` now requires a boolean `exists`, so a tree listing without the field fails validation.
- The `promptforge:folder-picked` listener stays registered for the panel's lifetime and is removed in `dispose`, because a cancelled pick dispatches no event.
- The panel posts `workspace-pick-folder` and listens for `promptforge:folder-picked`; the shell side of that bridge is not in this diff.
The gateway now carries the config UI by default, so every CI job that compiles the workspace must install the config UI's npm dependencies before Cargo runs. The change adds an `npm ci` step in `crates/promptforge-gateway-config-ui/ui` to the four jobs in `.github/workflows/ci.yml`, and extends the build job with typecheck, build, test, package, and artifact upload for the config UI.

- A workflow comment records that `promptforge-workshop` pins the gateway's `config-ui` feature, so its build script needs the config UI's `node_modules` even with `--no-default-features`.
- The build job runs `npm run build` before `npm test`; the new `AGENTS.md` line records that the config UI test suite imports the built `dist/app.js`.
- `.gitignore` gains `/target-msrv/`, but no staged file references that directory.
@vinniefalco vinniefalco changed the title Refactor: Crate Extraction Refactor and Gateway Config UI Aug 30, 2026
Keep model additions, cache listings, and untrusted model cards safe and consistent. `stripFrontmatter` disables executable `gray-matter` engines, `stageLocalModel` serializes full-config writes and extends `models`, and `ArtifactStore` rebuilds missing cache metadata for unpinned URL hits.

- `stageTail` orders each `putConfig` and `refreshPending` pair, so concurrent `Download` actions preserve all staged entries.
- `blob_meta_matches` requires matching source and size with a valid `sha256`, and cache lookup now matches complete normalized paths instead of filename suffixes.
Protect pending updates from false legacy-key matches and partial shadow writes. Use TOML spans for precise hard-break locations, restore earlier shadow content when paired writes fail, validate retained profile state, and reject padded `ProfileName` values. Add focused regression tests and public API examples.

- Move secret restoration and `${VAR}` discovery to `shadow/content.rs`, and move pending-state tests to `shadow/tests.rs`.
- Make `write_shadow` and `promote_shadow` use `replace_file` so fallback replacement keeps a recoverable backup.
- Make `recommended_pair_live_urls_match_pins` stream `response` chunks into the digest and accept only lowercase SHA-256 pins.
Keep active profile state consistent while requests finish during a switch. Serialize direct switches with `Apply`, commit state through `StatePersistence` inside the switch lock, leave `[server]` bearer key changes for restart, and cancel requests that exceed the bounded drain.

- `persist_profile_state` atomically replaces the real state file without consuming `gateway.state.toml.next`; `StatePersistence::Promote` consumes pending state during `Apply`.
- Mark `LocalStartOutcome` and `LocalStartFailure` as `#[non_exhaustive]`, and raise `promptforge-gateway` to `0.2.0` and `promptforge-gateway-config` to `1.1.0`.
- `drain_or_cancel` gives canceled requests a fixed grace period, and `drain_switch_stages` keeps retained events after `TryRecvError::Lagged`.
Make the gateway the sole owner of STT lifecycle and voice routing. Delete workshop provisioning and voice modules, narrow the transcription response API, and move voice contract coverage into `promptforge-stt`.

- Engine replacement uses `SttSlot` and `take` to wait for route-held `Arc<SttEngine>` handles. `Transcriber` and `FinalTranscriber` close their queues and join their workers on drop.
- `verbose_json` defaults to segment timestamps, and `transcribe` returns `Response` while concrete response types stay private.
- `crates/promptforge-stt/tests/it/voice.rs` covers unknown controls, interim-only silence gaps, silent takes, committed-prefix assembly, segment boundaries, and append-only committed frames. Cases that require `tests/fixtures/` remain ignored.
Keep panel navigation and model discovery consistent with the single-file configuration flow. Add view disposal and request cancellation, validate Hugging Face repository data and search values, fan out `pipeline_tag` searches for OR behavior, and restrict the workshop proxy by HTTP method and path.

- Use `gateway.toml` as the only pending configuration target. Remove obsolete profile-file, include, boot-shadow, switch, and direct `POST /v1/cache` routes from the harness.
- Classify STT entries from `pipeline_tag`, refresh cache state after deletion, and preserve focus, live announcements, filtered counts, and unknown VRAM labels in the profile shuttle.
- Reject malformed Hugging Face search JSON, unsafe repository and file paths, invalid cache digests, duplicate query fields, and values outside the `GET /admin/hf/search` allowlist.
Keep live template parity deterministic and expose catalog drift. Add `live_request_body` and `normalize_dynamic_output`, extend `override_for_model`, hash family metadata and all 181 `MODEL_FAMILIES` entries, and bound oracle `tojson` indentation.

- Move GGUF parser tests from `gguf.rs` to `gguf/tests.rs` without changing their cases.
- `live_request_body` nests four options in `chat_template_kwargs` and removes `bos_token` and `eos_token`; `normalize_dynamic_output` rewrites only the valid current date for `Family::GptOss`.
- `override_for_model` accepts suffixed `e2b-it` and `e4b-it` variants and rejects an empty variant.
Protect launch-time template selection from corrupt cache assets and incomplete metadata. Move selection into `launch_templates.rs`, move asset publication into `artifacts/staging.rs`, verify staged bytes before replacement, and preserve source provenance when template fetches fail. Add regression tests for precedence, missing-template refusal, asset repair, path confinement, and concurrent publication.

- `resolve_chat_template_file` keeps custom paths and `builtin:` families ahead of hash and sidecar overrides, accepts valid embedded templates, and returns `MissingChatTemplate` when no usable template exists.
- `stage_verified_asset` verifies replacement bytes before publication and restores the prior file when replacement fails on Windows.
- Sidecar cache hits require both `chat_template` and matching `source`; failed fetches still write `SidecarMeta` for conservative model ID matching.
Operators can now see and select chat templates in the Config UI instead of typing raw `chat_template_file` paths. A bearer-authed `GET /admin/chat-templates` route serves the bundled family catalog, the exact model-ID mapper, and the effective resolution of each configured local chat model. The Local Models tab replaces the text input with an Auto / family / custom-path dropdown plus a read-only resolution summary, and Discover pre-fills the mapped family for known Hugging Face repositories.

- `resolve_chat_template_file` and the admin view share one decision path: `inspect_chat_template` returns a `ChatTemplateResolution` without staging an asset, and the launcher matches exhaustively on its `Decision` payload.
- The route renders the pending shadow config through `load_pending_for_running`, so the displayed resolution tracks unapplied edits.
- A headless build without the `local` feature has no route; the UI treats a 404 as an empty catalog and offers only Auto and Custom path.
- The catalog covers local chat models only; remote and STT entries carry no template resolution.
Untrusted tool and Lua content can quote chat-template control markup that angle-bracket escaping cannot reach, forging turns and tool envelopes the template then honors. `wrap` now runs a `neutralize` pass after `<` escaping that spaces the opener of every delimiter in the new `CONTROL_MARKUP` inventory and breaks every occurrence of the run's nonce, so `[INST]` becomes `[ INST]`. The inventory lives in a new private `inventory` module as a static family-grouped table with a single-pass, allocation-bounded matcher.

- The inventory is closed on purpose; DeepSeek's fullwidth `<?name?>` markers are the one open name class, matched as a bounded class capped at `FULLWIDTH_NAME_MAX` because the family keeps adding spellings.
- `<<SYS>>` matches only at the second bracket via `prev_lt`, so `<SYS>>`, `cout << SYS`, and heredocs stay as typed.
- The pass is idempotent and keeps the byte-identical wrapping invariant (same input, same nonce, same output) that KV-cache prefix sharing depends on.
- Only untrusted tool and Lua content is swept; assistant replay and tool_call wire payloads stay untouched because mutating them breaks the wire format.
The documentation now teaches operators and prompt authors, in Simplified Technical English, how to run the gateway, configure profiles and speech-to-text models, write prompts, and integrate over MCP. Every per-crate user guide is rewritten in that voice, three new guides cover `promptforge-gateway-config`, `promptforge-gateway-local`, and `promptforge-stt`, and `guide/promptforge-user-guide.md` is regenerated from the set.

- The `GUIDES` array in `crates/make-user-guide/src/main.rs` gains `promptforge-gateway-config`, `promptforge-gateway-local`, and `promptforge-stt` entries, so the assembler picks up the three new guide files.
- The gateway `README.md` replaces its profile and voice sections with the single-file layout: `config-version = 2`, `[[profile]]` checklists, `[[stt_model]]` entries, the sibling state file, and `builtin:<family>` chat-template resolution.
- No source or test file changes; the suites are untouched.
An earlier commit added the `chat_template` field to the `GET /admin/model-info` response but left the test in `model_info.rs` asserting the old three-field body, so the test failed against the new wire shape. The expected body now includes `"chat_template": null`, and the route doc comment lists the field beside `architecture`, `layer_count`, and `parameter_count`.

- The fixture GGUF carries no chat-template metadata, so the pinned value is `null`; the populated case stays unpinned.
@vinniefalco
vinniefalco force-pushed the master branch 2 times, most recently from 8a1f311 to ae1be4c Compare August 31, 2026 14:01
Route the Discover README through the gateway instead of a CORS-blocked direct fetch, commit chip-input text on blur so endpoint selection survives Save, add an explicit Eye/EyeOff toggle to secret fields, and redirect whisper.cpp stderr into tracing so it stops corrupting indicatif bars.

- Replace the wildcard `{*repo}` mount with `{owner}/{name}` segments and add `GET /admin/hf/model/{owner}/{name}/readme`, capped at 1 MiB, returning `text/markdown`.
- `chip-input.ts` adds a blur handler and a `flush()` method; `settings-view.ts` adds the Lucide Eye toggle and `controls.css` suppresses `::-ms-reveal`.
- `promptforge-transcribe` enables `tracing_backend` on `whisper-rs` and calls `install_logging_hooks`; the gateway binary defaults the two hook modules to `warn`.
@vinniefalco
vinniefalco merged commit b135da1 into cppalliance:master Aug 31, 2026
5 checks passed
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.

1 participant