From f6111a330eff3072fd9e7b0bd3894420bbd412fe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 18 Sep 2026 02:11:18 +0000 Subject: [PATCH] chore: sync public mirror from internal --- Dockerfile | 5 +- README.md | 6 +- SECURITY.md | 8 +- deploy/helm/maestro/values.yaml | 2 +- docs/AGENT_PROFILES.md | 4 +- docs/ARCHITECTURE.md | 4 +- docs/BUILD_TESTING.md | 2 +- docs/CONDUCTOR_BRIDGE.md | 177 ----- docs/CONTRIBUTOR_RUNBOOK.md | 3 +- docs/FEATURES.md | 4 +- docs/QUICKSTART.md | 4 +- docs/README.md | 4 +- docs/SAFETY.md | 4 +- docs/THREAT_MODEL.md | 3 +- docs/WEB_UI.md | 13 - .../hosted-orb-delegation-acceptance-v1.json | 8 +- package.json | 4 +- .../local-host-rs/src/model_catalog_data.json | 33 +- packages/maestro-rs/src/cli.rs | 12 +- packages/maestro-rs/src/main.rs | 68 +- packages/maestro-rs/tests/dispatch.rs | 16 +- packages/runtime-gateway-rs/src/auth.rs | 170 ----- packages/runtime-gateway-rs/src/http.rs | 58 +- packages/runtime-gateway-rs/src/lib.rs | 53 +- .../runtime-gateway-rs/src/runtime_assets.rs | 320 --------- packages/runtime-gateway-rs/src/tests.rs | 495 +------------- .../tests/library_entrypoint.rs | 19 +- .../docs/user-guide/01-getting-started.md | 2 +- packages/tui-rs/src/mode_cli.rs | 129 +++- packages/tui-rs/src/update_cli.rs | 145 +--- packages/tui-rs/tests/entrypoint.rs | 2 +- packages/web/README.md | 5 - scripts/bridge/install-native-host.mjs | 208 ------ .../bridge/native-host-manifest.example.json | 9 - scripts/bridge/native-host.js | 638 ------------------ scripts/check-deixic-code-naming.mjs | 4 - scripts/check-deixic-code-naming.test.mjs | 4 +- scripts/check-prepared-public-mirror-tree.mjs | 5 - scripts/check-rust-only-docs.mjs | 1 - scripts/generate-runtime-passport.test.mjs | 4 +- scripts/install.sh | 17 +- scripts/materialize-native-package.mjs | 1 - scripts/public-documentation-boundary.mjs | 2 - scripts/run-runtime-conformance.test.mjs | 4 +- scripts/smoke-release-native-only.mjs | 10 +- scripts/test-install.sh | 31 +- scripts/update-lifecycle-contract.test.mjs | 11 +- test/fixtures/rust-cutover/cli-routing.json | 6 +- test/fixtures/rust-cutover/web-routes.json | 2 - 49 files changed, 258 insertions(+), 2481 deletions(-) delete mode 100644 docs/CONDUCTOR_BRIDGE.md delete mode 100644 docs/WEB_UI.md delete mode 100644 packages/runtime-gateway-rs/src/runtime_assets.rs delete mode 100644 packages/web/README.md delete mode 100755 scripts/bridge/install-native-host.mjs delete mode 100644 scripts/bridge/native-host-manifest.example.json delete mode 100644 scripts/bridge/native-host.js diff --git a/Dockerfile b/Dockerfile index aa10eb0fd..2e89248f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,14 +75,13 @@ RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.l && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=native /app/target/release/maestro /usr/local/bin/maestro -COPY packages/web/dist ./packages/web/dist COPY skills ./skills # The image binds to every interface, so the runtime gateway requires API-key # auth. Supply a key at run time, for example: -# docker run -p 3000:3000 -e MAESTRO_WEB_API_KEY="$(openssl rand -hex 32)" ghcr.io/evalops/maestro +# docker run -p 3000:3000 -e MAESTRO_WEB_API_KEY="$(openssl rand -hex 32)" ghcr.io/dx-corp/maestro # Do not add MAESTRO_WEB_REQUIRE_KEY=0 here: it is only honored for loopback # binds and the server refuses to start when it is combined with 0.0.0.0. ENV MAESTRO_CONTROL_HOST=0.0.0.0 PORT=3000 EXPOSE 3000 ENTRYPOINT ["maestro"] -CMD ["web"] +CMD ["serve"] diff --git a/README.md b/README.md index 563c275f8..2b409c5e4 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ deixic-code setup # check auth/config and show the next setup deixic-code "fix the failing test" # interactive with an initial prompt deixic-code exec "summarize this repository" deixic-code --headless # NDJSON protocol over stdio -deixic-code web --port 3000 # browser UI and HTTP runtime gateway +deixic-code serve --port 3000 # HTTP runtime gateway deixic-code hosted-runner ``` @@ -107,14 +107,14 @@ Rust owns every agent/runtime path: - `packages/session-history-rs`: remote redacted session-history adapter - `packages/runtime-gateway-rs`: HTTP/SSE/WebSocket runtime gateway -The repository contains no TypeScript source or TypeScript build toolchain. The browser UI is a versioned static asset snapshot served by the Rust runtime gateway; agent execution, protocols, adapters, CLI, and TUI are Rust. +The repository contains no TypeScript source or TypeScript build toolchain. Agent execution, protocols, adapters, CLI, and TUI are Rust. ```sh cargo test --workspace --locked npm run check:rust-only-runtime ``` -See [Architecture](docs/ARCHITECTURE.md), [Quickstart](docs/QUICKSTART.md), and [Web UI](docs/WEB_UI.md). +See [Architecture](docs/ARCHITECTURE.md) and [Quickstart](docs/QUICKSTART.md). Use the [terminal screenshot framework](docs/tui-screenshots.md) to capture the native UI with local fixtures, custom dimensions, and verifiable PNG bundles. diff --git a/SECURITY.md b/SECURITY.md index 6d2dd4ae3..4bf576286 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,7 +32,7 @@ We follow a 90-day coordinated disclosure window. If you report a vulnerability, This policy applies to the Maestro codebase and its official packages: - `@evalops/maestro` and all `@evalops/*` packages -- Official Docker images (`ghcr.io/evalops/maestro`) +- Official Docker images (`ghcr.io/dx-corp/maestro`) - The Maestro VS Code extension and JetBrains plugin ## Recognition @@ -41,13 +41,13 @@ We appreciate security researchers who help keep Maestro safe. With your permiss ## Running the container image -`ghcr.io/evalops/maestro` binds the runtime gateway to `0.0.0.0`, so it requires +`ghcr.io/dx-corp/maestro` binds the runtime gateway to `0.0.0.0`, so it requires API-key auth and will refuse to start without it: ```bash docker run --rm -p 3000:3000 \ -e MAESTRO_WEB_API_KEY="$(openssl rand -hex 32)" \ - ghcr.io/evalops/maestro + ghcr.io/dx-corp/maestro ``` Clients send the key as `Authorization: Bearer ` or `x-maestro-api-key`. @@ -68,7 +68,7 @@ with or without a port. If you front the server with a tunnel or a proxy that rewrites `Host`, list the extra names: ```bash -MAESTRO_WEB_ALLOWED_HOSTS="maestro.tunnel.example,dev.box.internal" maestro web +MAESTRO_WEB_ALLOWED_HOSTS="maestro.tunnel.example,dev.box.internal" maestro serve ``` Non-loopback binds are not `Host`-checked, because the names that reach them are diff --git a/deploy/helm/maestro/values.yaml b/deploy/helm/maestro/values.yaml index 824bad946..7b655dedf 100644 --- a/deploy/helm/maestro/values.yaml +++ b/deploy/helm/maestro/values.yaml @@ -22,7 +22,7 @@ operatingLayer: evidenceRetentionDays: 90 image: - repository: ghcr.io/evalops/maestro + repository: ghcr.io/dx-corp/maestro # Defaults to Chart.appVersion when empty. Prefer immutable sha-* tags or digest. tag: "" digest: "" diff --git a/docs/AGENT_PROFILES.md b/docs/AGENT_PROFILES.md index ede60d3f4..19681d53f 100644 --- a/docs/AGENT_PROFILES.md +++ b/docs/AGENT_PROFILES.md @@ -43,11 +43,11 @@ budgets: maxCostUsd: 5 ``` -Oracle configurations must be read-only and reasoning-capable at runtime. The built-in profiles prefer a different provider family for the Oracle on higher levels. +Oracle configurations must be read-only and reasoning-capable at runtime. The built-in profiles always choose the Oracle, and the high and ultra reviewer specialists, from a different provider family than the primary model (`cross_provider` in `packages/tui-rs/src/mode_cli.rs`). Runtime reviewer subagent dispatch for the canonical levels still uses the parent provider; see evalops/mono#9321. ## Routing evidence -An assistant response completing without an error is not a verified success. Automatic promotion requires at least 20 verified outcomes for the workload. Verification results, explicit user acceptance, rejection, and retries can supply outcome evidence; unverified runs remain useful for latency and cost observations only. +An assistant response completing without an error is not a verified success. There is no automatic promotion today: the model-candidate lifecycle is not wired into profile defaults, so any promotion is a manual, advisory decision (tracked in evalops/mono#9322). When automatic promotion ships, it must require at least 20 verified outcomes for the workload. Verification results, explicit user acceptance, rejection, and retries can supply outcome evidence; unverified runs remain useful for latency and cost observations only. Clients can request a profile through `X-Maestro-Agent-Profile` (or the compatibility `X-Composer-Agent-Profile`) and inspect the versioned resolved profile on the routing decision. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3fe2961ad..0bed43a55 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -10,8 +10,8 @@ Browser / Slack / GitHub adapters ─HTTP─ packages/runtime-gateway-rs └─ packages/session-rs local persistence ``` -`packages/maestro-rs` classifies commands and invokes Rust libraries in-process. `packages/tui-rs` owns providers, model routing, tools, permissions, session orchestration, hooks, the terminal UI, and the headless protocol. `packages/session-rs` owns local JSONL sessions, forks, indexes, exports, and file checkpoints; `packages/session-history-rs` remains the remote redacted transcript adapter. `packages/runtime-gateway-rs` owns HTTP, SSE, WebSocket, A2A, automation, session, telemetry, and static-web routes. +`packages/maestro-rs` classifies commands and invokes Rust libraries in-process. `packages/tui-rs` owns providers, model routing, tools, permissions, session orchestration, hooks, the terminal UI, and the headless protocol. `packages/session-rs` owns local JSONL sessions, forks, indexes, exports, and file checkpoints; `packages/session-history-rs` remains the remote redacted transcript adapter. `packages/runtime-gateway-rs` owns HTTP, SSE, WebSocket, A2A, automation, session, and telemetry routes. -The browser UI and service adapters may use TypeScript as presentation or transport code. They do not construct agents, load providers, or provide execution fallbacks. They call the Rust runtime gateway. +Service adapters may use TypeScript as presentation or transport code. They do not construct agents, load providers, or provide execution fallbacks. They call the Rust runtime gateway. Release artifacts contain a single native executable per supported platform. The npm package's `bin/maestro` is a shell selector for the packaged executable and requires no Node.js or Bun runtime. diff --git a/docs/BUILD_TESTING.md b/docs/BUILD_TESTING.md index 357b5fc2a..576866aaf 100644 --- a/docs/BUILD_TESTING.md +++ b/docs/BUILD_TESTING.md @@ -20,7 +20,7 @@ npm run build npm run smoke:release-native-only ``` -`npm run build` compiles the optimized `maestro` binary and materializes the native npm package under `vendor/maestro/-`. The checked-in browser assets under `packages/web/dist` are served directly by the Rust control plane. +`npm run build` compiles the optimized `maestro` binary and materializes the native npm package under `vendor/maestro/-`. ## Packed-package verification diff --git a/docs/CONDUCTOR_BRIDGE.md b/docs/CONDUCTOR_BRIDGE.md deleted file mode 100644 index bc8a9f7bf..000000000 --- a/docs/CONDUCTOR_BRIDGE.md +++ /dev/null @@ -1,177 +0,0 @@ -# Conductor Bridge (Local Development) - -This guide describes how to connect the Conductor Chrome extension to a local -Deixic Code web server during development, plus an optional native messaging host -that can launch and monitor Deixic Code automatically. - -## Quick Start (HTTP Bridge) - -1. Start Deixic Code's web server, allowing your Conductor extension's origin: - -```bash -MAESTRO_WEB_REQUIRE_REDIS=0 \ - MAESTRO_WEB_ORIGIN="chrome-extension://" \ - maestro web -``` - -`maestro web` binds to `127.0.0.1` and runs without an API key by default, so -`MAESTRO_WEB_REQUIRE_KEY=0` is unnecessary here. Never set -`MAESTRO_WEB_ORIGIN="*"`: it tells the server to accept every origin, so any -page you visit while the server is running can call the local agent runtime. - -2. In Conductor settings, enable "Maestro Bridge" (the extension's legacy compatibility label) and set: - -``` -http://localhost:8080 -``` - -Conductor will fetch `/api/models` and stream `/api/chat` responses from the -Deixic Code server. - -## Capabilities (Client-Side Tools) - -When the Conductor extension is connected, Deixic Code can delegate browser actions -to the client. These tools execute inside the browser context and return results -to the server. - -| Category | Examples | -| --- | --- | -| Operator loop | `browser_operator` observes the active page, performs one semantic action, and returns verification plus latest page state | -| Read & search | `read_page`, `search_page`, `find_on_page`, `extract_links`, `extract_table_data`, `extract_document` | -| Navigation & tabs | `navigate_to`, `open_links_in_tabs`, `wait_for_selector`, `scroll_page` | -| Interaction | `click_element`, `type_text`, `select_element`, `highlight_element`, `mouse_action`, `pointer_action`, `keyboard_action` | -| Diagnostics | `capture_screenshot`, `capture_network`, `capture_console_errors`, `collect_diagnostics` | -| Native actions (CDP) | `native_click`, `native_type`, `native_press`, `native_key_down`, `native_key_up` | -| Skills & artifacts | `run_skill`, `manage_artifact`, `patch_artifact` | -| MCP bridging | `list_mcp_servers`, `list_mcp_tools`, `list_mcp_resources`, `read_mcp_resource` | - -`browser_operator` is the preferred task-level browser-control surface. Call it -first with `phase: "observe"`, a `goal`, and no action to inspect the current -page. Then call it with `phase: "act"`, the same `goal`, the prior -`previous_observation_id` when available, an `expected_result`, and exactly one -semantic action such as `{ "kind": "click", "refId": "..." }`, -`{ "kind": "type", "refId": "...", "text": "..." }`, or -`{ "kind": "select", "selector": "...", "value": "..." }`. - -Prefer the `refId` returned by observation over a CSS selector. For targets -inside embedded apps or iframes, preserve both `frameId` and `refId` from the -observed element and send them back with the action. Use `include_frames: true` -on observe/recover calls when the target may be inside a frame. Observation -results may also include page-observer freshness metadata; if a frame changed -after the prior observation, recover by observing again before retrying. - -Conductor executes the action in the active tab, refreshes the page observation, -and returns verification fields instead of making the model infer success from a -low-level click or keypress alone. If verification fails or the ref/selector is -stale, call `browser_operator` again with `phase: "recover"` and no typed secret -values; the client should re-observe before retrying. - -## Optional: Native Messaging Host (Auto-Launch + Status) - -Deixic Code ships a local native messaging host script at: - -``` -composer/scripts/bridge/native-host.js -``` - -The host supports: -- `status` requests (probe `/api/bridge/status`) -- `launch` requests (start `maestro web` if needed) -- JSON-RPC notifications (`bridge/status`) when connectivity changes -- CRX-style browser-control decision notifications - (`onBrowserControlDecision`) forwarded to Platform `RecordRunEvent` when - Agent Runtime configuration is present - -### Install the native host manifest - -1. (Recommended) Run the installer script: - -``` -node composer/scripts/bridge/install-native-host.mjs --extension-id -``` - -This attempts to auto-detect the extension ID (via `CONDUCTOR_EXTENSION_ID`, -`CONDUCTOR_PEM_PATH`, or an installed Chrome profile) and writes the manifest to -the correct OS location. - -2. Or copy the example manifest: - -``` -composer/scripts/bridge/native-host-manifest.example.json -``` - -3. Replace `path` with the absolute path to `native-host.js`. -4. Replace `YOUR_EXTENSION_ID` with the Conductor extension ID. - -Place the manifest in the standard Chrome location (if you did not use the script): - -- macOS: `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/` -- Linux: `~/.config/google-chrome/NativeMessagingHosts/` -- Windows: `%LOCALAPPDATA%\\Google\\Chrome\\User Data\\NativeMessagingHosts\\` - -### Host configuration (env) - -| Variable | Purpose | Default | -| --- | --- | --- | -| `MAESTRO_BRIDGE_BASE_URL` | Base URL to probe/launch | `http://localhost:8080` | -| `MAESTRO_BRIDGE_COMMAND` | Command to launch Deixic Code | `maestro` | -| `MAESTRO_BRIDGE_ARGS` | Extra args (JSON array or space-delimited) | empty | -| `MAESTRO_BRIDGE_POLL_MS` | Status poll interval (ms) | `2000` | -| `MAESTRO_BRIDGE_LAUNCH_TIMEOUT_MS` | Launch timeout (ms) | `15000` | -| `MAESTRO_BRIDGE_AGENT_RUNTIME_URL` | Optional Agent Runtime base URL for browser-control decision events | empty | -| `MAESTRO_BRIDGE_AGENT_RUNTIME_TOKEN` | Optional bearer token for Agent Runtime | empty | -| `MAESTRO_BRIDGE_AGENT_RUNTIME_ORG_ID` | Optional organization header for Agent Runtime | empty | -| `MAESTRO_BRIDGE_PLATFORM_RUN_ID` | Fallback Platform run ID when the Conductor receipt does not include one | empty | -| `MAESTRO_BRIDGE_PLATFORM_RUNTIME_TIMEOUT_MS` | Agent Runtime event write timeout (ms) | `2000` | - -When the host launches Deixic Code, and unless the variable is already set, it -sets: - -``` -MAESTRO_WEB_REQUIRE_REDIS=0 -MAESTRO_WEB_ORIGIN="chrome-extension://" -``` - -The extension ID comes from the origin argument Chrome passes to the native -messaging host, or from `CONDUCTOR_EXTENSION_ID`. When neither is available the -host leaves `MAESTRO_WEB_ORIGIN` unset so the server keeps its built-in -localhost allowlist; it never falls back to `*`. - -The host does not set `MAESTRO_WEB_REQUIRE_KEY`. Deixic Code's default loopback bind -already runs without an API key, and forcing the kill switch on would strip auth -from a non-loopback bind an operator configured deliberately. - -When `onBrowserControlDecision` includes `platformRunId`, the native host writes -a channel-safe `RUNTIME_EVENT_TYPE_AGENT_PROGRESS_RECORDED` event with -`schemaVersion=browser-control-runtime-decision/v1`. Platform projects that -event into the browser-control decision metric and Deploy alerts on missing or -invalid Platform receipts. - -## Notes - -- The HTTP bridge supports Conductor client tools (browser automation) because - the Deixic Code web server exposes the client tool API. -- Never use `MAESTRO_WEB_ORIGIN="*"`, in development or production. - -## Security & CORS Guidance - -The bridge exposes tool execution, so treat its CORS configuration as an -authorization boundary: - -- Lock CORS to your extension origin: - `MAESTRO_WEB_ORIGIN="chrome-extension://"`. The server also - accepts the built-in `localhost`/`127.0.0.1` development origins so the web UI - keeps working. -- `MAESTRO_WEB_ORIGIN="*"` means "this is a public API". The server answers with - a literal `Access-Control-Allow-Origin: *` and withholds - `Access-Control-Allow-Credentials`, but it still accepts WebSocket upgrades - from any origin, so a wildcard is never appropriate for a local bridge. -- Keep the bridge on localhost unless you explicitly need remote access. On a - non-loopback bind the server requires auth and refuses to start with - `MAESTRO_WEB_REQUIRE_KEY=0`. -- On a loopback bind the server also validates the `Host` header and answers - `421 Misdirected Request` for names that do not resolve to that interface, so - a DNS-rebinding page cannot reach the bridge. Use `MAESTRO_WEB_ALLOWED_HOSTS` - if you front the bridge with a tunnel that rewrites `Host`. -- For shared deployments set `MAESTRO_WEB_REQUIRE_KEY=1` and configure - `MAESTRO_WEB_API_KEY`. diff --git a/docs/CONTRIBUTOR_RUNBOOK.md b/docs/CONTRIBUTOR_RUNBOOK.md index 74c39d2f0..2d25d296a 100644 --- a/docs/CONTRIBUTOR_RUNBOOK.md +++ b/docs/CONTRIBUTOR_RUNBOOK.md @@ -19,7 +19,7 @@ npm run build npm run smoke:release-native-only ``` -The browser bundle under `packages/web/dist` is a versioned static input. Product code, protocols, adapters, CLI, TUI, and the runtime gateway are Rust. +Product code, protocols, adapters, CLI, TUI, and the runtime gateway are Rust. ## 2. Inner loop @@ -44,7 +44,6 @@ The browser bundle under `packages/web/dist` is a versioned static input. Produc ## 4. Docs and references - TUI/CLI UX: `docs/FEATURES.md` -- Web parity: `docs/WEB_UI.md` - Tool behavior: `docs/TOOLS_REFERENCE.md` - Native crates: `packages/maestro-rs`, `packages/tui-rs`, `packages/runtime-gateway-rs`, and `packages/ambient-agent-rs` - Historical design documents are reference material, not current build instructions. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index f8ea6ffac..e98a32f2e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -11,8 +11,8 @@ protocol, storage, environment, and artifact identifiers for compatibility. - Persistent sessions, resume/export/import, usage and cost accounting - Account-scoped hosted Computer handoffs: freeze a selected remote diff/files/artifacts for another same-tenant thread, then list or read the immutable package by digest -- Web runtime gateway with SSE/WebSocket chat, automations, A2A, hosted runners, telemetry, and model management +- Runtime gateway (`maestro serve`) with SSE/WebSocket chat, automations, A2A, hosted runners, telemetry, and model management - Scripted scenarios, replay, trajectory evaluation, and protocol conformance - Slack and GitHub adapters backed by the native runtime gateway -All product execution, browser runtime-gateway, and adapter behavior runs through Rust. The browser client is a checked-in static asset snapshot served by the native runtime gateway. +All product execution, runtime-gateway, and adapter behavior runs through Rust. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 71ad1bcaa..5f8c8978f 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,6 +1,6 @@ # Quickstart -Install Rust and Node.js for repository development. The browser UI is checked in as a versioned static snapshot; all product behavior is built from Rust. +Install Rust and Node.js for repository development. All product behavior is built from Rust. ```sh npm install @@ -8,7 +8,7 @@ npm run build ./bin/deixic-code --version ./bin/deixic-code setup ./bin/deixic-code -./bin/deixic-code web --port 3000 +./bin/deixic-code serve --port 3000 ``` Run checks with: diff --git a/docs/README.md b/docs/README.md index 65d8ca94a..e6ffb98e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,14 +3,12 @@ Use this index to jump to the right guide quickly and see how the pieces connect. ## Start Here -- [Quickstart](QUICKSTART.md) — installation, environment prep, builds, and the fast path to running the CLI/TUI/Web. +- [Quickstart](QUICKSTART.md) — installation, environment prep, builds, and the fast path to running the CLI/TUI. - [TUI User Guide](../packages/tui-rs/docs/user-guide/README.md) — numbered native TUI guide (getting started through worktrees). - [Contributor Runbook](CONTRIBUTOR_RUNBOOK.md) — day-one flow for contributors (build, lint, verify). - [Feature Guide](FEATURES.md) — user-facing capabilities across the TUI and CLI with power-user tips. - [Dynamic workflows](DYNAMIC_WORKFLOWS.md) — run bounded, resumable workflow graphs from the native CLI. -- [Web UI Guide](WEB_UI.md) — browser workflow and TUI/Web parity appendix. - [Compatibility and migration](DEIXIC_CODE_MIGRATION.md) — canonical names, retained Maestro coordinates, and publication boundaries. -- [Conductor Bridge](CONDUCTOR_BRIDGE.md) — connect the Conductor extension to a local Deixic Code server. ## Core Reference - [Tools Reference](TOOLS_REFERENCE.md) — authoritative slash command and flag definitions. diff --git a/docs/SAFETY.md b/docs/SAFETY.md index 8fe8bcdfc..3142c6d46 100644 --- a/docs/SAFETY.md +++ b/docs/SAFETY.md @@ -6,7 +6,7 @@ Audience: operators and contributors configuring approvals and sandboxing. -Nav: [Docs index](README.md) · [Quickstart](QUICKSTART.md) · [Web UI](WEB_UI.md) +Nav: [Docs index](README.md) · [Quickstart](QUICKSTART.md) Deixic Code can execute shell commands and change files on the host. The action firewall, approval flow, policy checks, and optional native sandbox are @@ -216,7 +216,7 @@ For a shared or remote web deployment: MAESTRO_PROFILE=prod \ MAESTRO_JWT_JWKS_URL="https://identity.example/.well-known/jwks.json" \ MAESTRO_WEB_CSRF_TOKEN="$(openssl rand -hex 32)" \ -maestro web +maestro serve ``` The runtime gateway requires authentication on non-loopback binds. It accepts diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 22681ea57..8bcc86a38 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -210,7 +210,6 @@ The HTTP runtime gateway is authenticated by The custom HMAC `MAESTRO_AUTH_SHARED_SECRET` authenticator is gone. - A trusted proxy can authenticate a subject with `MAESTRO_WEB_TRUST_PROXY_AUTH_TOKEN` and an identity header. -- Runtime session cookies are HMAC-bound to the configured API key. - State-changing `/api/` and A2A requests require the configured CSRF token when CSRF enforcement is enabled. - Non-loopback binds require configured authentication. Disabling the key @@ -250,7 +249,7 @@ For a local or shared deployment: - For a remote bind, configure a supported tenant-bearing JWT or trusted-proxy authenticator for session and chat routes. A static `MAESTRO_WEB_API_KEY` may additionally protect non-tenant process routes; use `MAESTRO_PROFILE=prod` - and configure CSRF for browser clients. + and configure CSRF for remote access. - Keep approval mode at prompt or fail for untrusted work. Do not combine automatic approval with `danger-full-access`. - Select `read-only` or `workspace-write` sandboxing where the host supports diff --git a/docs/WEB_UI.md b/docs/WEB_UI.md deleted file mode 100644 index f030a5ebd..000000000 --- a/docs/WEB_UI.md +++ /dev/null @@ -1,13 +0,0 @@ -# Web UI - -Run the native runtime gateway and browser UI with: - -```sh -deixic-code web --port 3000 -``` - -The canonical Rust process serves `packages/web/dist`, HTTP APIs, SSE and WebSocket chat, sessions, models, telemetry, automations, A2A, and hosted-runner endpoints. Chat invokes the Rust agent core in a native child only where isolation is required; it never invokes a JavaScript agent. - -For local browser development, build or run `packages/web` separately and point it at the native runtime gateway. Production containers contain the Rust executable and compiled static browser assets; Node.js and Bun are absent from the runtime image. - -`GET /healthz` is the unauthenticated process health endpoint. Configure authentication and persistence with the retained `MAESTRO_WEB_*` compatibility environment variables documented by `deixic-code web --help` and the deployment configuration. diff --git a/docs/protocols/hosted-orb-delegation-acceptance-v1.json b/docs/protocols/hosted-orb-delegation-acceptance-v1.json index 86ae0f447..713ceacac 100644 --- a/docs/protocols/hosted-orb-delegation-acceptance-v1.json +++ b/docs/protocols/hosted-orb-delegation-acceptance-v1.json @@ -1482,7 +1482,7 @@ "tenant": "tenant-a", "request": { "project": "fixture-project", - "repository_url": "https://github.com/evalops/maestro.git", + "repository_url": "https://github.com/evalops/mono.git", "prompt": "fixture atomic prompt", "idempotency_key": "atomic-case-1" }, @@ -1522,7 +1522,7 @@ "tenant": "tenant-a", "request": { "project": "fixture-project", - "repository_url": "https://github.com/evalops/maestro.git", + "repository_url": "https://github.com/evalops/mono.git", "prompt": "fixture atomic prompt", "idempotency_key": "atomic-case-1" }, @@ -1592,7 +1592,7 @@ "tenant": "tenant-a", "request": { "project": "fixture-project", - "repository_url": "https://github.com/evalops/maestro.git", + "repository_url": "https://github.com/evalops/mono.git", "prompt": "fixture override prompt", "runtime_profile": "sandboxwich/restricted/sandboxed_container/unprivileged", "idempotency_key": "atomic-override-1" @@ -1732,7 +1732,7 @@ "tenant": "tenant-a", "request": { "project": "fixture-project", - "repository_url": "https://github.com/evalops/maestro.git", + "repository_url": "https://github.com/evalops/mono.git", "prompt": "fixture rejected prompt", "runtime_profile": "sandboxwich/unavailable/sandboxed_container/unprivileged", "idempotency_key": "atomic-rejected-1" diff --git a/package.json b/package.json index 96f024bce..c56f936d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/deixic-code", - "description": "Deixic Code — native Rust coding agent, CLI, TUI, and web runtime gateway", + "description": "Deixic Code — native Rust coding agent, CLI, TUI, and runtime gateway", "version": "0.10.90", "private": false, "type": "module", @@ -19,7 +19,6 @@ "bin", "vendor/maestro", "skills", - "packages/web/dist", "proto/maestro/v1/protocol-compatibility-manifest.json" ], "scripts": { @@ -35,7 +34,6 @@ "start": "./bin/deixic-code", "cli": "./bin/deixic-code", "tui": "./bin/deixic-code", - "web": "./bin/deixic-code web", "check:localization": "python3 scripts/check-localization.py && python3 scripts/test_check_localization.py", "check:deixic-code-naming": "node scripts/check-deixic-code-naming.mjs && node --test scripts/check-deixic-code-naming.test.mjs", "check:workspace-contract": "node scripts/check-workspace-contract.mjs", diff --git a/packages/local-host-rs/src/model_catalog_data.json b/packages/local-host-rs/src/model_catalog_data.json index 395aa230a..981ea0f19 100644 --- a/packages/local-host-rs/src/model_catalog_data.json +++ b/packages/local-host-rs/src/model_catalog_data.json @@ -1,5 +1,5 @@ { - "generated_at": 1789544007, + "generated_at": 1789630828, "source": "https://models.dev/api.json+https://openrouter.ai/api/v1/models", "models": [ { @@ -1324,7 +1324,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1048576, - "output_tokens": 943718 + "output_tokens": 384000 }, "verification": { "state": "catalog", @@ -1552,7 +1552,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1310720, - "output_tokens": 943718 + "output_tokens": 235929 }, "verification": { "state": "catalog", @@ -6736,7 +6736,7 @@ "reasoning": false, "streaming": true, "context_tokens": 262144, - "output_tokens": 16384 + "output_tokens": 32768 }, "verification": { "state": "catalog", @@ -7408,6 +7408,25 @@ "source": "openrouter" } }, + { + "id": "stealth/union-alpha", + "name": "Union Alpha", + "provider": "openrouter", + "description": "Union Alpha is a multimodal model built for research, coding, and agentic workflows, while delivering frontier-level…", + "capabilities": { + "protocol": "openai-chat", + "tools": true, + "vision": true, + "reasoning": false, + "streaming": true, + "context_tokens": 262144, + "output_tokens": 131072 + }, + "verification": { + "state": "catalog", + "source": "openrouter" + } + }, { "id": "stepfun/step-3.5-flash", "name": "StepFun: Step 3.5 Flash", @@ -7624,7 +7643,7 @@ "description": "UnslopNemo v4.1 is the latest addition from the creator of Rocinante, designed for adventure writing and role-play…", "capabilities": { "protocol": "openai-chat", - "tools": true, + "tools": false, "vision": false, "reasoning": false, "streaming": true, @@ -8009,7 +8028,7 @@ "reasoning": true, "streaming": true, "context_tokens": 204800, - "output_tokens": 16384 + "output_tokens": 131072 }, "verification": { "state": "catalog", @@ -8142,7 +8161,7 @@ "reasoning": true, "streaming": true, "context_tokens": 1048576, - "output_tokens": 128000 + "output_tokens": 131072 }, "verification": { "state": "catalog", diff --git a/packages/maestro-rs/src/cli.rs b/packages/maestro-rs/src/cli.rs index af245d167..7897ed31f 100644 --- a/packages/maestro-rs/src/cli.rs +++ b/packages/maestro-rs/src/cli.rs @@ -13,7 +13,7 @@ use std::ffi::OsString; /// variant removes that dead computation and the duplicated table. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Command { - Web { port: Option }, + Serve { port: Option }, Forward, Help, Version, @@ -43,7 +43,7 @@ where ) { return Ok(Command::Help); } - if first == Some("web") { + if first == Some("serve") { let mut port = None; let mut index = 1; while index < strings.len() { @@ -54,22 +54,22 @@ where port = Some( value .parse::() - .map_err(|_| format!("invalid web port: {value}"))?, + .map_err(|_| format!("invalid serve port: {value}"))?, ); } else if let Some(value) = argument.strip_prefix("--port=") { port = Some( value .parse::() - .map_err(|_| format!("invalid web port: {value}"))?, + .map_err(|_| format!("invalid serve port: {value}"))?, ); } else { return Err(format!( - "`maestro web` does not accept prompt arguments or option `{argument}`" + "`maestro serve` does not accept prompt arguments or option `{argument}`" )); } index += 1; } - return Ok(Command::Web { port }); + return Ok(Command::Serve { port }); } // Every other invocation (interactive TUI, `exec`/`print`/`-p`, diff --git a/packages/maestro-rs/src/main.rs b/packages/maestro-rs/src/main.rs index d730965a5..91286e820 100644 --- a/packages/maestro-rs/src/main.rs +++ b/packages/maestro-rs/src/main.rs @@ -1,7 +1,6 @@ use anyhow::Result; use maestro::{Command, classify}; use maestro_runtime_gateway::{RuntimeGatewayConfig, serve}; -use std::path::{Path, PathBuf}; // Process hardening must run before `main` — and therefore before the Tokio // runtime spawns worker threads — so the prctl/setrlimit calls and the @@ -12,31 +11,7 @@ fn pre_main_hardening() { maestro_tui::process_hardening::pre_main_hardening(); } -fn web_static_root(executable: &Path) -> Option { - let executable_dir = executable.parent()?; - let installed_assets = executable_dir.join("maestro-web"); - if installed_assets.join("index.html").is_file() { - return Some(installed_assets); - } - - executable_dir.ancestors().find_map(|ancestor| { - let candidate = ancestor.join("packages/web/dist"); - candidate.join("index.html").is_file().then_some(candidate) - }) -} - -fn configure_web_static_root() { - if std::env::var_os("MAESTRO_WEB_STATIC_ROOT").is_some() { - return; - } - if let Ok(executable) = std::env::current_exe() { - if let Some(root) = web_static_root(&executable) { - std::env::set_var("MAESTRO_WEB_STATIC_ROOT", root); - } - } -} - -const HELP: &str = "Deixic Code\n\nUsage:\n deixic-code [options] [prompt]\n deixic-code exec \n deixic-code -w [prompt]\n deixic-code doctor [--json] [--live] [--model ]\n deixic-code setup [--json] [--live] [--model ]\n deixic-code --headless\n deixic-code web [--port ]\n deixic-code hosted-runner [options]\n\nCommon commands:\n deixic-code setup Check authentication and show the next setup step\n deixic-code config Inspect or change Deixic Code configuration\n deixic-code experiments Review or change tool experiment participation\n deixic-code models List available models and providers\n deixic-code sessions List, export, or import saved sessions\n deixic-code scenario Run or inspect scripted scenarios\n deixic-code run Inspect or replay a saved session\n\nAdvanced commands:\n deixic-code diagnostics Project compiler JSONL, optionally against a prior observation\n deixic-code codex Manage Codex authentication and app-server access\n deixic-code mcp Configure MCP servers\n deixic-code plugins Inspect and manage plugins\n deixic-code connections Manage API keys and delegated accounts\n deixic-code init Register a Deixic agent integration\n\nCompatibility:\n maestro remains available as an alias; MAESTRO_* variables and .maestro paths are unchanged.\n\nOptions:\n -w, --worktree Run the session in a new git worktree at ../-wt-\n on a new branch; interactive worktrees and branches are kept\n exec removes only clean worktrees with no new commits\n\nThe product runtime is native Rust; no Node.js or Bun runtime is required."; +const HELP: &str = "Deixic Code\n\nUsage:\n deixic-code [options] [prompt]\n deixic-code exec \n deixic-code -w [prompt]\n deixic-code doctor [--json] [--live] [--model ]\n deixic-code setup [--json] [--live] [--model ]\n deixic-code --headless\n deixic-code serve [--port ]\n deixic-code hosted-runner [options]\n\nCommon commands:\n deixic-code setup Check authentication and show the next setup step\n deixic-code config Inspect or change Deixic Code configuration\n deixic-code experiments Review or change tool experiment participation\n deixic-code models List available models and providers\n deixic-code sessions List, export, or import saved sessions\n deixic-code scenario Run or inspect scripted scenarios\n deixic-code run Inspect or replay a saved session\n\nAdvanced commands:\n deixic-code diagnostics Project compiler JSONL, optionally against a prior observation\n deixic-code codex Manage Codex authentication and app-server access\n deixic-code mcp Configure MCP servers\n deixic-code plugins Inspect and manage plugins\n deixic-code connections Manage API keys and delegated accounts\n deixic-code init Register a Deixic agent integration\n\nCompatibility:\n maestro remains available as an alias; MAESTRO_* variables and .maestro paths are unchanged.\n\nOptions:\n -w, --worktree Run the session in a new git worktree at ../-wt-\n on a new branch; interactive worktrees and branches are kept\n exec removes only clean worktrees with no new commits\n\nThe product runtime is native Rust; no Node.js or Bun runtime is required."; const VERSION: &str = concat!("deixic-code ", env!("CARGO_PKG_VERSION")); fn sync_command_output(command: &Command) -> Option<&'static str> { @@ -65,8 +40,7 @@ fn main() -> Result<()> { .build()? .block_on(async move { match command { - Command::Web { port } => { - configure_web_static_root(); + Command::Serve { port } => { if let Some(port) = port { std::env::set_var("PORT", port.to_string()); } @@ -80,44 +54,8 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { - use super::{HELP, VERSION, sync_command_output, web_static_root}; + use super::{HELP, VERSION, sync_command_output}; use maestro::Command; - use std::{fs, path::PathBuf}; - - fn test_root(label: &str) -> PathBuf { - std::env::temp_dir().join(format!( - "maestro-web-root-{label}-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("clock should follow the Unix epoch") - .as_nanos() - )) - } - - #[test] - fn finds_assets_next_to_installed_binary() { - let root = test_root("installed"); - let executable = root.join("bin/maestro"); - let assets = root.join("bin/maestro-web"); - fs::create_dir_all(&assets).expect("asset directory should be created"); - fs::write(assets.join("index.html"), "ok").expect("index should be written"); - - assert_eq!(web_static_root(&executable), Some(assets)); - fs::remove_dir_all(root).expect("test directory should be removed"); - } - - #[test] - fn finds_assets_from_native_npm_package() { - let root = test_root("npm"); - let executable = root.join("vendor/maestro/linux-x64/maestro"); - let assets = root.join("packages/web/dist"); - fs::create_dir_all(&assets).expect("asset directory should be created"); - fs::write(assets.join("index.html"), "ok").expect("index should be written"); - - assert_eq!(web_static_root(&executable), Some(assets)); - fs::remove_dir_all(root).expect("test directory should be removed"); - } #[test] fn help_and_version_are_handled_without_async_runtime() { diff --git a/packages/maestro-rs/tests/dispatch.rs b/packages/maestro-rs/tests/dispatch.rs index 7ee8555d8..2679c8d84 100644 --- a/packages/maestro-rs/tests/dispatch.rs +++ b/packages/maestro-rs/tests/dispatch.rs @@ -25,18 +25,18 @@ struct FixtureCase { } #[test] -fn web_dispatches_to_in_process_control_plane() { - assert_eq!(classify(["web"]).unwrap(), Command::Web { port: None }); +fn serve_dispatches_to_in_process_control_plane() { + assert_eq!(classify(["serve"]).unwrap(), Command::Serve { port: None }); assert_eq!( - classify(["web", "--port", "9090"]).unwrap(), - Command::Web { port: Some(9090) } + classify(["serve", "--port", "9090"]).unwrap(), + Command::Serve { port: Some(9090) } ); } #[test] fn exec_is_forwarded_to_native_dispatch() { // `classify` no longer distinguishes exec/print/headless/utility argv from - // one another; it only needs to know whether to serve the in-process web + // one another; it only needs to know whether to serve the in-process // control plane. Everything else forwards to `maestro_tui::run_cli`, which // owns the real routing decision (see `packages/tui-rs/tests/entrypoint.rs`). assert_eq!(classify(["exec", "hello"]).unwrap(), Command::Forward); @@ -84,7 +84,7 @@ fn frozen_cli_routes_are_owned_by_native_dispatch() { let command = result.unwrap_or_else(|error| panic!("{}: {error}", case.name)); match (case.route.as_str(), case.name.as_str()) { ("native-control-plane", _) => { - assert_eq!(command, Command::Web { port: None }, "{}", case.name) + assert_eq!(command, Command::Serve { port: None }, "{}", case.name) } ("native", "version") => assert_eq!(command, Command::Version, "{}", case.name), ("native", "help" | "hidden-help") => { @@ -99,8 +99,8 @@ fn frozen_cli_routes_are_owned_by_native_dispatch() { } #[test] -fn web_rejects_prompt_arguments() { - assert!(classify(["web", "prompt"]).is_err()); +fn serve_rejects_prompt_arguments() { + assert!(classify(["serve", "prompt"]).is_err()); } #[test] diff --git a/packages/runtime-gateway-rs/src/auth.rs b/packages/runtime-gateway-rs/src/auth.rs index 6737b0bc5..89265af8c 100644 --- a/packages/runtime-gateway-rs/src/auth.rs +++ b/packages/runtime-gateway-rs/src/auth.rs @@ -12,7 +12,6 @@ use crate::{Config, now_millis, trimmed_env}; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub(crate) enum AuthSource { IdentityJwt, - SessionCookie, StaticGatewayKey, TrustedProxy, #[default] @@ -33,7 +32,6 @@ impl AuthContext { pub(crate) fn actor_label(&self) -> String { let source = match self.source { AuthSource::IdentityJwt => "identity-jwt", - AuthSource::SessionCookie => "session-cookie", AuthSource::StaticGatewayKey => "api-key", AuthSource::TrustedProxy => "trusted-proxy", AuthSource::LoopbackDev => "loopback-dev", @@ -60,20 +58,6 @@ impl AuthContext { } } -enum RuntimeSessionAuth { - Scoped { - subject: String, - organization_id: Option, - workspace_id: Option, - scopes: Vec, - }, - ApiKey, -} - -pub(crate) const RUNTIME_SESSION_COOKIE_NAME: &str = "maestro_web_session"; -const RUNTIME_SESSION_COOKIE_CONTEXT: &[u8] = b"maestro-web-session:v1"; -const RUNTIME_SESSION_SCOPED_COOKIE_CONTEXT: &[u8] = b"maestro-web-session-scoped:v1"; -const RUNTIME_SESSION_API_KEY_COOKIE_CONTEXT: &[u8] = b"maestro-web-session-api-key:v1"; const RUNTIME_WRITE_SCOPE: &str = "maestro:write"; // Elevated role required to mutate process-global control surfaces: the managed // enterprise safety policy (`/api/admin/*`) and MCP server configuration @@ -329,30 +313,6 @@ pub(crate) fn auth_context(head: &RequestHead, config: &Config) -> Option AuthContext { - source: AuthSource::StaticGatewayKey, - scopes: configured_api_key_scopes(), - unrestricted: static_key_is_unrestricted(config), - ..AuthContext::default() - }, - RuntimeSessionAuth::Scoped { - subject, - organization_id, - workspace_id, - scopes, - } => AuthContext { - subject: Some(subject), - organization_id, - workspace_id, - scopes, - source: AuthSource::SessionCookie, - unrestricted: false, - }, - }); - } - if !config.require_key && !auth_is_configured(config) { return Some(AuthContext { source: AuthSource::LoopbackDev, @@ -383,78 +343,6 @@ pub(crate) fn header_auth_matches( .unwrap_or(false) } -fn runtime_session_cookie_auth(head: &RequestHead, config: &Config) -> Option { - let provided = cookie_value(head, RUNTIME_SESSION_COOKIE_NAME)?; - let (encoded_subject, _signature) = provided.split_once('.')?; - let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(encoded_subject).ok()?).ok()?; - let api_key_expected = runtime_session_api_key_cookie_value(config)?; - if constant_time_eq(provided.as_bytes(), api_key_expected.as_bytes()) { - return Some(RuntimeSessionAuth::ApiKey); - } - if let Some(expected) = runtime_session_cookie_value_for_payload( - config, - RUNTIME_SESSION_SCOPED_COOKIE_CONTEXT, - &payload, - ) { - if constant_time_eq(provided.as_bytes(), expected.as_bytes()) { - let payload: Value = serde_json::from_str(&payload).ok()?; - let subject = payload - .get("subject") - .and_then(Value::as_str) - .and_then(nonempty_str) - .map(str::to_owned)?; - let organization_id = payload - .get("organizationId") - .or_else(|| payload.get("organization_id")) - .and_then(Value::as_str) - .and_then(nonempty_str) - .map(str::to_owned); - let workspace_id = payload - .get("workspaceId") - .or_else(|| payload.get("workspace_id")) - .and_then(Value::as_str) - .and_then(nonempty_str) - .map(str::to_owned); - let scopes = payload - .get("scopes") - .and_then(Value::as_array) - .map(|values| { - values - .iter() - .filter_map(Value::as_str) - .map(str::trim) - .filter(|scope| !scope.is_empty()) - .map(str::to_owned) - .collect() - }) - .unwrap_or_default(); - return Some(RuntimeSessionAuth::Scoped { - subject, - organization_id, - workspace_id, - scopes, - }); - } - } - let expected = runtime_session_cookie_value(config, &payload)?; - constant_time_eq(provided.as_bytes(), expected.as_bytes()).then_some( - RuntimeSessionAuth::Scoped { - subject: payload, - organization_id: None, - workspace_id: None, - scopes: Vec::new(), - }, - ) -} - -pub(crate) fn cookie_value<'a>(head: &'a RequestHead, name: &str) -> Option<&'a str> { - let cookies = head.headers.get("cookie")?; - cookies.split(';').find_map(|cookie| { - let (cookie_name, value) = cookie.trim().split_once('=')?; - (cookie_name == name).then_some(value) - }) -} - pub(crate) fn trusted_proxy_auth_identity(head: &RequestHead) -> Option<(String, Vec)> { let expected_token = trimmed_env("MAESTRO_WEB_TRUST_PROXY_AUTH_TOKEN")?; let provided_token = head @@ -984,61 +872,3 @@ pub(crate) fn hmac_sha256_base64url(secret: &[u8], payload: &[u8]) -> String { mac.update(payload); URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()) } - -pub(crate) fn runtime_session_cookie_value(config: &Config, subject: &str) -> Option { - runtime_session_cookie_value_for_payload(config, RUNTIME_SESSION_COOKIE_CONTEXT, subject) -} - -pub(crate) fn runtime_session_cookie_value_with_identity( - config: &Config, - subject: &str, - organization_id: Option<&str>, - workspace_id: Option<&str>, - scopes: &[String], -) -> Option { - let mut payload = serde_json::Map::new(); - payload.insert("subject".to_string(), Value::String(subject.to_string())); - if let Some(organization_id) = organization_id.and_then(nonempty_str) { - payload.insert( - "organizationId".to_string(), - Value::String(organization_id.to_string()), - ); - } - if let Some(workspace_id) = workspace_id.and_then(nonempty_str) { - payload.insert( - "workspaceId".to_string(), - Value::String(workspace_id.to_string()), - ); - } - payload.insert("scopes".to_string(), serde_json::json!(scopes)); - let payload = Value::Object(payload); - let payload = serde_json::to_string(&payload).ok()?; - runtime_session_cookie_value_for_payload( - config, - RUNTIME_SESSION_SCOPED_COOKIE_CONTEXT, - &payload, - ) -} - -pub(crate) fn runtime_session_api_key_cookie_value(config: &Config) -> Option { - runtime_session_cookie_value_for_payload( - config, - RUNTIME_SESSION_API_KEY_COOKIE_CONTEXT, - "api-key", - ) -} - -pub(crate) fn runtime_session_cookie_value_for_payload( - config: &Config, - context: &[u8], - payload: &str, -) -> Option { - let api_key = config.api_key.as_deref()?; - let mut mac = Hmac::::new_from_slice(api_key.as_bytes()).ok()?; - mac.update(context); - mac.update(b":"); - mac.update(payload.as_bytes()); - let encoded_subject = URL_SAFE_NO_PAD.encode(payload.as_bytes()); - let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()); - Some(format!("{encoded_subject}.{signature}")) -} diff --git a/packages/runtime-gateway-rs/src/http.rs b/packages/runtime-gateway-rs/src/http.rs index 668acc0eb..95cee494e 100644 --- a/packages/runtime-gateway-rs/src/http.rs +++ b/packages/runtime-gateway-rs/src/http.rs @@ -186,53 +186,13 @@ pub(crate) fn response(status: u16, content_type: &str, body: &[u8]) -> Vec 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 { - 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 { - 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 { - 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 { response_with_extra_headers_and_length( status, content_type, body, "Cache-Control: no-store, no-cache, must-revalidate\r\n", - content_length, + body.len(), ) } @@ -419,3 +379,19 @@ pub(crate) fn percent_decode_component(value: &str) -> String { } String::from_utf8_lossy(&decoded).to_string() } + +pub(crate) fn mime_for_path(path: &std::path::Path) -> &'static str { + match path.extension().and_then(|extension| extension.to_str()) { + Some("html") => "text/html; charset=utf-8", + Some("js") => "application/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("json") => "application/json", + Some("svg") => "image/svg+xml", + Some("png") => "image/png", + Some("jpg" | "jpeg") => "image/jpeg", + Some("webp") => "image/webp", + Some("ico") => "image/x-icon", + Some("wasm") => "application/wasm", + _ => "application/octet-stream", + } +} diff --git a/packages/runtime-gateway-rs/src/lib.rs b/packages/runtime-gateway-rs/src/lib.rs index d7797048c..db054810f 100644 --- a/packages/runtime-gateway-rs/src/lib.rs +++ b/packages/runtime-gateway-rs/src/lib.rs @@ -50,7 +50,6 @@ mod local; mod markitdown; mod migrations; mod model_catalog; -mod runtime_assets; mod session_messaging; mod sessions; @@ -104,14 +103,12 @@ pub(crate) use http::MAX_JSON_BODY_BYTES; #[cfg(test)] use http::parse_request_head; use http::{ - RequestHead, header_end, json_response, origin_allowed, percent_decode_component, query_flag, - read_request_body, read_request_body_with_limit, read_request_head, requested_cors_origin, - response, response_cors_credentials_header, response_cors_origin, response_with_extra_headers, - response_with_extra_headers_and_length, response_with_no_store, text_response, - with_response_cors_origin, + RequestHead, header_end, json_response, mime_for_path, origin_allowed, + percent_decode_component, query_flag, read_request_body, read_request_body_with_limit, + read_request_head, requested_cors_origin, response, response_cors_credentials_header, + response_cors_origin, response_with_extra_headers, response_with_extra_headers_and_length, + response_with_no_store, text_response, with_response_cors_origin, }; -#[cfg(test)] -use http::{response_with_cache_and_length, response_with_no_store_and_length}; use local::*; use markitdown::{extract_with_markitdown, should_prefer_markitdown, should_try_markitdown}; pub(crate) use model_catalog::{ModelInfo, available_models, default_model, resolve_model}; @@ -120,7 +117,6 @@ use model_catalog::{ ModelRegistry, builtin_models, default_model_from_registry, emergency_default_model, merge_configured_models, merge_llm_gateway_model_catalog, }; -use runtime_assets::*; #[cfg(test)] pub(crate) use session_messaging::{ LIST_SESSION_PEERS_TOOL, SEND_SESSION_MESSAGE_TOOL, SESSION_MESSAGE_INBOX_CAP, SessionMessage, @@ -296,8 +292,6 @@ pub struct RuntimeGatewayConfig { usage_file_path: PathBuf, a2a_tasks_file_path: PathBuf, automation_file_path: PathBuf, - static_root: PathBuf, - static_cache_max_age: u64, llm_gateway_models_url: Option, llm_gateway_token: Option, llm_gateway_org_id: Option, @@ -353,13 +347,6 @@ impl RuntimeGatewayConfig { usage_file_path: usage_file_path(), a2a_tasks_file_path: a2a_tasks_file_path(), automation_file_path: automation_file_path(), - static_root: env::var("MAESTRO_WEB_STATIC_ROOT") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("packages/web/dist")), - static_cache_max_age: env::var("MAESTRO_STATIC_MAX_AGE") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(86_400), llm_gateway_models_url, llm_gateway_token: if openrouter_models { trimmed_env("MAESTRO_OPENROUTER_API_KEY") @@ -406,8 +393,6 @@ impl RuntimeGatewayConfig { usage_file_path: state_root.join("usage.jsonl"), a2a_tasks_file_path: state_root.join("a2a-tasks.json"), automation_file_path: state_root.join("automations.json"), - static_root: PathBuf::from("packages/web/dist"), - static_cache_max_age: 0, llm_gateway_models_url: None, llm_gateway_token: None, llm_gateway_org_id: None, @@ -415,11 +400,6 @@ impl RuntimeGatewayConfig { } } - pub fn with_static_root(mut self, static_root: PathBuf) -> Self { - self.static_root = static_root; - self - } - pub fn with_session_store_path(mut self, session_store_path: PathBuf) -> Self { self.session_store_path = session_store_path; self @@ -601,8 +581,6 @@ struct OnboardingStep { struct ServerSnapshot { uptime: f64, version: String, - #[serde(rename = "staticCacheMaxAgeSeconds")] - static_cache_max_age_seconds: u64, runtime: &'static str, } @@ -825,26 +803,6 @@ async fn handle_connection(mut stream: TcpStream, state: AppState) -> anyhow::Re return Ok(()); } - if is_runtime_config_request(&head) { - let response = runtime_config_response(&head, &state.config); - stream - .write_all(&response) - .await - .context("failed to write runtime config response")?; - let _ = stream.shutdown().await; - return Ok(()); - } - - if is_static_asset_request(&head) { - let response = static_response(&head, &state.config).await; - stream - .write_all(&response) - .await - .context("failed to write static asset response")?; - let _ = stream.shutdown().await; - return Ok(()); - } - let response = json_response( 501, &serde_json::json!({ @@ -1467,7 +1425,6 @@ async fn build_status_snapshot(state: &AppState) -> StatusSnapshot { server: ServerSnapshot { uptime: state.started_at.elapsed().as_secs_f64(), version: env!("CARGO_PKG_VERSION").to_string(), - static_cache_max_age_seconds: state.config.static_cache_max_age, runtime: "rust-control-plane", }, database: DatabaseSnapshot { diff --git a/packages/runtime-gateway-rs/src/runtime_assets.rs b/packages/runtime-gateway-rs/src/runtime_assets.rs deleted file mode 100644 index 96baeb428..000000000 --- a/packages/runtime-gateway-rs/src/runtime_assets.rs +++ /dev/null @@ -1,320 +0,0 @@ -use std::path::{Component, Path, PathBuf}; - -use crate::Config; -use crate::auth::{ - RUNTIME_SESSION_COOKIE_NAME, header_auth_matches, runtime_session_api_key_cookie_value, - runtime_session_cookie_value_with_identity, trusted_proxy_auth_context, -}; -use crate::http::{ - RequestHead, json_response, response_with_cache, response_with_cache_and_length, - response_with_extra_headers_and_length, response_with_no_store, - response_with_no_store_and_length, -}; - -pub(crate) const RUNTIME_CONFIG_SCRIPT_PATH: &str = "/__maestro/runtime-config.js"; -pub(crate) const RUNTIME_CONFIG_SCRIPT_TAG: &str = - r#" "#; - -pub(crate) enum StaticPathResolution { - Found(PathBuf), - Missing, - Forbidden, -} - -pub(crate) fn is_static_asset_request(head: &RequestHead) -> bool { - matches!(head.method.as_str(), "GET" | "HEAD") && !head.path.starts_with("/api/") -} - -pub(crate) fn is_runtime_config_request(head: &RequestHead) -> bool { - matches!(head.method.as_str(), "GET" | "HEAD") && head.path == RUNTIME_CONFIG_SCRIPT_PATH -} - -pub(crate) fn runtime_config_response(head: &RequestHead, config: &Config) -> Vec { - let body = runtime_config_script(config); - if head.method == "HEAD" { - response_with_no_store_and_length( - 200, - "application/javascript; charset=utf-8", - &[], - body.len(), - ) - } else { - response_with_no_store(200, "application/javascript; charset=utf-8", &body) - } -} - -pub(crate) fn runtime_config_script(config: &Config) -> Vec { - let csrf_token = - serde_json::to_string(&config.csrf_token).unwrap_or_else(|_| "null".to_string()); - format!("delete window.__MAESTRO_API_KEY__;\nwindow.__MAESTRO_CSRF_TOKEN__ = {csrf_token};\n") - .into_bytes() -} - -pub(crate) fn should_inject_runtime_config(config: &Config) -> bool { - config.api_key.is_some() || config.csrf_token.is_some() -} - -pub(crate) fn spa_entry_body(bytes: &[u8], config: &Config) -> Vec { - if !should_inject_runtime_config(config) { - return bytes.to_vec(); - } - let Ok(html) = std::str::from_utf8(bytes) else { - return bytes.to_vec(); - }; - if html.contains(RUNTIME_CONFIG_SCRIPT_PATH) { - return bytes.to_vec(); - } - let Some(head_end) = html.find("") else { - return bytes.to_vec(); - }; - let mut updated = String::with_capacity(html.len() + RUNTIME_CONFIG_SCRIPT_TAG.len() + 1); - updated.push_str(&html[..head_end]); - updated.push_str(RUNTIME_CONFIG_SCRIPT_TAG); - updated.push('\n'); - updated.push_str(&html[head_end..]); - updated.into_bytes() -} - -pub(crate) fn request_is_secure(head: &RequestHead) -> bool { - let forwarded_proto = head - .headers - .get("x-forwarded-proto") - .and_then(|value| value.split(',').next()) - .map(str::trim) - .is_some_and(|value| value.eq_ignore_ascii_case("https")); - let forwarded_scheme = head - .headers - .get("x-forwarded-scheme") - .map(String::as_str) - .is_some_and(|value| value.eq_ignore_ascii_case("https")); - let forwarded_ssl = head - .headers - .get("x-forwarded-ssl") - .map(String::as_str) - .is_some_and(|value| value.eq_ignore_ascii_case("on")); - forwarded_proto || forwarded_scheme || forwarded_ssl -} - -pub(crate) fn spa_entry_session_cookie_value( - head: &RequestHead, - config: &Config, -) -> Option { - let bearer = head - .headers - .get("authorization") - .and_then(|value| value.strip_prefix("Bearer ")) - .map(str::trim); - let header_key = head - .headers - .get("x-maestro-api-key") - .or_else(|| head.headers.get("x-composer-api-key")) - .map(String::as_str); - if header_auth_matches(config, bearer, header_key) { - return runtime_session_api_key_cookie_value(config); - } - if let Some(auth) = trusted_proxy_auth_context(head) { - let subject = auth.subject.as_deref()?; - return runtime_session_cookie_value_with_identity( - config, - subject, - auth.organization_id.as_deref(), - auth.workspace_id.as_deref(), - &auth.scopes, - ); - } - None -} - -pub(crate) fn spa_entry_extra_headers(head: &RequestHead, config: &Config) -> String { - let mut headers = "Cache-Control: no-store, no-cache, must-revalidate\r\n".to_string(); - if let Some(cookie_value) = spa_entry_session_cookie_value(head, config) { - let secure = if request_is_secure(head) { - "; Secure" - } else { - "" - }; - headers.push_str(&format!( - "Set-Cookie: {RUNTIME_SESSION_COOKIE_NAME}={cookie_value}; Path=/; HttpOnly; SameSite=Lax{secure}\r\n" - )); - } - headers -} - -pub(crate) fn spa_entry_response( - head: &RequestHead, - mime: &str, - body: &[u8], - config: &Config, -) -> Vec { - let extra_headers = spa_entry_extra_headers(head, config); - if head.method == "HEAD" { - response_with_extra_headers_and_length(200, mime, &[], &extra_headers, body.len()) - } else { - response_with_extra_headers_and_length(200, mime, body, &extra_headers, body.len()) - } -} - -pub(crate) async fn static_response(head: &RequestHead, config: &Config) -> Vec { - let Some(path) = resolve_static_path(&config.static_root, &head.path) else { - return json_response(403, &serde_json::json!({ "error": "Forbidden" })); - }; - - match canonical_static_path(&config.static_root, &path).await { - StaticPathResolution::Found(path) => match tokio::fs::read(&path).await { - Ok(bytes) => { - if is_spa_entry_path(&path) { - let body = spa_entry_body(&bytes, config); - spa_entry_response(head, mime_for_path(&path), &body, config) - } else if head.method == "HEAD" { - response_with_cache_and_length( - 200, - mime_for_path(&path), - &[], - config.static_cache_max_age, - bytes.len(), - ) - } else { - response_with_cache( - 200, - mime_for_path(&path), - &bytes, - config.static_cache_max_age, - ) - } - } - Err(_) => json_response( - 404, - &serde_json::json!({ - "error": "Not found", - "staticRoot": config.static_root - }), - ), - }, - StaticPathResolution::Forbidden => { - json_response(403, &serde_json::json!({ "error": "Forbidden" })) - } - StaticPathResolution::Missing => { - if !should_spa_fallback(head) { - return json_response( - 404, - &serde_json::json!({ - "error": "Not found", - "staticRoot": config.static_root - }), - ); - } - let index = config.static_root.join("index.html"); - match canonical_static_path(&config.static_root, &index).await { - StaticPathResolution::Found(index) => match tokio::fs::read(&index).await { - Ok(bytes) => { - let body = spa_entry_body(&bytes, config); - spa_entry_response(head, "text/html; charset=utf-8", &body, config) - } - Err(_) => json_response( - 404, - &serde_json::json!({ - "error": "Not found", - "staticRoot": config.static_root - }), - ), - }, - StaticPathResolution::Forbidden => { - json_response(403, &serde_json::json!({ "error": "Forbidden" })) - } - StaticPathResolution::Missing => json_response( - 404, - &serde_json::json!({ - "error": "Not found", - "staticRoot": config.static_root - }), - ), - } - } - } -} - -pub(crate) fn should_spa_fallback(head: &RequestHead) -> bool { - let trimmed = head.path.trim_end_matches('/'); - let last_segment = trimmed.rsplit('/').next().unwrap_or_default(); - !last_segment.contains('.') -} - -pub(crate) async fn canonical_static_path(root: &Path, path: &Path) -> StaticPathResolution { - let Ok(canonical_root) = tokio::fs::canonicalize(root).await else { - return StaticPathResolution::Missing; - }; - match tokio::fs::canonicalize(path).await { - Ok(canonical_path) if canonical_path.starts_with(&canonical_root) => { - StaticPathResolution::Found(canonical_path) - } - Ok(_) => StaticPathResolution::Forbidden, - Err(_) => StaticPathResolution::Missing, - } -} - -pub(crate) fn resolve_static_path(root: &Path, request_path: &str) -> Option { - let trimmed = request_path.trim_start_matches('/'); - let mut relative = PathBuf::new(); - if trimmed.is_empty() { - relative.push("index.html"); - } else { - for component in Path::new(trimmed).components() { - match component { - Component::Normal(segment) => { - if segment.to_string_lossy().contains('\\') { - return None; - } - relative.push(segment); - } - _ => return None, - } - } - } - - let candidate = root.join(&relative); - let Some(canonical_root) = dunce::canonicalize(root).ok() else { - return Some(candidate); - }; - let existing = existing_static_ancestor(&candidate, root)?; - if !dunce::canonicalize(existing) - .ok()? - .starts_with(&canonical_root) - { - return None; - } - Some(candidate) -} - -pub(crate) fn existing_static_ancestor<'a>(mut path: &'a Path, root: &'a Path) -> Option<&'a Path> { - loop { - if path.exists() { - return Some(path); - } - if path == root { - return None; - } - path = path.parent()?; - } -} - -pub(crate) fn is_spa_entry_path(path: &Path) -> bool { - path.file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| name.eq_ignore_ascii_case("index.html")) -} - -pub(crate) fn mime_for_path(path: &Path) -> &'static str { - match path.extension().and_then(|extension| extension.to_str()) { - Some("html") => "text/html; charset=utf-8", - Some("js") => "application/javascript; charset=utf-8", - Some("css") => "text/css; charset=utf-8", - Some("json") => "application/json", - Some("svg") => "image/svg+xml", - Some("png") => "image/png", - Some("jpg" | "jpeg") => "image/jpeg", - Some("webp") => "image/webp", - Some("ico") => "image/x-icon", - Some("wasm") => "application/wasm", - _ => "application/octet-stream", - } -} diff --git a/packages/runtime-gateway-rs/src/tests.rs b/packages/runtime-gateway-rs/src/tests.rs index 312087047..fe18a99fc 100644 --- a/packages/runtime-gateway-rs/src/tests.rs +++ b/packages/runtime-gateway-rs/src/tests.rs @@ -2587,8 +2587,6 @@ fn auth_test_config() -> Config { usage_file_path: PathBuf::from("usage.jsonl"), a2a_tasks_file_path: unique_test_dir("maestro-a2a-tasks").join("tasks.json"), automation_file_path: unique_test_dir("maestro-automations").join("automations.json"), - static_root: PathBuf::from("dist"), - static_cache_max_age: 0, llm_gateway_models_url: None, llm_gateway_token: None, llm_gateway_org_id: None, @@ -12241,8 +12239,13 @@ fn default_model_handles_empty_registry() { fn head_response_keeps_get_content_length_without_body() { let _guard = ENV_LOCK.blocking_lock(); - let response = - response_with_cache_and_length(200, "text/plain; charset=utf-8", &[], 60, "hello".len()); + let response = response_with_extra_headers_and_length( + 200, + "text/plain; charset=utf-8", + &[], + "Cache-Control: no-store, no-cache, must-revalidate\r\n", + "hello".len(), + ); let response = String::from_utf8(response).expect("response should be utf-8"); assert!(response.contains("Content-Length: 5\r\n")); @@ -12450,156 +12453,6 @@ fn chat_prompt_preserves_structured_history() { assert!(prompt.contains("\"attachments\"")); } -#[test] -fn spa_entry_response_uses_no_store() { - let _guard = ENV_LOCK.blocking_lock(); - - let response = - response_with_no_store_and_length(200, "text/html; charset=utf-8", &[], "index".len()); - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(response.contains("Content-Length: 5\r\n")); - assert!(response.contains("Cache-Control: no-store, no-cache, must-revalidate\r\n")); -} - -#[test] -fn runtime_config_script_serializes_csrf_without_api_key() { - let mut config = auth_test_config(); - config.csrf_token = Some("csrf\"token".to_string()); - - let script = String::from_utf8(runtime_config_script(&config)).expect("script should be utf-8"); - - assert!(!script.contains("api-key")); - assert!(script.contains("delete window.__MAESTRO_API_KEY__;")); - assert!(script.contains("window.__MAESTRO_CSRF_TOKEN__ = \"csrf\\\"token\";")); -} - -#[test] -fn runtime_config_head_reports_script_length_without_body() { - let mut config = auth_test_config(); - config.csrf_token = Some("csrf-token".to_string()); - let head = RequestHead { - method: "HEAD".to_string(), - path: RUNTIME_CONFIG_SCRIPT_PATH.to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - let expected_length = runtime_config_script(&config).len(); - - let response = runtime_config_response(&head, &config); - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(response.contains("Content-Type: application/javascript; charset=utf-8\r\n")); - assert!(response.contains(&format!("Content-Length: {expected_length}\r\n"))); - assert!(response.ends_with("\r\n\r\n")); -} - -#[tokio::test] -async fn spa_entry_injects_runtime_config_script_when_browser_auth_configured() { - let root = TestDir::new("runtime-config-spa"); - let index = "Maestrook"; - fs::write(root.path().join("index.html"), index).expect("index should be written"); - let mut config = auth_test_config(); - config.static_root = root.path().to_path_buf(); - config.csrf_token = Some("csrf-token".to_string()); - let head = RequestHead { - method: "GET".to_string(), - path: "/".to_string(), - query: HashMap::new(), - headers: HashMap::from([("x-maestro-api-key".to_string(), "api-key".to_string())]), - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - let body = response_body_text(&response); - - assert!(body.contains(RUNTIME_CONFIG_SCRIPT_TAG)); - assert!(body.contains("ok")); - assert!(!body.contains("window.__MAESTRO_API_KEY__ =")); - assert!(response.contains(&format!( - "Set-Cookie: {RUNTIME_SESSION_COOKIE_NAME}={}; Path=/; HttpOnly; SameSite=Lax\r\n", - runtime_session_api_key_cookie_value(&config).expect("cookie should be available") - ))); - assert!(response.contains(&format!("Content-Length: {}\r\n", body.len()))); -} - -#[test] -fn runtime_session_cookie_authorizes_same_origin_browser_requests() { - let config = auth_test_config(); - let cookie = runtime_session_cookie_value(&config, "jonathan@evalops.dev") - .expect("cookie should be available"); - let head = RequestHead { - method: "GET".to_string(), - path: "/api/status".to_string(), - query: HashMap::new(), - headers: HashMap::from([( - "cookie".to_string(), - format!("theme=dark; {RUNTIME_SESSION_COOKIE_NAME}={cookie}; other=value"), - )]), - }; - - let context = auth_context(&head, &config).expect("cookie should authorize request"); - - assert!(!context.unrestricted); - assert_eq!(context.subject.as_deref(), Some("jonathan@evalops.dev")); -} - -#[test] -fn bearer_token_identity_wins_over_runtime_session_cookie() { - let _guard = ENV_LOCK.blocking_lock(); - let previous = env::var_os("MAESTRO_JWT_SECRET"); - env::set_var("MAESTRO_JWT_SECRET", "shared-secret"); - let config = auth_test_config(); - let cookie = - runtime_session_cookie_value(&config, "cookie-user").expect("cookie should be available"); - let bearer_user = "bearer-user"; - let token = identity_jwt_bearer_token(b"shared-secret", bearer_user); - let head = RequestHead { - method: "GET".to_string(), - path: "/api/status".to_string(), - query: HashMap::new(), - headers: HashMap::from([ - ("authorization".to_string(), format!("Bearer {token}")), - ( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={cookie}"), - ), - ]), - }; - - let context = auth_context(&head, &config).expect("bearer token should authorize"); - - assert_eq!(context.subject.as_deref(), Some(bearer_user)); - assert!(!context.unrestricted); - - if let Some(previous) = previous { - env::set_var("MAESTRO_JWT_SECRET", previous); - } else { - env::remove_var("MAESTRO_JWT_SECRET"); - } -} - -#[test] -fn loopback_api_key_runtime_session_cookie_keeps_legacy_unrestricted_access() { - let _guard = ENV_LOCK.blocking_lock(); - let config = auth_test_config(); - let cookie = runtime_session_api_key_cookie_value(&config).expect("cookie should be available"); - let head = RequestHead { - method: "GET".to_string(), - path: "/api/sessions".to_string(), - query: HashMap::new(), - headers: HashMap::from([( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={cookie}"), - )]), - }; - - let context = auth_context(&head, &config).expect("api-key cookie should authorize"); - - assert!(context.subject.is_none()); - assert!(context.unrestricted); -} - #[test] fn loopback_static_key_remains_unrestricted_for_mutations() { let _guard = ENV_LOCK.blocking_lock(); @@ -12637,47 +12490,12 @@ fn hosted_loopback_static_key_requires_write_scope_for_mutations() { }; assert!(authorize(&head, &config).is_err()); - let cookie = runtime_session_api_key_cookie_value(&config).expect("cookie should be available"); - let cookie_head = RequestHead { - method: head.method.clone(), - path: head.path.clone(), - query: head.query.clone(), - headers: HashMap::from([( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={cookie}"), - )]), - }; - assert!(authorize(&cookie_head, &config).is_err()); - env::set_var("MAESTRO_WEB_API_KEY_SCOPES", "maestro:write"); assert!(authorize(&head, &config).is_ok()); - assert!(authorize(&cookie_head, &config).is_ok()); restore_env(snapshot); } -#[test] -fn scoped_runtime_session_cookie_for_api_key_sentinel_subject_stays_scoped() { - let config = auth_test_config(); - let subject = "api-key:unrestricted"; - let cookie = - runtime_session_cookie_value(&config, subject).expect("cookie should be available"); - let head = RequestHead { - method: "GET".to_string(), - path: "/api/sessions".to_string(), - query: HashMap::new(), - headers: HashMap::from([( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={cookie}"), - )]), - }; - - let context = auth_context(&head, &config).expect("scoped cookie should authorize"); - - assert_eq!(context.subject.as_deref(), Some(subject)); - assert!(!context.unrestricted); -} - #[test] fn trusted_proxy_auth_requires_shared_proxy_token() { let _guard = ENV_LOCK.blocking_lock(); @@ -12725,28 +12543,10 @@ fn trusted_proxy_auth_requires_shared_proxy_token() { assert_eq!(context.workspace_id.as_deref(), Some("workspace-proxy")); assert_eq!(context.scopes, vec!["maestro:write"]); assert!(!context.unrestricted); - let cookie = spa_entry_session_cookie_value(&trusted, &config) - .expect("trusted proxy session cookie should be available"); let mut mutation = trusted; mutation.method = "POST".to_string(); mutation.path = "/api/config".to_string(); assert!(authorize(&mutation, &config).is_ok()); - let cookie_mutation = RequestHead { - method: "POST".to_string(), - path: "/api/config".to_string(), - query: HashMap::new(), - headers: HashMap::from([( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={cookie}"), - )]), - }; - assert!(authorize(&cookie_mutation, &config).is_ok()); - let cookie_context = auth_context(&cookie_mutation, &config).expect("cookie auth"); - assert_eq!(cookie_context.organization_id.as_deref(), Some("org-proxy")); - assert_eq!( - cookie_context.workspace_id.as_deref(), - Some("workspace-proxy") - ); if let Some(previous) = previous { env::set_var("MAESTRO_WEB_TRUST_PROXY_AUTH_TOKEN", previous); @@ -12780,7 +12580,7 @@ fn trusted_proxy_token_counts_as_configured_auth() { } #[test] -fn web_auth_mode_matrix_pins_control_plane_access() { +fn auth_mode_matrix_pins_control_plane_access() { let _guard = ENV_LOCK.blocking_lock(); let preserved_env: Vec<_> = [ "MAESTRO_WEB_TRUST_PROXY_AUTH_TOKEN", @@ -12799,11 +12599,6 @@ fn web_auth_mode_matrix_pins_control_plane_access() { let mut open_dev_config = auth_test_config(); open_dev_config.api_key = None; open_dev_config.require_key = false; - let scoped_cookie = runtime_session_cookie_value(&api_key_config, "web-user") - .expect("scoped cookie should be available"); - let api_key_cookie = runtime_session_api_key_cookie_value(&api_key_config) - .expect("api-key cookie should be available"); - struct AuthMatrixCase { name: &'static str, config: Config, @@ -12841,26 +12636,6 @@ fn web_auth_mode_matrix_pins_control_plane_access() { expected_subject: None, expected_unrestricted: Some(true), }, - AuthMatrixCase { - name: "scoped runtime session cookie stays subject-scoped", - config: api_key_config.clone(), - headers: HashMap::from([( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={scoped_cookie}"), - )]), - expected_subject: Some("web-user"), - expected_unrestricted: Some(false), - }, - AuthMatrixCase { - name: "legacy api-key runtime session cookie stays unrestricted", - config: api_key_config.clone(), - headers: HashMap::from([( - "cookie".to_string(), - format!("{RUNTIME_SESSION_COOKIE_NAME}={api_key_cookie}"), - )]), - expected_subject: None, - expected_unrestricted: Some(true), - }, ]; for case in cases { @@ -12940,140 +12715,6 @@ fn web_auth_mode_matrix_pins_control_plane_access() { } } -#[tokio::test] -async fn loopback_api_key_web_first_load_requires_authenticated_cookie_issuer() { - let root = TestDir::new("runtime-config-spa-loopback-api-key"); - fs::write(root.path().join("index.html"), "") - .expect("index should be written"); - let mut config = auth_test_config(); - config.static_root = root.path().to_path_buf(); - config.listen_host = "127.0.0.1".to_string(); - let head = RequestHead { - method: "GET".to_string(), - path: "/".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(!response.contains("Set-Cookie: maestro_web_session=")); -} - -#[tokio::test] -async fn spa_entry_does_not_mint_session_cookie_without_authenticated_issuer() { - let root = TestDir::new("runtime-config-spa-unauth"); - fs::write(root.path().join("index.html"), "") - .expect("index should be written"); - let mut config = auth_test_config(); - config.static_root = root.path().to_path_buf(); - config.listen_host = "0.0.0.0".to_string(); - let head = RequestHead { - method: "GET".to_string(), - path: "/".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(!response.contains("Set-Cookie: maestro_web_session=")); -} - -#[tokio::test] -async fn spa_entry_head_uses_injected_body_length() { - let root = TestDir::new("runtime-config-spa-head"); - let index = "ok"; - fs::write(root.path().join("index.html"), index).expect("index should be written"); - let mut config = auth_test_config(); - config.static_root = root.path().to_path_buf(); - let expected_length = spa_entry_body(index.as_bytes(), &config).len(); - let head = RequestHead { - method: "HEAD".to_string(), - path: "/".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(response.contains(&format!("Content-Length: {expected_length}\r\n"))); - assert!(response.ends_with("\r\n\r\n")); -} - -#[tokio::test] -async fn spa_entry_without_browser_auth_does_not_inject_runtime_config() { - let root = TestDir::new("runtime-config-spa-disabled"); - let index = "ok"; - fs::write(root.path().join("index.html"), index).expect("index should be written"); - let mut config = auth_test_config(); - config.static_root = root.path().to_path_buf(); - config.api_key = None; - config.csrf_token = None; - let head = RequestHead { - method: "GET".to_string(), - path: "/".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(!response.contains(RUNTIME_CONFIG_SCRIPT_PATH)); - assert!(response.contains(index)); -} - -#[cfg(unix)] -#[tokio::test] -async fn canonical_static_path_rejects_symlink_escape() { - let base = env::temp_dir().join(format!( - "maestro-static-test-{}-{}", - process::id(), - ATTACHMENT_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed) - )); - let root = base.join("static"); - let outside = base.join("outside"); - tokio::fs::create_dir_all(&root).await.expect("create root"); - tokio::fs::create_dir_all(&outside) - .await - .expect("create outside"); - tokio::fs::write(outside.join("secret.txt"), "secret") - .await - .expect("write secret"); - std::os::unix::fs::symlink(outside.join("secret.txt"), root.join("secret.txt")) - .expect("create symlink"); - - assert!(matches!( - canonical_static_path(&root, &root.join("secret.txt")).await, - StaticPathResolution::Forbidden - )); - - let _ = tokio::fs::remove_dir_all(base).await; -} - -#[test] -fn missing_asset_paths_do_not_spa_fallback() { - let asset = RequestHead { - method: "GET".to_string(), - path: "/assets/app.js".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - let route = RequestHead { - method: "GET".to_string(), - path: "/settings".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - - assert!(!should_spa_fallback(&asset)); - assert!(should_spa_fallback(&route)); -} - #[test] fn prepared_attachments_drop_removes_temp_dir() { let dir = env::temp_dir().join(format!( @@ -13143,36 +12784,12 @@ fn telemetry_explicit_false_overrides_endpoint_and_file_configuration() { } } -#[test] -fn resolves_missing_static_paths_within_root() { - let root = TestDir::new("static-root"); - fs::write(root.path().join("index.html"), "").expect("index should be written"); - - let resolved = - resolve_static_path(root.path(), "/assets/app.js").expect("path should stay in root"); - - assert_eq!(resolved, root.path().join("assets/app.js")); -} - -#[cfg(unix)] -#[test] -fn rejects_static_paths_that_escape_through_symlinks() { - let root = TestDir::new("static-root"); - let outside = TestDir::new("outside-root"); - let escape = root.path().join("escape"); - fs::write(root.path().join("index.html"), "").expect("index should be written"); - fs::write(outside.path().join("secret.txt"), "secret").expect("secret should be written"); - std::os::unix::fs::symlink(outside.path(), &escape).expect("symlink should be created"); - - assert!(resolve_static_path(root.path(), "/escape/secret.txt").is_none()); -} - #[test] fn validates_run_script_inputs() { assert!(is_valid_script_name("build:all")); assert!(!is_valid_script_name("build && rm -rf /")); assert!(contains_shell_metachars("foo; bar")); - assert!(!contains_shell_metachars("--filter packages/web")); + assert!(!contains_shell_metachars("--filter packages/tui")); assert_eq!( runner_args_for_script("npm", "db:migrate"), ["--ignore-scripts", "run", "db:migrate"] @@ -13545,96 +13162,6 @@ async fn prepared_attachments_use_workspace_for_docker_sandbox() { ); } -#[tokio::test] -async fn missing_static_asset_returns_404_instead_of_index() { - let static_root = unique_test_dir("maestro-static-asset"); - fs::create_dir_all(&static_root).expect("static root should exist"); - fs::write(static_root.join("index.html"), "ok").expect("index should exist"); - - let head = RequestHead { - method: "GET".to_string(), - path: "/assets/app.js".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - let config = Config { - listen_host: "127.0.0.1".to_string(), - listen_port: 8080, - api_key: None, - allowed_hosts: Vec::new(), - require_key: false, - require_key_explicitly_disabled: false, - csrf_token: None, - require_csrf: false, - cwd: PathBuf::from("."), - session_store_path: static_root.join("sessions.json"), - session_messages_path: static_root.join("session-messages.json"), - command_prefs_path: static_root.join("command-prefs.json"), - usage_file_path: static_root.join("usage.json"), - a2a_tasks_file_path: static_root.join("a2a-tasks.json"), - automation_file_path: static_root.join("automations.json"), - static_root: static_root.clone(), - static_cache_max_age: 60, - llm_gateway_models_url: None, - llm_gateway_token: None, - llm_gateway_org_id: None, - llm_gateway_timeout_ms: 2_500, - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(response.starts_with("HTTP/1.1 404 Not Found\r\n")); - assert!(!response.contains("ok")); - - let _ = fs::remove_dir_all(static_root); -} - -#[tokio::test] -async fn missing_spa_route_falls_back_to_index() { - let static_root = unique_test_dir("maestro-static-spa"); - fs::create_dir_all(&static_root).expect("static root should exist"); - fs::write(static_root.join("index.html"), "ok").expect("index should exist"); - - let head = RequestHead { - method: "GET".to_string(), - path: "/chat/session".to_string(), - query: HashMap::new(), - headers: HashMap::new(), - }; - let config = Config { - listen_host: "127.0.0.1".to_string(), - listen_port: 8080, - api_key: None, - allowed_hosts: Vec::new(), - require_key: false, - require_key_explicitly_disabled: false, - csrf_token: None, - require_csrf: false, - cwd: PathBuf::from("."), - session_store_path: static_root.join("sessions.json"), - session_messages_path: static_root.join("session-messages.json"), - command_prefs_path: static_root.join("command-prefs.json"), - usage_file_path: static_root.join("usage.json"), - a2a_tasks_file_path: static_root.join("a2a-tasks.json"), - automation_file_path: static_root.join("automations.json"), - static_root: static_root.clone(), - static_cache_max_age: 60, - llm_gateway_models_url: None, - llm_gateway_token: None, - llm_gateway_org_id: None, - llm_gateway_timeout_ms: 2_500, - }; - - let response = static_response(&head, &config).await; - let response = String::from_utf8(response).expect("response should be utf-8"); - - assert!(response.starts_with("HTTP/1.1 200 OK\r\n")); - assert!(response.contains("ok")); - - let _ = fs::remove_dir_all(static_root); -} - #[tokio::test] async fn delete_session_subpath_returns_404_without_removing_session() { let root = TestDir::new("session-delete-subpath"); @@ -13670,8 +13197,6 @@ async fn delete_session_subpath_returns_404_without_removing_session() { usage_file_path: root.path().join("usage.json"), a2a_tasks_file_path: root.path().join("a2a-tasks.json"), automation_file_path: root.path().join("automations.json"), - static_root: root.path().to_path_buf(), - static_cache_max_age: 60, llm_gateway_models_url: None, llm_gateway_token: None, llm_gateway_org_id: None, @@ -13763,8 +13288,6 @@ async fn invalid_session_store_is_left_untouched_and_future_writes_are_blocked() usage_file_path: root.path().join("usage.json"), a2a_tasks_file_path: root.path().join("a2a-tasks.json"), automation_file_path: root.path().join("automations.json"), - static_root: root.path().to_path_buf(), - static_cache_max_age: 60, llm_gateway_models_url: None, llm_gateway_token: None, llm_gateway_org_id: None, diff --git a/packages/runtime-gateway-rs/tests/library_entrypoint.rs b/packages/runtime-gateway-rs/tests/library_entrypoint.rs index 6b82e72cf..117861f3e 100644 --- a/packages/runtime-gateway-rs/tests/library_entrypoint.rs +++ b/packages/runtime-gateway-rs/tests/library_entrypoint.rs @@ -1,5 +1,4 @@ use maestro_runtime_gateway::{RuntimeGatewayConfig, serve_listener}; -use std::time::{SystemTime, UNIX_EPOCH}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; @@ -21,28 +20,14 @@ async fn get(addr: std::net::SocketAddr, path: &str) -> String { } #[tokio::test] -async fn library_server_serves_health_and_static_assets() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock") - .as_nanos(); - let root = std::env::temp_dir().join(format!("maestro-runtime-gateway-library-{unique}")); - tokio::fs::create_dir_all(&root).await.expect("create root"); - tokio::fs::write(root.join("index.html"), "
native
") - .await - .expect("write index"); - +async fn library_server_serves_health() { let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let addr = listener.local_addr().expect("local address"); - let config = RuntimeGatewayConfig::test_default().with_static_root(root.clone()); + let config = RuntimeGatewayConfig::test_default(); let server = tokio::spawn(serve_listener(listener, config)); let health = get(addr, "/healthz").await; assert!(health.starts_with("HTTP/1.1 200"), "{health}"); - let index = get(addr, "/").await; - assert!(index.starts_with("HTTP/1.1 200"), "{index}"); - assert!(index.contains("
native
"), "{index}"); server.abort(); - let _ = tokio::fs::remove_dir_all(root).await; } diff --git a/packages/tui-rs/docs/user-guide/01-getting-started.md b/packages/tui-rs/docs/user-guide/01-getting-started.md index 1a532e4d1..e4c2e5bf6 100644 --- a/packages/tui-rs/docs/user-guide/01-getting-started.md +++ b/packages/tui-rs/docs/user-guide/01-getting-started.md @@ -62,7 +62,7 @@ Other common entrypoints: ```bash deixic-code "…" # interactive TUI with trailing prompt -deixic-code web # browser UI on http://localhost:8080 +deixic-code serve --port 8080 # HTTP runtime gateway on http://localhost:8080 deixic-code --resume # interactive session picker deixic-code --continue # resume most recent session for this cwd ``` diff --git a/packages/tui-rs/src/mode_cli.rs b/packages/tui-rs/src/mode_cli.rs index 27d6abc50..b49717d63 100644 --- a/packages/tui-rs/src/mode_cli.rs +++ b/packages/tui-rs/src/mode_cli.rs @@ -414,6 +414,30 @@ fn provider_name(provider: ModelProvider) -> &'static str { } } +fn provider_family(provider: ModelProvider) -> &'static str { + match provider { + ModelProvider::Anthropic => "anthropic", + ModelProvider::OpenAi | ModelProvider::OpenAiCodex => "openai", + ModelProvider::Google => "google", + } +} + +/// The read-only oracle and cross-check reviewers must come from a different +/// provider family than the primary model, so a second opinion never shares +/// the primary's blind spots. Keep the level's preferred provider when it +/// already differs; otherwise switch to the other major family. +fn cross_provider(preferred: ModelProvider, primary: ModelProvider) -> ModelProvider { + if provider_family(preferred) != provider_family(primary) { + return preferred; + } + match primary { + ModelProvider::Anthropic => ModelProvider::OpenAiCodex, + ModelProvider::OpenAi | ModelProvider::OpenAiCodex | ModelProvider::Google => { + ModelProvider::Anthropic + } + } +} + fn effort_name(effort: ReasoningEffort) -> &'static str { match effort { ReasoningEffort::Low => "low", @@ -585,6 +609,7 @@ fn profile_json(mode: AgentMode, provider: ModelProvider) -> Option { ), AgentMode::Replay => return None, }; + let oracle_provider = cross_provider(oracle_provider, provider); let profile_config = find_mode(level)?; let specialist = |specialist_provider: ModelProvider, tier: ModelTier, effort: ReasoningEffort| { @@ -652,7 +677,7 @@ fn profile_json(mode: AgentMode, provider: ModelProvider) -> Option { specialists.insert( "reviewer".into(), specialist( - ModelProvider::Anthropic, + cross_provider(ModelProvider::Anthropic, provider), ModelTier::Opus, ReasoningEffort::High, ), @@ -674,7 +699,7 @@ fn profile_json(mode: AgentMode, provider: ModelProvider) -> Option { specialists.insert( "reviewer".into(), specialist( - ModelProvider::OpenAiCodex, + cross_provider(ModelProvider::OpenAiCodex, provider), ModelTier::Opus, ReasoningEffort::XHigh, ), @@ -866,6 +891,106 @@ mod tests { assert_eq!(value["agentProfile"]["oracle"]["readOnly"], true); } + #[test] + fn oracle_and_cross_check_reviewers_never_share_the_primary_provider_family() { + let providers = [ + ModelProvider::Anthropic, + ModelProvider::OpenAi, + ModelProvider::OpenAiCodex, + ModelProvider::Google, + ]; + let family = |name: &str| match name { + "anthropic" => "anthropic", + "openai" | "openai-codex" => "openai", + "google" => "google", + other => panic!("unknown provider {other}"), + }; + for level in ["low", "medium", "high", "ultra"] { + for provider in providers { + let value = describe_mode(find_mode(level).unwrap(), provider); + let profile = &value["agentProfile"]; + let primary = family(profile["primary"]["provider"].as_str().unwrap()); + let oracle = family(profile["oracle"]["provider"].as_str().unwrap()); + assert_ne!( + oracle, primary, + "{level} oracle shares the {primary} family for primary {provider:?}" + ); + if matches!(level, "high" | "ultra") { + let reviewer = family( + profile["specialists"]["reviewer"]["provider"] + .as_str() + .unwrap(), + ); + assert_ne!( + reviewer, primary, + "{level} reviewer shares the {primary} family for primary {provider:?}" + ); + } + } + } + } + + #[test] + fn cross_provider_keeps_a_different_preferred_family_and_switches_a_shared_one() { + use ModelProvider::{Anthropic, Google, OpenAi, OpenAiCodex}; + let all = [Anthropic, OpenAi, OpenAiCodex, Google]; + for preferred in all { + for primary in all { + let chosen = cross_provider(preferred, primary); + assert_ne!( + provider_family(chosen), + provider_family(primary), + "preferred {preferred:?} with primary {primary:?} chose {chosen:?}" + ); + if provider_family(preferred) != provider_family(primary) { + assert_eq!( + chosen, preferred, + "a preferred provider outside the primary family is kept" + ); + } + } + } + assert_eq!(cross_provider(Anthropic, Anthropic), OpenAiCodex); + assert_eq!(cross_provider(OpenAiCodex, OpenAi), Anthropic); + assert_eq!(cross_provider(OpenAi, OpenAiCodex), Anthropic); + assert_eq!(cross_provider(Google, Google), Anthropic); + } + + #[test] + fn profile_second_opinions_are_pinned_for_representative_primaries() { + let oracle = |level: &str, provider: ModelProvider| { + describe_mode(find_mode(level).unwrap(), provider)["agentProfile"]["oracle"]["provider"] + .as_str() + .unwrap() + .to_string() + }; + let reviewer = |level: &str, provider: ModelProvider| { + describe_mode(find_mode(level).unwrap(), provider)["agentProfile"]["specialists"] + ["reviewer"]["provider"] + .as_str() + .unwrap() + .to_string() + }; + // Unchanged where the level's preference already differs from the primary. + assert_eq!(oracle("high", ModelProvider::OpenAiCodex), "anthropic"); + assert_eq!(reviewer("high", ModelProvider::OpenAiCodex), "anthropic"); + assert_eq!(oracle("ultra", ModelProvider::Anthropic), "openai-codex"); + assert_eq!(reviewer("ultra", ModelProvider::Anthropic), "openai-codex"); + // Switched where the preference used to share the primary's family. + assert_eq!(oracle("medium", ModelProvider::Anthropic), "openai-codex"); + assert_eq!(oracle("high", ModelProvider::Anthropic), "openai-codex"); + assert_eq!(reviewer("high", ModelProvider::Anthropic), "openai-codex"); + assert_eq!(oracle("low", ModelProvider::OpenAi), "anthropic"); + assert_eq!(oracle("ultra", ModelProvider::OpenAiCodex), "anthropic"); + assert_eq!(reviewer("ultra", ModelProvider::OpenAiCodex), "anthropic"); + // The Oracle stays read-only and on the top tier whichever family wins. + for provider in [ModelProvider::Anthropic, ModelProvider::OpenAiCodex] { + let value = describe_mode(find_mode("high").unwrap(), provider); + assert_eq!(value["agentProfile"]["oracle"]["readOnly"], true); + assert_eq!(value["agentProfile"]["oracle"]["reasoningEffort"], "high"); + } + } + #[test] fn hidden_modes_are_not_in_default_list_source() { assert!(!find_mode("frontier").unwrap().visible); diff --git a/packages/tui-rs/src/update_cli.rs b/packages/tui-rs/src/update_cli.rs index ad1d881ab..1fff2b475 100644 --- a/packages/tui-rs/src/update_cli.rs +++ b/packages/tui-rs/src/update_cli.rs @@ -42,7 +42,6 @@ const RELEASE_METADATA_SCHEMA: &str = "evalops.maestro.release-metadata.v1"; const UPDATE_HISTORY_FILE: &str = "update-history.json"; const INSTALL_RECEIPT_FILE: &str = "install-receipt.json"; const RELEASE_METADATA_FILE: &str = "release-metadata.json"; -const WEB_ARCHIVE_FILE: &str = "maestro-web-dist.tar.gz"; static UPDATE_ATTEMPT_COUNTER: AtomicU64 = AtomicU64::new(0); #[cfg(unix)] const INSTALL_CLEANUP_GRACE: Duration = Duration::from_secs(2); @@ -166,8 +165,6 @@ struct InstallVerification { #[serde(default)] artifact_sha256: Option, #[serde(default)] - web_sha256: Option, - #[serde(default)] metadata_sha256: Option, #[serde(default)] metadata_checksum_verified: bool, @@ -1749,17 +1746,11 @@ fn is_verified_release(release_dir: &Path, version: &Version, receipt: &InstallR && verification.manifest_checksum_verified && verification.signature_verified && release_binary_path(release_dir).is_file() - && release_dir.join(WEB_ARCHIVE_FILE).is_file() && verification .artifact_sha256 .as_deref() .zip(sha256_file(&release_binary_path(release_dir)).as_deref()) .is_some_and(|(expected, actual)| expected == actual) - && verification - .web_sha256 - .as_deref() - .zip(sha256_file(&release_dir.join(WEB_ARCHIVE_FILE)).as_deref()) - .is_some_and(|(expected, actual)| expected == actual) && metadata_verified } @@ -1872,78 +1863,6 @@ fn reported_version_matches(reported: &str, expected: &str) -> bool { .any(|version| version == expected) } -fn restore_verified_web_tree(release_dir: &Path) -> Result<()> { - let archive = release_dir.join(WEB_ARCHIVE_FILE); - let temporary = tempfile::Builder::new() - .prefix(".web-restore-") - .tempdir_in(release_dir) - .with_context(|| format!("Failed to stage web restore in {}", release_dir.display()))?; - let status = Command::new("tar") - .args(["-xzf"]) - .arg(&archive) - .arg("-C") - .arg(temporary.path()) - .status() - .with_context(|| format!("Failed to run tar for {}", archive.display()))?; - if !status.success() { - bail!( - "{}", - crate::localization::cli_locale().format( - "Failed to extract verified web archive {0}", - &[(archive.display()).to_string()] - ) - ); - } - if !temporary.path().join("index.html").is_file() { - bail!( - "{}", - crate::localization::cli_locale().format( - "Verified web archive {0} has no index.html", - &[(archive.display()).to_string()] - ) - ); - } - let restored = temporary.keep(); - let web_dir = release_dir.join("web"); - let backup = release_dir.join(format!(".web-backup-{}", now_ms())); - let had_web_tree = fs::symlink_metadata(&web_dir).is_ok(); - if backup.exists() { - bail!( - "{}", - crate::localization::cli_locale().format( - "Web restore backup path already exists: {0}", - &[(backup.display()).to_string()] - ) - ); - } - if had_web_tree { - fs::rename(&web_dir, &backup).with_context(|| { - format!( - "Failed to stage the existing web tree for {}", - release_dir.display() - ) - })?; - } - if let Err(error) = fs::rename(&restored, &web_dir) { - if had_web_tree { - let _ = fs::rename(&backup, &web_dir); - } else { - let _ = fs::remove_dir_all(&restored); - } - return Err(error).with_context(|| { - format!( - "Failed to atomically restore the web tree for {}", - release_dir.display() - ) - }); - } - if had_web_tree { - fs::remove_dir_all(&backup) - .with_context(|| format!("Failed to remove the old web tree {}", backup.display()))?; - } - Ok(()) -} - fn select_rollback_release( data_dir: &Path, current: &str, @@ -1996,7 +1915,7 @@ fn launcher_contents( version: &str, ) -> Vec { format!( - "#!/usr/bin/env bash\nset -eu\nrelease_dir={}\ninstall_dir={}\ndata_dir={}\nrelease_version={}\nexport MAESTRO_WEB_STATIC_ROOT=\"${{MAESTRO_WEB_STATIC_ROOT:-$release_dir/web}}\"\nexport MAESTRO_INSTALL_METHOD=release\nexport MAESTRO_INSTALL_DIR=\"$install_dir\"\nexport MAESTRO_DATA_DIR=\"$data_dir\"\nexport MAESTRO_STARTUP_UPDATE_STATE=\"${{MAESTRO_STARTUP_UPDATE_STATE:-$data_dir/startup-update-state.json}}\"\nexport MAESTRO_VERSION=\"$release_version\"\nexec \"$release_dir/bin/maestro\" \"$@\"\n", + "#!/usr/bin/env bash\nset -eu\nrelease_dir={}\ninstall_dir={}\ndata_dir={}\nrelease_version={}\nexport MAESTRO_INSTALL_METHOD=release\nexport MAESTRO_INSTALL_DIR=\"$install_dir\"\nexport MAESTRO_DATA_DIR=\"$data_dir\"\nexport MAESTRO_STARTUP_UPDATE_STATE=\"${{MAESTRO_STARTUP_UPDATE_STATE:-$data_dir/startup-update-state.json}}\"\nexport MAESTRO_VERSION=\"$release_version\"\nexec \"$release_dir/bin/maestro\" \"$@\"\n", shell_quote_path(release_dir), shell_quote_path(install_dir), shell_quote_path(data_dir), @@ -2388,7 +2307,6 @@ pub async fn run_startup_update(raw_args: &[std::ffi::OsString]) -> Option let mut restart = Command::new(launcher(&context)); restart .args(raw_args.iter().skip(1)) - .env_remove("MAESTRO_WEB_STATIC_ROOT") .env("MAESTRO_SKIP_STARTUP_UPDATE", "1"); match restart.status() { Ok(status) => Some(status.code().unwrap_or(1)), @@ -2754,16 +2672,6 @@ async fn run_rollback(requested: Option, json: bool) -> Result { ); return Err(error); } - if let Err(error) = restore_verified_web_tree(&release.release_dir) { - let _ = finish_update_attempt( - history_path.as_deref(), - &attempt.attempt_id, - "failed", - Some(format!("{error:#}")), - Some(release.receipt.clone()), - ); - return Err(error); - } if let Err(error) = persist_rollback_suppression(&startup_state_path, &release.version_text) { let _ = finish_update_attempt( history_path.as_deref(), @@ -3878,29 +3786,8 @@ mod tests { let release_dir = data_dir.join("releases/0.9.0/native.fixture"); let binary = release_dir.join("bin/maestro"); fs::create_dir_all(binary.parent().expect("binary parent")).expect("create binary dir"); - fs::create_dir_all(release_dir.join("web")).expect("create web dir"); fs::write(&binary, b"#!/bin/sh\nprintf 'maestro 0.9.0\\n'\n").expect("write binary"); fs::set_permissions(&binary, fs::Permissions::from_mode(0o755)).expect("chmod binary"); - fs::write(release_dir.join("web/index.html"), b"fixture web").expect("write web"); - fs::write(release_dir.join("web/app.js"), b"fixture js").expect("write web script"); - let web_archive = release_dir.join(WEB_ARCHIVE_FILE); - let archive_source = temporary.path().join("web-source"); - fs::create_dir_all(&archive_source).expect("create web archive source"); - fs::write(archive_source.join("index.html"), b"fixture web") - .expect("write archived web index"); - fs::write(archive_source.join("app.js"), b"fixture js").expect("write archived web script"); - assert!( - Command::new("tar") - .args(["-czf"]) - .arg(&web_archive) - .args(["-C"]) - .arg(&archive_source) - .arg(".") - .status() - .expect("create web archive") - .success() - ); - let web_archive_bytes = fs::read(&web_archive).expect("read web archive"); let metadata = VersionMetadata { version: "0.9.0".to_owned(), schema_version: Some("evalops.maestro.release-metadata.v1".to_owned()), @@ -3928,7 +3815,6 @@ mod tests { manifest_checksum_verified: true, signature_verified: true, artifact_sha256: sha256_file(&binary), - web_sha256: sha256_file(&web_archive), metadata_sha256: sha256_file(&metadata_path), metadata_checksum_verified: true, }, @@ -3962,34 +3848,6 @@ mod tests { let selected = select_rollback_release(&data_dir, "1.0.0", None).expect("select rollback"); verify_retained_release(&selected).expect("verify retained binary"); - fs::remove_file(selected.release_dir.join("web/index.html")) - .expect("remove extracted web index"); - assert_eq!( - list_verified_releases(&data_dir) - .expect("list release with missing extracted index") - .len(), - 1 - ); - fs::remove_file(selected.release_dir.join("web/app.js")) - .expect("remove extracted web script"); - restore_verified_web_tree(&selected.release_dir).expect("restore extracted web tree"); - assert_eq!( - fs::read(selected.release_dir.join("web/index.html")).expect("read restored index"), - b"fixture web" - ); - assert_eq!( - fs::read(selected.release_dir.join("web/app.js")).expect("read restored script"), - b"fixture js" - ); - - fs::write(&web_archive, b"corrupted archive").expect("corrupt web archive"); - assert!( - list_verified_releases(&data_dir) - .expect("list corrupted web archive") - .is_empty() - ); - fs::write(&web_archive, &web_archive_bytes).expect("restore web archive"); - let launcher = temporary.path().join("bin/maestro"); fs::create_dir_all(launcher.parent().expect("launcher parent")) .expect("create launcher dir"); @@ -4007,7 +3865,6 @@ mod tests { let launcher_text = fs::read_to_string(&launcher).expect("read launcher"); assert!(launcher_text.contains("MAESTRO_STARTUP_UPDATE_STATE")); assert!(launcher_text.contains(&selected.release_dir.display().to_string())); - assert!(selected.release_dir.join("web/index.html").is_file()); fs::remove_file(&metadata_path).expect("remove optional metadata fixture"); receipt.verification.metadata_sha256 = None; diff --git a/packages/tui-rs/tests/entrypoint.rs b/packages/tui-rs/tests/entrypoint.rs index 9189df92e..eb638101b 100644 --- a/packages/tui-rs/tests/entrypoint.rs +++ b/packages/tui-rs/tests/entrypoint.rs @@ -4,7 +4,7 @@ //! command list and its own `--headless`/`--rpc`/`--mode=headless`/`exec`/ //! `print`/`-p` matching, independently of the routing here — with nothing //! enforcing that the two stayed in sync. `maestro-rs` now forwards -//! everything except `web`/`--help`/`--version` to `run_cli` unchanged, so +//! everything except `serve`/`--help`/`--version` to `run_cli` unchanged, so //! this crate's `entrypoint` module is the single place that decides //! whether a subcommand reaches the utility handler, the headless server, //! the exec/print bridge, or the interactive TUI. This test drives that diff --git a/packages/web/README.md b/packages/web/README.md deleted file mode 100644 index b41e9c009..000000000 --- a/packages/web/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Deixic Code browser assets - -This directory contains the versioned browser bundle served by the native Rust runtime gateway. The bundle is intentionally committed so building, packaging, and running Deixic Code never requires a TypeScript or JavaScript toolchain. - -`dist/index.html` is the asset entry point. Update the snapshot only as part of an explicit browser-client replacement; runtime and wire-protocol behavior belong in `packages/runtime-gateway-rs` and `packages/tui-rs`. diff --git a/scripts/bridge/install-native-host.mjs b/scripts/bridge/install-native-host.mjs deleted file mode 100755 index 877d663a3..000000000 --- a/scripts/bridge/install-native-host.mjs +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env node -/** - * Install the Maestro native messaging host manifest. - * - * Resolution order for extension ID: - * 1) --extension-id / CONDUCTOR_EXTENSION_ID - * 2) CONDUCTOR_PEM_PATH / ../conductor/conductor.pem - * 3) Scan Chrome profiles for an installed Conductor extension - */ - -import { readFile, writeFile, mkdir, readdir, chmod } from "node:fs/promises"; -import { createHash } from "node:crypto"; -import path from "node:path"; -import os from "node:os"; -import { fileURLToPath } from "node:url"; - -const ROOT = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", - ".." -); -const HOST_PATH = path.resolve(ROOT, "scripts", "bridge", "native-host.js"); -const HOST_NAME = "com.evalops.composer_bridge"; - -const args = process.argv.slice(2); -const argIndex = args.findIndex((value) => value === "--extension-id"); -const argExtensionId = argIndex >= 0 ? args[argIndex + 1] : null; - -const envExtensionId = process.env.CONDUCTOR_EXTENSION_ID || null; -const envPemPath = process.env.CONDUCTOR_PEM_PATH || null; - -async function deriveExtensionIdFromPem(pemContent) { - try { - const crypto = await import("node:crypto"); - const key = crypto.createPrivateKey(pemContent); - const publicKey = crypto.createPublicKey(key); - const publicKeyDer = publicKey.export({ type: "spki", format: "der" }); - const hash = createHash("sha256").update(publicKeyDer).digest(); - // Chrome extension IDs are 32 characters: each byte produces 2 hex chars mapped to 'a'-'p' - return Array.from(hash.slice(0, 16)) - .flatMap((byte) => [ - String.fromCharCode(97 + ((byte >> 4) & 0x0f)), // high nibble - String.fromCharCode(97 + (byte & 0x0f)), // low nibble - ]) - .join(""); - } catch { - return null; - } -} - -async function findPemExtensionId() { - const candidates = []; - if (envPemPath) candidates.push(envPemPath); - candidates.push(path.resolve(ROOT, "..", "conductor", "conductor.pem")); - - for (const candidate of candidates) { - try { - const pem = await readFile(candidate, "utf8"); - const id = await deriveExtensionIdFromPem(pem); - if (id) return id; - } catch { - // ignore missing/invalid PEM - } - } - return null; -} - -async function listProfileDirs(baseDir) { - try { - const entries = await readdir(baseDir, { withFileTypes: true }); - return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name); - } catch { - return []; - } -} - -async function findInstalledExtensionId() { - if (process.platform !== "darwin") return null; - const chromeBase = path.join( - os.homedir(), - "Library", - "Application Support", - "Google", - "Chrome" - ); - const profiles = await listProfileDirs(chromeBase); - for (const profile of profiles) { - const extensionsRoot = path.join(chromeBase, profile, "Extensions"); - let extensionDirs = []; - try { - extensionDirs = await readdir(extensionsRoot, { withFileTypes: true }); - } catch { - continue; - } - - for (const extensionDir of extensionDirs) { - if (!extensionDir.isDirectory()) continue; - const extensionId = extensionDir.name; - const versionRoot = path.join(extensionsRoot, extensionId); - let versionDirs = []; - try { - versionDirs = await readdir(versionRoot, { withFileTypes: true }); - } catch { - continue; - } - for (const versionDir of versionDirs) { - if (!versionDir.isDirectory()) continue; - const manifestPath = path.join(versionRoot, versionDir.name, "manifest.json"); - try { - const raw = await readFile(manifestPath, "utf8"); - const manifest = JSON.parse(raw); - const updateUrl = manifest.update_url || ""; - const name = manifest.name || ""; - if (updateUrl.includes("evalops.dev") || name === "Conductor") { - return extensionId; - } - } catch { - // ignore parse errors - } - } - } - } - return null; -} - -function resolveManifestPath() { - if (process.platform === "darwin") { - return path.join( - os.homedir(), - "Library", - "Application Support", - "Google", - "Chrome", - "NativeMessagingHosts", - `${HOST_NAME}.json` - ); - } - if (process.platform === "win32") { - const base = process.env.LOCALAPPDATA; - if (!base) return null; - return path.join( - base, - "Google", - "Chrome", - "User Data", - "NativeMessagingHosts", - `${HOST_NAME}.json` - ); - } - return path.join( - os.homedir(), - ".config", - "google-chrome", - "NativeMessagingHosts", - `${HOST_NAME}.json` - ); -} - -async function main() { - let extensionId = argExtensionId || envExtensionId; - - if (!extensionId) { - extensionId = await findPemExtensionId(); - } - if (!extensionId) { - extensionId = await findInstalledExtensionId(); - } - - if (!extensionId) { - console.error( - [ - "Unable to determine Conductor extension ID.", - "Provide it via --extension-id or CONDUCTOR_EXTENSION_ID,", - "or set CONDUCTOR_PEM_PATH to a conductor.pem file,", - "or install the extension so it can be discovered in Chrome profiles.", - ].join(" ") - ); - process.exit(1); - } - - const manifestPath = resolveManifestPath(); - if (!manifestPath) { - console.error("Unable to resolve native host manifest path for this OS."); - process.exit(1); - } - - await mkdir(path.dirname(manifestPath), { recursive: true }); - - const payload = { - name: HOST_NAME, - description: "Maestro native host for the Conductor bridge", - path: HOST_PATH, - type: "stdio", - allowed_origins: [`chrome-extension://${extensionId}/`], - }; - - await writeFile(manifestPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); - - // Chrome requires the native host script to be executable on Unix-like systems - if (process.platform !== "win32") { - await chmod(HOST_PATH, 0o755); - } - - console.log(`Installed native host manifest at ${manifestPath}`); - console.log(`Allowed extension: ${extensionId}`); -} - -await main(); diff --git a/scripts/bridge/native-host-manifest.example.json b/scripts/bridge/native-host-manifest.example.json deleted file mode 100644 index 93ab197a0..000000000 --- a/scripts/bridge/native-host-manifest.example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "com.evalops.composer_bridge", - "description": "Maestro bridge native messaging host", - "path": "/ABSOLUTE/PATH/TO/composer/scripts/bridge/native-host.js", - "type": "stdio", - "allowed_origins": [ - "chrome-extension://YOUR_EXTENSION_ID/" - ] -} diff --git a/scripts/bridge/native-host.js b/scripts/bridge/native-host.js deleted file mode 100644 index 76f009f93..000000000 --- a/scripts/bridge/native-host.js +++ /dev/null @@ -1,638 +0,0 @@ -#!/usr/bin/env node -// Native messaging host for the Conductor <-> Maestro bridge. -// Implements Chrome's length-prefixed JSON protocol and launches/monitors -// the local Maestro web server as needed. - -import { spawn } from "node:child_process"; -import { setTimeout as delay } from "node:timers/promises"; - -const DEFAULT_BASE_URL = - process.env.MAESTRO_BRIDGE_BASE_URL?.trim() || "http://localhost:8080"; -const HOST_NAME = - process.env.MAESTRO_BRIDGE_HOST?.trim() || "com.evalops.composer_bridge"; -const COMMAND = - process.env.MAESTRO_BRIDGE_COMMAND?.trim() || "composer"; -const RAW_ARGS = process.env.MAESTRO_BRIDGE_ARGS?.trim() || ""; -const STATUS_POLL_INTERVAL_MS = Number.parseInt( - process.env.MAESTRO_BRIDGE_POLL_MS || "2000", - 10, -); -const LAUNCH_TIMEOUT_MS = Number.parseInt( - process.env.MAESTRO_BRIDGE_LAUNCH_TIMEOUT_MS || "15000", - 10, -); -const PLATFORM_RUNTIME_TIMEOUT_MS = Number.parseInt( - process.env.MAESTRO_BRIDGE_PLATFORM_RUNTIME_TIMEOUT_MS || - process.env.MAESTRO_AGENT_RUNTIME_TIMEOUT_MS || - process.env.MAESTRO_AGENT_RUNTIME_SERVICE_TIMEOUT_MS || - process.env.AGENT_RUNTIME_SERVICE_TIMEOUT_MS || - "2000", - 10, -); -const RECORD_RUN_EVENT_PATH = - "/agentruntime.v1.AgentRuntimeService/RecordRunEvent"; -const HANDLE_TRIGGER_PATH = - "/agentruntime.v1.AgentRuntimeService/HandleTrigger"; -const AGENT_RUNTIME_SERVICE_SUFFIX = "/agentruntime.v1.AgentRuntimeService"; - -let webProcess = null; -let statusTimer = null; -let lastStatusSignature = ""; -let currentBaseUrl = DEFAULT_BASE_URL; -let polling = false; - -function parseArgs(raw) { - if (!raw) return []; - try { - const parsed = JSON.parse(raw); - if (Array.isArray(parsed)) { - return parsed.map((value) => String(value)); - } - } catch { - // fall back to whitespace split - } - return raw.split(/\s+/).filter(Boolean); -} - -const EXTRA_ARGS = parseArgs(RAW_ARGS); - -function resolveBaseUrls(baseUrl) { - const urls = []; - if (!baseUrl) return urls; - urls.push(baseUrl); - try { - const parsed = new URL(baseUrl); - if (parsed.hostname === "localhost") { - urls.push(baseUrl.replace("localhost", "127.0.0.1")); - } else if (parsed.hostname === "127.0.0.1") { - urls.push(baseUrl.replace("127.0.0.1", "localhost")); - } - } catch { - // ignore invalid base url - } - return Array.from(new Set(urls)); -} - -async function fetchWithTimeout(url, timeoutMs) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - try { - const response = await fetch(url, { signal: controller.signal }); - return response; - } finally { - clearTimeout(timer); - } -} - -async function probeBridgeStatus(baseUrl) { - const candidates = resolveBaseUrls(baseUrl); - let lastError = null; - - for (const candidate of candidates) { - try { - const response = await fetchWithTimeout( - new URL("/api/bridge/status", candidate).toString(), - 4000, - ); - if (!response.ok) { - throw new Error(`status ${response.status}`); - } - const data = await response.json(); - return { - reachable: true, - baseUrl: candidate, - bridgeStatus: data, - error: null, - }; - } catch (error) { - lastError = error; - } - } - - return { - reachable: false, - baseUrl: candidates[0] || baseUrl || null, - bridgeStatus: null, - error: lastError ? String(lastError) : "Bridge status unavailable", - }; -} - -function isProcessRunning(proc) { - return Boolean(proc && proc.exitCode === null && !proc.killed); -} - -function getPortFromBaseUrl(baseUrl) { - try { - const parsed = new URL(baseUrl); - return parsed.port ? Number.parseInt(parsed.port, 10) : null; - } catch { - return null; - } -} - -const EXTENSION_ID_PATTERN = /^[a-p]{32}$/; -const EXTENSION_ORIGIN_PATTERN = /^chrome-extension:\/\/([a-p]{32})\/?$/; - -/** - * Resolve the concrete `chrome-extension://` origin allowed to call the - * local Maestro server. Chrome starts a native messaging host with the calling - * extension's origin as a command-line argument, and that argument is - * authenticated by the host manifest's `allowed_origins`, so it is the most - * trustworthy source. `CONDUCTOR_EXTENSION_ID` is the documented fallback used - * by `install-native-host.mjs`. - * - * Returns null when the extension identity cannot be established. Callers must - * not fall back to "*": a wildcard origin lets any page the user visits reach - * the local agent runtime. - */ -function resolveExtensionOrigin() { - for (const arg of process.argv.slice(1)) { - const match = EXTENSION_ORIGIN_PATTERN.exec(String(arg).trim()); - if (match) return `chrome-extension://${match[1]}`; - } - const configured = process.env.CONDUCTOR_EXTENSION_ID?.trim(); - if (configured && EXTENSION_ID_PATTERN.test(configured)) { - return `chrome-extension://${configured}`; - } - return null; -} - -function buildLaunchEnv() { - const env = { ...process.env }; - // MAESTRO_WEB_REQUIRE_KEY is deliberately left alone. The bridge launches - // `maestro web` on its default loopback bind, where the control plane - // already runs without an API key; forcing the kill-switch on would also - // strip auth from a non-loopback bind the operator configured on purpose. - if (!env.MAESTRO_WEB_REQUIRE_REDIS) env.MAESTRO_WEB_REQUIRE_REDIS = "0"; - if (!env.MAESTRO_WEB_ORIGIN) { - const extensionOrigin = resolveExtensionOrigin(); - // No wildcard fallback. Leaving MAESTRO_WEB_ORIGIN unset keeps the - // server's built-in localhost allowlist, which is still correct for the - // web UI even when the extension identity is unknown. - if (extensionOrigin) env.MAESTRO_WEB_ORIGIN = extensionOrigin; - } - return env; -} - -async function ensureComposerWeb(baseUrl) { - currentBaseUrl = baseUrl || currentBaseUrl; - const status = await probeBridgeStatus(currentBaseUrl); - if (status.reachable) { - return { ...status, launched: false }; - } - - if (!isProcessRunning(webProcess)) { - const port = getPortFromBaseUrl(currentBaseUrl); - const args = ["web", ...(port ? ["--port", String(port)] : []), ...EXTRA_ARGS]; - try { - webProcess = spawn(COMMAND, args, { - stdio: "ignore", - env: buildLaunchEnv(), - }); - webProcess.on("exit", () => { - webProcess = null; - }); - webProcess.on("error", (_err) => { - // Handle spawn errors (e.g., ENOENT when command not found) - // The error is logged but not thrown to avoid crashing the host - webProcess = null; - }); - } catch (error) { - return { - reachable: false, - baseUrl: currentBaseUrl, - bridgeStatus: null, - error: `Failed to launch Maestro: ${error instanceof Error ? error.message : String(error)}`, - launched: false, - }; - } - } - - const startedAt = Date.now(); - while (Date.now() - startedAt < LAUNCH_TIMEOUT_MS) { - const refreshed = await probeBridgeStatus(currentBaseUrl); - if (refreshed.reachable) { - return { ...refreshed, launched: true }; - } - await delay(500); - } - - return { - reachable: false, - baseUrl: currentBaseUrl, - bridgeStatus: null, - error: "Maestro did not become reachable in time", - launched: false, - }; -} - -function makeStatusPayload(status, extras = {}) { - return { - host: { - name: HOST_NAME, - command: COMMAND, - pid: webProcess?.pid ?? null, - }, - baseUrl: status.baseUrl ?? currentBaseUrl, - reachable: Boolean(status.reachable), - error: status.error ?? null, - bridgeStatus: status.bridgeStatus ?? null, - ...extras, - }; -} - -function statusSignature(payload) { - return JSON.stringify({ - reachable: payload.reachable, - baseUrl: payload.baseUrl, - version: payload.bridgeStatus?.version ?? null, - }); -} - -function startStatusPolling() { - if (statusTimer) return; - statusTimer = setInterval(async () => { - if (polling) return; - polling = true; - try { - const status = await probeBridgeStatus(currentBaseUrl); - const payload = makeStatusPayload(status, { event: "poll" }); - const signature = statusSignature(payload); - if (signature !== lastStatusSignature) { - lastStatusSignature = signature; - sendNotification("bridge/status", payload); - } - } catch (error) { - sendNotification("bridge/status", { - host: { name: HOST_NAME }, - reachable: false, - error: String(error), - baseUrl: currentBaseUrl, - }); - } finally { - polling = false; - } - }, STATUS_POLL_INTERVAL_MS); - statusTimer.unref?.(); -} - -function stopStatusPolling() { - if (!statusTimer) return; - clearInterval(statusTimer); - statusTimer = null; -} - -function sendNativeMessage(message) { - const json = JSON.stringify(message); - const buffer = Buffer.from(json, "utf8"); - const header = Buffer.alloc(4); - header.writeUInt32LE(buffer.length, 0); - process.stdout.write(Buffer.concat([header, buffer])); -} - -function sendResponse(id, type, payload) { - sendNativeMessage({ - id, - type, - ok: !payload?.error, - ...payload, - }); -} - -function sendNotification(method, params) { - sendNativeMessage({ - jsonrpc: "2.0", - method, - params, - }); -} - -function firstEnv(...names) { - for (const name of names) { - const value = process.env[name]?.trim(); - if (value) return value; - } - return ""; -} - -function normalizeAgentRuntimeBaseUrl(value) { - let normalized = value.trim().replace(/\/+$/, ""); - for (const suffix of [ - RECORD_RUN_EVENT_PATH, - HANDLE_TRIGGER_PATH, - AGENT_RUNTIME_SERVICE_SUFFIX, - ]) { - if (normalized.endsWith(suffix)) { - normalized = normalized.slice(0, -suffix.length).replace(/\/+$/, ""); - } - } - return normalized; -} - -function platformRuntimeConfigFromEnv() { - const baseUrl = firstEnv( - "MAESTRO_BRIDGE_AGENT_RUNTIME_URL", - "MAESTRO_AGENT_RUNTIME_SERVICE_URL", - "AGENT_RUNTIME_SERVICE_URL", - ); - if (!baseUrl) return null; - return { - baseUrl: normalizeAgentRuntimeBaseUrl(baseUrl), - token: firstEnv( - "MAESTRO_BRIDGE_AGENT_RUNTIME_TOKEN", - "MAESTRO_AGENT_RUNTIME_SERVICE_TOKEN", - "AGENT_RUNTIME_SERVICE_TOKEN", - "MAESTRO_EVALOPS_ACCESS_TOKEN", - "EVALOPS_TOKEN", - ), - organizationId: firstEnv( - "MAESTRO_BRIDGE_AGENT_RUNTIME_ORG_ID", - "MAESTRO_AGENT_RUNTIME_ORG_ID", - "MAESTRO_AGENT_RUNTIME_ORGANIZATION_ID", - "AGENT_RUNTIME_ORG_ID", - "AGENT_RUNTIME_ORGANIZATION_ID", - "MAESTRO_EVALOPS_ORG_ID", - "EVALOPS_ORGANIZATION_ID", - "EVALOPS_ORG_ID", - "MAESTRO_ENTERPRISE_ORG_ID", - "MAESTRO_LLM_GATEWAY_ORG_ID", - "MAESTRO_REMOTE_RUNNER_ORG_ID", - ), - runId: firstEnv( - "MAESTRO_BRIDGE_PLATFORM_RUN_ID", - "MAESTRO_AGENT_RUNTIME_RUN_ID", - "AGENT_RUNTIME_RUN_ID", - ), - timeoutMs: Number.isFinite(PLATFORM_RUNTIME_TIMEOUT_MS) - ? PLATFORM_RUNTIME_TIMEOUT_MS - : 2000, - }; -} - -function cleanString(value, maxLength = 256) { - if (typeof value !== "string") return ""; - const trimmed = value.trim(); - if (!trimmed) return ""; - return trimmed.slice(0, maxLength); -} - -function cleanBoolean(value) { - return typeof value === "boolean" ? value : undefined; -} - -function compactObject(values) { - const result = {}; - for (const [key, value] of Object.entries(values)) { - if (value === undefined || value === null || value === "") continue; - result[key] = value; - } - return result; -} - -function browserControlDecisionRunId(params, config) { - return ( - cleanString(params?.platformRunId, 160) || - cleanString(params?.platform_run_id, 160) || - cleanString(params?.runId, 160) || - cleanString(params?.run_id, 160) || - config.runId - ); -} - -function buildBrowserControlDecisionRuntimeEvent(params, config) { - if (!params || typeof params !== "object") return null; - const runId = browserControlDecisionRunId(params, config); - if (!runId) return null; - const method = cleanString(params.method, 120) || "unknown"; - const decision = cleanString(params.decision, 32) || "unknown"; - const denyReason = cleanString(params.denyReason ?? params.deny_reason, 120); - const methodProfile = cleanString( - params.methodProfile ?? params.method_profile, - 120, - ); - const safeSummary = - decision === "denied" - ? `Browser-control denied: ${method}${denyReason ? ` (${denyReason})` : ""}` - : `Browser-control ${decision}: ${method}`; - - return { - runId, - type: "RUNTIME_EVENT_TYPE_AGENT_PROGRESS_RECORDED", - message: safeSummary, - attributes: compactObject({ - schemaVersion: "browser-control-runtime-decision/v1", - adapter: "maestro-conductor-native-host", - source: "conductor-browser-control-native-host", - nativeHost: HOST_NAME, - traceId: cleanString(params.traceId ?? params.trace_id, 160), - observedAt: cleanString(params.observedAt ?? params.observed_at, 80), - method, - methodProfile, - policyHash: cleanString(params.policyHash ?? params.policy_hash, 160), - platformReceiptPresent: cleanBoolean( - params.platformReceiptPresent ?? params.platform_receipt_present, - ), - platformReceiptId: cleanString( - params.platformReceiptId ?? params.platform_receipt_id, - 160, - ), - platformRequestHash: cleanString( - params.platformRequestHash ?? params.platform_request_hash, - 160, - ), - approvalRequired: cleanBoolean( - params.approvalRequired ?? params.approval_required, - ), - decision, - denyReason: denyReason || "none", - conductorContractVersion: cleanString( - params.conductorContractVersion ?? - params.conductor_contract_version, - 80, - ), - }), - visibility: { - level: "RUNTIME_VISIBILITY_LEVEL_ADMIN_VISIBLE", - audiences: [ - "RUNTIME_AUDIENCE_WORKSPACE_ADMINS", - "RUNTIME_AUDIENCE_AUDIT", - "RUNTIME_AUDIENCE_SYSTEM", - ], - sensitivity: "RUNTIME_SENSITIVITY_INTERNAL", - safeSummary, - }, - }; -} - -function runtimeHeaders(config) { - return compactObject({ - Authorization: config.token ? `Bearer ${config.token}` : undefined, - "Content-Type": "application/json", - "Connect-Protocol-Version": "1", - "X-Organization-ID": config.organizationId, - }); -} - -async function postBrowserControlDecisionToPlatform(params) { - const config = platformRuntimeConfigFromEnv(); - if (!config) { - return { recorded: false, reason: "platform_runtime_not_configured" }; - } - const event = buildBrowserControlDecisionRuntimeEvent(params, config); - if (!event) { - return { recorded: false, reason: "platform_run_id_missing" }; - } - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), config.timeoutMs); - try { - const response = await fetch(`${config.baseUrl}${RECORD_RUN_EVENT_PATH}`, { - method: "POST", - headers: runtimeHeaders(config), - body: JSON.stringify(event), - signal: controller.signal, - }); - if (!response.ok) { - return { recorded: false, reason: `platform_runtime_${response.status}` }; - } - const body = await response.json().catch(() => ({})); - return { - recorded: true, - eventId: cleanString(body?.event?.id, 160), - sequence: typeof body?.event?.sequence === "number" ? body.event.sequence : undefined, - }; - } catch (error) { - return { - recorded: false, - reason: error instanceof Error ? error.message : String(error), - }; - } finally { - clearTimeout(timer); - } -} - -function handleJsonRpcNotification(message) { - if ( - message.method !== "onBrowserControlDecision" || - Object.hasOwn(message, "id") - ) { - return false; - } - const params = message.params ?? {}; - void postBrowserControlDecisionToPlatform(params); - return true; -} - -function handleMessage(message) { - if (!message || typeof message !== "object") { - return; - } - if (message.jsonrpc === "2.0" && typeof message.method === "string") { - if (handleJsonRpcNotification(message)) { - return; - } - } - const { id, type } = message; - if (id == null || typeof type !== "string") { - sendNativeMessage({ - type: "error", - ok: false, - error: "Invalid bridge message", - }); - return; - } - - const baseUrl = - typeof message.baseUrl === "string" && message.baseUrl.trim() - ? message.baseUrl.trim() - : currentBaseUrl; - - switch (type) { - case "ping": { - sendResponse(id, "pong", { data: { host: HOST_NAME } }); - break; - } - case "status": { - currentBaseUrl = baseUrl; - startStatusPolling(); - probeBridgeStatus(baseUrl) - .then((status) => { - const payload = makeStatusPayload(status); - sendResponse(id, "status_response", { data: payload }); - }) - .catch((error) => { - sendResponse(id, "status_response", { - error: error instanceof Error ? error.message : String(error), - }); - }); - break; - } - case "launch": { - currentBaseUrl = baseUrl; - startStatusPolling(); - ensureComposerWeb(baseUrl) - .then((status) => { - const payload = makeStatusPayload(status, { - launched: status.launched ?? false, - }); - sendResponse(id, "launch_response", { data: payload }); - }) - .catch((error) => { - sendResponse(id, "launch_response", { - error: error instanceof Error ? error.message : String(error), - }); - }); - break; - } - case "shutdown": { - if (isProcessRunning(webProcess)) { - try { - webProcess.kill(); - } catch { - // ignore errors - } - } - stopStatusPolling(); - sendResponse(id, "shutdown_response", { data: { stopped: true } }); - break; - } - default: { - sendResponse(id, "error", { error: `Unknown request type: ${type}` }); - } - } -} - -// Native messaging framing -let buffer = Buffer.alloc(0); -process.stdin.on("data", (chunk) => { - buffer = Buffer.concat([buffer, chunk]); - while (buffer.length >= 4) { - const messageLength = buffer.readUInt32LE(0); - if (buffer.length < 4 + messageLength) return; - const messageBuffer = buffer.slice(4, 4 + messageLength); - buffer = buffer.slice(4 + messageLength); - try { - const message = JSON.parse(messageBuffer.toString("utf8")); - handleMessage(message); - } catch (error) { - sendNativeMessage({ - type: "error", - ok: false, - error: `Failed to parse message: ${error instanceof Error ? error.message : String(error)}`, - }); - } - } -}); - -process.stdin.on("end", () => { - stopStatusPolling(); - if (isProcessRunning(webProcess)) { - try { - webProcess.kill(); - } catch { - // ignore errors - } - } -}); - -process.stdin.resume(); diff --git a/scripts/check-deixic-code-naming.mjs b/scripts/check-deixic-code-naming.mjs index 0de740cea..92507afd9 100644 --- a/scripts/check-deixic-code-naming.mjs +++ b/scripts/check-deixic-code-naming.mjs @@ -13,7 +13,6 @@ const REQUIRED_TEXT = new Map([ ["docs/THREAT_MODEL.md", ["# Deixic Code Threat Model"]], ["docs/ENTERPRISE.md", ["# Deixic Code Enterprise"]], ["packages/tui-rs/README.md", ["# Deixic Code TUI (Rust)"]], - ["packages/web/README.md", ["# Deixic Code browser assets"]], ["packages/tui-rs/src/crash_handler.rs", ["=== Deixic Code Crash Report ==="]], ["packages/runtime-gateway-rs/src/a2a/tasks.rs", ["Deixic Code is working on the A2A task."]], ["packages/maestro-rs/Cargo.toml", ["description = \"Canonical native Rust CLI for Deixic Code\""]], @@ -26,7 +25,6 @@ const REQUIRED_TEXT = new Map([ ["packages/a2a-ledger-rs/Cargo.toml", ["Deixic Code A2A SQLite task ledger"]], ["packages/session-history-rs/Cargo.toml", ["authenticated Deixic Code sessions"]], ["examples/hooks/wasm-plugin/Cargo.toml", ["hook plugin for Deixic Code"]], - ["packages/web/dist/index.html", ["Deixic Code - AI Coding Assistant", "Loading Deixic Code..."]], ["packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml", ["Deixic Code", "id=\"Maestro\"", "id=\"Maestro Notifications\""]], ["scripts/install.sh", ["$install_dir/deixic-code", "$install_dir/maestro", "Installed Deixic Code"]], ["scripts/materialize-native-package.mjs", ["resolve(\"bin\", \"deixic-code\")", "exec \"$bin_dir/maestro\" \"$@\""]], @@ -40,7 +38,6 @@ const FORBIDDEN_DISPLAY_TEXT = new Map([ ["docs/SAFETY.md", ["Maestro can execute shell commands"]], ["docs/mcp-config.md", ["(Maestro)", "approach for Maestro"]], ["packages/tui-rs/README.md", ["# Maestro TUI", "terminal UI for Maestro"]], - ["packages/web/README.md", ["# Maestro browser assets", "running Maestro never requires"]], ["packages/tui-rs/docs/user-guide/07-mcp-servers.md", ["Maestro supports the Model Context Protocol", "Launch Maestro"]], ["packages/tui-rs/docs/user-guide/12-sandbox-and-safety.md", ["Maestro runs tools on your machine"]], ["packages/tui-rs/docs/user-guide/14-worktrees.md", ["Maestro can run a whole session", "Maestro prints its path"]], @@ -82,7 +79,6 @@ const FORBIDDEN_DISPLAY_TEXT = new Map([ ["packages/a2a-ledger-rs/Cargo.toml", ["Maestro A2A SQLite task ledger"]], ["packages/session-history-rs/Cargo.toml", ["authenticated Maestro sessions"]], ["examples/hooks/wasm-plugin/Cargo.toml", ["hook plugin for Maestro"]], - ["packages/web/dist/index.html", ["Maestro", "Loading Maestro"]], ["packages/maestro-rs/src/main.rs", ["const HELP: &str = \"Maestro", "Usage:\\n maestro setup"]], ["packages/presentation-rs/src/components/deixic_logo.rs", ["shimmer_spans(\"Maestro\")"]], ["packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml", ["<name>Maestro</name>", "text=\"Focus Maestro\""]], diff --git a/scripts/check-deixic-code-naming.test.mjs b/scripts/check-deixic-code-naming.test.mjs index c771427ad..0960e24d0 100644 --- a/scripts/check-deixic-code-naming.test.mjs +++ b/scripts/check-deixic-code-naming.test.mjs @@ -39,9 +39,9 @@ test("the guard rejects a missing command alias", () => { }); test("the guard rejects stale customer-facing Maestro display copy", () => { - const path = "packages/web/dist/index.html"; + const path = "packages/maestro-rs/src/main.rs"; const content = readFileSync(new URL(`../${path}`, import.meta.url), "utf8") - .replace("<title>Deixic Code", "<title>Maestro"); + .replace('const HELP: &str = "Deixic Code', 'const HELP: &str = "Maestro'); const problems = findDeixicCodeNamingProblems(root, new Map([[path, content]])); assert(problems.some((problem) => problem.includes("stale display text"))); }); diff --git a/scripts/check-prepared-public-mirror-tree.mjs b/scripts/check-prepared-public-mirror-tree.mjs index bb349aa90..1dca03343 100644 --- a/scripts/check-prepared-public-mirror-tree.mjs +++ b/scripts/check-prepared-public-mirror-tree.mjs @@ -76,11 +76,6 @@ if (credentialArtifacts.length > 0) { ); } -const browserEntry = resolve(targetRoot, "packages/web/dist/index.html"); -if (!existsSync(browserEntry)) { - fail(`Prepared public mirror is missing versioned browser assets: ${browserEntry}`); -} - const boundaryScript = resolve( targetRoot, "scripts/check-public-surface-boundary.mjs", diff --git a/scripts/check-rust-only-docs.mjs b/scripts/check-rust-only-docs.mjs index fd453de0d..2837f62a0 100755 --- a/scripts/check-rust-only-docs.mjs +++ b/scripts/check-rust-only-docs.mjs @@ -8,7 +8,6 @@ const files = [ "docs/ARCHITECTURE.md", "docs/TUI_ARCHITECTURE.md", "docs/NATIVE_TUI_PARITY.md", - "docs/WEB_UI.md", "docs/FEATURES.md", "docs/QUICKSTART.md", "docs/CONTRIBUTOR_RUNBOOK.md", diff --git a/scripts/generate-runtime-passport.test.mjs b/scripts/generate-runtime-passport.test.mjs index b1d951ea2..dac26645e 100644 --- a/scripts/generate-runtime-passport.test.mjs +++ b/scripts/generate-runtime-passport.test.mjs @@ -80,7 +80,7 @@ test("conformance rejects a tag-only OCI reference without an artifact digest", try { execFileSync( process.execPath, - ["scripts/run-runtime-conformance.mjs", "--docker-image", "ghcr.io/evalops/maestro:review"], + ["scripts/run-runtime-conformance.mjs", "--docker-image", "ghcr.io/dx-corp/maestro:review"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ); } catch (caught) { @@ -98,7 +98,7 @@ test("conformance rejects an artifact digest that differs from the pinned OCI im [ "scripts/run-runtime-conformance.mjs", "--docker-image", - `ghcr.io/evalops/maestro@sha256:${"a".repeat(64)}`, + `ghcr.io/dx-corp/maestro@sha256:${"a".repeat(64)}`, "--artifact-digest", `sha256:${"b".repeat(64)}`, ], diff --git a/scripts/install.sh b/scripts/install.sh index f6f4b15f3..6a87df70b 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -74,7 +74,6 @@ case "$platform" in esac asset="maestro-${platform}" -web_asset="maestro-web-dist.tar.gz" metadata_asset="release-metadata.json" case "$platform" in darwin-x64) @@ -991,7 +990,6 @@ signature_verified=0 metadata_checksum_verified=0 metadata_available=0 binary_checksum_verified=0 -web_checksum_verified=0 if fetch_manifest "$manifest" "${release_url}/SHA256SUMS"; then manifest_available=1 manifest_sha256="$(receipt_hash_file "$manifest")" @@ -1037,13 +1035,10 @@ if [[ "$manifest_available" == "1" && "$metadata_manifest_entry" == "0" ]]; then fi download "${release_url}/${asset}" "$tmpdir/$asset" "$asset" -download "${release_url}/${web_asset}" "$tmpdir/$web_asset" "$web_asset" progress_step 2 "Verifying checksum..." if [[ "$manifest_available" == "1" ]]; then verify_manifest_checksum "$manifest" "$tmpdir/$asset" "$asset" - verify_manifest_checksum "$manifest" "$tmpdir/$web_asset" "$web_asset" binary_checksum_verified=1 - web_checksum_verified=1 if [[ "$allow_unsigned" == "1" || "$allow_unsigned" == "true" || "$allow_unsigned" == "yes" ]]; then printf 'Checksum manifest verified; signature verification was explicitly bypassed.\n' >&2 else @@ -1053,9 +1048,6 @@ fi progress_step 3 "Installing update..." chmod 755 "$tmpdir/$asset" -mkdir -p "$tmpdir/maestro-web" -tar -xzf "$tmpdir/$web_asset" -C "$tmpdir/maestro-web" -[[ -f "$tmpdir/maestro-web/index.html" ]] || fail "$web_asset does not contain index.html" version_output="$("$tmpdir/$asset" --version 2>/dev/null)" || fail "Downloaded Maestro binary could not report its version" @@ -1079,7 +1071,6 @@ stage="$(mktemp -d "$release_root/.staging.XXXXXX")" || mkdir -p "$stage/bin" cp "$tmpdir/$asset" "$stage/bin/maestro" chmod 755 "$stage/bin/maestro" -mv "$tmpdir/maestro-web" "$stage/web" release_version_root="$release_root/$release_version" mkdir -p "$release_version_root" @@ -1088,21 +1079,17 @@ staged_version="$("$stage/bin/maestro" --version)" || release_dir="$(mktemp -d "$release_version_root/${platform}.XXXXXX")" || fail "Could not create release directory" mv "$stage/bin" "$release_dir/bin" -mv "$stage/web" "$release_dir/web" -cp "$tmpdir/$web_asset" "$release_dir/$web_asset" if [[ "$metadata_available" == "1" ]]; then cp "$tmpdir/$metadata_asset" "$release_dir/$metadata_asset" fi binary_receipt_sha256="$(receipt_hash_file "$release_dir/bin/maestro")" -web_receipt_sha256="$(receipt_hash_file "$tmpdir/$web_asset")" metadata_receipt_sha256="" if [[ "$metadata_available" == "1" ]]; then metadata_receipt_sha256="$(receipt_hash_file "$tmpdir/$metadata_asset")" fi installed_at_ms="$(( $(date +%s) * 1000 ))" verified=0 -if [[ "$signature_verified" == "1" && "$binary_checksum_verified" == "1" && - "$web_checksum_verified" == "1" ]]; then +if [[ "$signature_verified" == "1" && "$binary_checksum_verified" == "1" ]]; then verified=1 fi { @@ -1117,7 +1104,6 @@ fi printf ' "manifestChecksumVerified": %s,\n' "$([[ "$manifest_available" == "1" ]] && printf true || printf false)" printf ' "signatureVerified": %s,\n' "$([[ "$signature_verified" == "1" ]] && printf true || printf false)" printf ' "artifactSha256": "%s",\n' "$binary_receipt_sha256" - printf ' "webSha256": "%s",\n' "$web_receipt_sha256" printf ' "metadataSha256": ' if [[ "$metadata_available" == "1" ]]; then printf '"%s",\n' "$metadata_receipt_sha256" @@ -1166,7 +1152,6 @@ install_channel_quoted="$(shell_quote "$install_channel")" # These lines are intentionally literal: they are the generated launcher. # shellcheck disable=SC2016 printf '%s\n' \ - 'export MAESTRO_WEB_STATIC_ROOT="${MAESTRO_WEB_STATIC_ROOT:-$release_dir/web}"' \ 'export MAESTRO_INSTALL_METHOD=release' \ 'export MAESTRO_INSTALL_DIR="$install_dir"' \ 'export MAESTRO_DATA_DIR="$data_dir"' \ diff --git a/scripts/materialize-native-package.mjs b/scripts/materialize-native-package.mjs index 07c45c590..d615627cc 100644 --- a/scripts/materialize-native-package.mjs +++ b/scripts/materialize-native-package.mjs @@ -102,7 +102,6 @@ MAESTRO_INSTALL_METHOD=package \ MAESTRO_PACKAGE_NAME='${packageMetadata.name}' \ MAESTRO_PACKAGE_ROOT="$root" \ MAESTRO_VERSION='${packageMetadata.version}' \ -MAESTRO_WEB_STATIC_ROOT="\${MAESTRO_WEB_STATIC_ROOT:-$root/packages/web/dist}" \ exec "$root/vendor/maestro/$os-$arch/maestro" "$@" `; const launcherPath = resolve("bin", "maestro"); diff --git a/scripts/public-documentation-boundary.mjs b/scripts/public-documentation-boundary.mjs index cd4d8ebcf..973a09f52 100644 --- a/scripts/public-documentation-boundary.mjs +++ b/scripts/public-documentation-boundary.mjs @@ -5,7 +5,6 @@ const publicDocumentationPaths = [ "docs/ARCHITECTURE_DIAGRAM.md", "docs/BUILD_TESTING.md", "docs/CI_VERSION_PINS.md", - "docs/CONDUCTOR_BRIDGE.md", "docs/CONTRIBUTOR_RUNBOOK.md", "docs/CUSTOM_AGENTS.md", "docs/DEIXIC_CODE_MIGRATION.md", @@ -25,7 +24,6 @@ const publicDocumentationPaths = [ "docs/TOOLS_REFERENCE.md", "docs/TUI_ARCHITECTURE.md", "docs/VSCODE_ARCHITECTURE.md", - "docs/WEB_UI.md", "docs/cookbook", "docs/design/AGENT_SAFETY_BOUNDARY.md", "docs/design/AGENT_STATE_MACHINE.md", diff --git a/scripts/run-runtime-conformance.test.mjs b/scripts/run-runtime-conformance.test.mjs index 576087351..759f44f89 100644 --- a/scripts/run-runtime-conformance.test.mjs +++ b/scripts/run-runtime-conformance.test.mjs @@ -63,14 +63,14 @@ test("daemon-local fixture creation is a shell step before file search", () => { test("detached Docker conformance keeps the fixture stdin open", () => { const args = dockerConformanceRunArgs({ containerName: "maestro-runtime-conformance-test", - dockerImage: "ghcr.io/evalops/maestro@sha256:" + "a".repeat(64), + dockerImage: "ghcr.io/dx-corp/maestro@sha256:" + "a".repeat(64), }); assert.deepEqual(args.slice(0, 4), ["run", "-d", "-i", "--rm"]); assert.deepEqual(args.slice(-4), [ "--mount", "type=tmpfs,destination=/conformance-workspace", - "ghcr.io/evalops/maestro@sha256:" + "a".repeat(64), + "ghcr.io/dx-corp/maestro@sha256:" + "a".repeat(64), "conformance", ]); assert.equal(args.some((argument) => argument.includes("/tmp/maestro-runtime-conformance-test")), false); diff --git a/scripts/smoke-release-native-only.mjs b/scripts/smoke-release-native-only.mjs index 213967e6c..f5c9dddaf 100755 --- a/scripts/smoke-release-native-only.mjs +++ b/scripts/smoke-release-native-only.mjs @@ -93,7 +93,7 @@ if (existsSync(scenario)) { } const port = 31_000 + Math.floor(Math.random() * 1_000); -const server = spawn(binary, ["web", "--port", String(port)], { +const server = spawn(binary, ["serve", "--port", String(port)], { env, stdio: ["ignore", "pipe", "pipe"], }); @@ -109,13 +109,7 @@ try { } catch {} await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); } - if (!healthy) throw new Error("native web health smoke failed"); - if (existsSync(resolve("packages/web/dist/index.html"))) { - const response = await fetch(`http://127.0.0.1:${port}/`); - if (!response.ok || !(await response.text()).includes("<html")) { - throw new Error("native web asset smoke failed"); - } - } + if (!healthy) throw new Error("native serve health smoke failed"); } finally { server.kill("SIGTERM"); } diff --git a/scripts/test-install.sh b/scripts/test-install.sh index b9fcae86e..043173779 100644 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -56,14 +56,11 @@ case "$platform_os:$platform_arch" in esac asset="maestro-$platform" -web_asset="maestro-web-dist.tar.gz" release_dir="$fixture/v0.0.1" manifest_failure_release_dir="$fixture/v0.0.2" legacy_release_dir="$fixture/v0.0.8" unsigned_legacy_release_dir="$fixture/v0.0.9" -mkdir -p "$release_dir" "$manifest_failure_release_dir" "$legacy_release_dir" "$unsigned_legacy_release_dir" "$fixture/web-source" "$fixture/home" -printf '%s\n' '<!doctype html><title>fixture' > "$fixture/web-source/index.html" -tar -czf "$release_dir/$web_asset" -C "$fixture/web-source" . +mkdir -p "$release_dir" "$manifest_failure_release_dir" "$legacy_release_dir" "$unsigned_legacy_release_dir" "$fixture/home" write_fixture_binary() { local version="$1" @@ -71,7 +68,7 @@ write_fixture_binary() { { printf '%s\n' '#!/bin/sh' # shellcheck disable=SC2016 - printf 'if [ "$1" = "--version" ]; then printf "maestro %s\\n"; elif [ "$1" = "--print-web-root" ]; then printf "%%s\\n" "$MAESTRO_WEB_STATIC_ROOT"; else printf "fixture binary\\n"; fi\n' "$version" + printf 'if [ "$1" = "--version" ]; then printf "maestro %s\\n"; else printf "fixture binary\\n"; fi\n' "$version" } > "$target_dir/$asset" chmod 755 "$target_dir/$asset" } @@ -79,39 +76,30 @@ write_fixture_binary() { write_manifest() { local target_dir="${1:-$release_dir}" local binary_digest - local web_digest binary_digest="$(hash_file "$target_dir/$asset")" - web_digest="$(hash_file "$target_dir/$web_asset")" { printf '%s %s\n' "$binary_digest" "$asset" - printf '%s %s\n' "$web_digest" "$web_asset" } > "$target_dir/SHA256SUMS" } write_fixture_binary "0.0.1" write_manifest cp "$release_dir/$asset" "$manifest_failure_release_dir/$asset" -cp "$release_dir/$web_asset" "$manifest_failure_release_dir/$web_asset" write_fixture_binary "0.0.8" "$legacy_release_dir" -cp "$release_dir/$web_asset" "$legacy_release_dir/$web_asset" write_manifest "$legacy_release_dir" write_fixture_binary "0.0.9" "$unsigned_legacy_release_dir" -cp "$release_dir/$web_asset" "$unsigned_legacy_release_dir/$web_asset" preview_release_dir="$fixture/v0.0.3-beta.1" mkdir -p "$preview_release_dir" write_fixture_binary "0.0.3-beta.1" write_manifest cp "$release_dir/$asset" "$preview_release_dir/$asset" -cp "$release_dir/$web_asset" "$preview_release_dir/$web_asset" { printf '%s %s\n' "$(hash_file "$preview_release_dir/$asset")" "$asset" - printf '%s %s\n' "$(hash_file "$preview_release_dir/$web_asset")" "$web_asset" } > "$preview_release_dir/SHA256SUMS" alpha_release_dir="$fixture/v0.0.4-alpha.1" mkdir -p "$alpha_release_dir" write_fixture_binary "0.0.4-alpha.1" "$alpha_release_dir" -cp "$release_dir/$web_asset" "$alpha_release_dir/$web_asset" write_manifest "$alpha_release_dir" write_fixture_binary "0.0.1" write_manifest @@ -514,8 +502,6 @@ grep -q '^export MAESTRO_VERSION=' "$install_dir/maestro" || release_binary="$(find "$data_dir/releases" -type f -path '*/bin/maestro' -print -quit)" [[ -n "$release_binary" ]] || fail "versioned release binary was not staged" release_root="$(dirname "$(dirname "$release_binary")")" -[[ -f "$release_root/web/index.html" ]] || fail "web assets were not staged beside the binary" -[[ -f "$release_root/$web_asset" ]] || fail "verified web archive was not retained beside the binary" [[ -f "$release_root/install-receipt.json" ]] || fail "install receipt was not staged beside the binary" python3 - "$release_root/install-receipt.json" <<'PY' import json @@ -526,7 +512,7 @@ assert receipt["schemaVersion"] == "evalops.maestro.install-receipt.v1" assert receipt["version"] == "0.0.1" assert receipt["verified"] is False assert receipt["verification"]["artifactSha256"].startswith("sha256:") -assert receipt["verification"]["webSha256"].startswith("sha256:") +assert "webSha256" not in receipt["verification"] assert receipt["verification"]["metadataSha256"] is None assert receipt["releaseMetadataAsset"] is None PY @@ -536,17 +522,6 @@ case "$release_dir_name" in *) fail "release directory is not uniquely allocated: $release_dir_name" ;; esac -custom_web_root="$fixture/custom-web-root" -[[ "$(MAESTRO_WEB_STATIC_ROOT="$custom_web_root" "$install_dir/maestro" --print-web-root)" == "$custom_web_root" ]] || - fail "launcher overwrote an explicit MAESTRO_WEB_STATIC_ROOT" -default_web_root="$( - unset MAESTRO_WEB_STATIC_ROOT - "$install_dir/maestro" --print-web-root -)" -expected_default_web_root="$(cd "$release_root/web" && pwd -P)" -[[ "$default_web_root" == "$expected_default_web_root" ]] || - fail "launcher did not provide the bundled default web root" - runtime_version_install_dir="$fixture/runtime-version-bin" runtime_version_data_dir="$fixture/runtime-version-data" HOME="$fixture/home" \ diff --git a/scripts/update-lifecycle-contract.test.mjs b/scripts/update-lifecycle-contract.test.mjs index 5926bda98..52a46940d 100644 --- a/scripts/update-lifecycle-contract.test.mjs +++ b/scripts/update-lifecycle-contract.test.mjs @@ -55,7 +55,8 @@ test("installer and release workflow preserve receipt metadata", () => { const channelResolver = read("scripts/resolve-release-channel.mjs"); assert.match(installer, /release-metadata\.json/); assert.match(installer, /install-receipt\.json/); - assert.match(installer, /cp \"\$tmpdir\/\$web_asset\" \"\$release_dir\/\$web_asset\"/); + assert.doesNotMatch(installer, /\$web_asset/); + assert.doesNotMatch(installer, /webSha256/); assert.match(installer, /MAESTRO_STARTUP_UPDATE_STATE/); assert.match(installer, /MAESTRO_INSTALL_CHANNEL/); assert.match(installer, /MAESTRO_UPDATE_CHANNEL/); @@ -72,9 +73,11 @@ test("installer and release workflow preserve receipt metadata", () => { assert.doesNotMatch(installer, /maestro-\$\{install_channel\}-channel/); assert.match(installer, /receipt_hash_file/); assert.doesNotMatch(installer, /refusing installation without release receipt metadata/); - assert.match(updater, /restore_verified_web_tree/); + assert.doesNotMatch(updater, /restore_verified_web_tree/); + assert.doesNotMatch(updater, /web_sha256/); + assert.doesNotMatch(updater, /MAESTRO_WEB_STATIC_ROOT/); assert.match(updater, /load_verified_release_metadata/); - assert.match(updater, /Command::new\("tar"\)/); + assert.doesNotMatch(updater, /Command::new\("tar"\)/); assert.match(updater, /durability_warning/); assert.match(updater, /legacy_channel_manifest_url/); assert.match(updater, /GITHUB_RELEASES_API_URL/); @@ -103,6 +106,8 @@ test("installer and release workflow preserve receipt metadata", () => { assert.match(release, /release-metadata\.json/); assert.match(release, /create-release-channel-manifest\.mjs/); assert.match(release, /channel-manifest\.json/); + assert.doesNotMatch(release, /maestro-web-dist/); + assert.doesNotMatch(release, /packages\/web\/dist/); assert.match(channelManifest, /createPrivateKey/); assert.match(channelResolver, /alpha or beta/); }); diff --git a/test/fixtures/rust-cutover/cli-routing.json b/test/fixtures/rust-cutover/cli-routing.json index 996328cb4..4474ad11b 100644 --- a/test/fixtures/rust-cutover/cli-routing.json +++ b/test/fixtures/rust-cutover/cli-routing.json @@ -27,7 +27,7 @@ "argv": ["--mode", "headless"], "route": "native-headless" }, - { "name": "web", "argv": ["web"], "route": "native-control-plane" }, + { "name": "serve", "argv": ["serve"], "route": "native-control-plane" }, { "name": "a2a", "argv": ["a2a", "--help"], "route": "native" }, { "name": "hosted-runner", @@ -39,8 +39,8 @@ { "name": "skill", "argv": ["skill", "--help"], "route": "native" }, { "name": "update", "argv": ["update", "--help"], "route": "native" }, { - "name": "web-rejects-prompt", - "argv": ["web", "prompt"], + "name": "serve-rejects-prompt", + "argv": ["serve", "prompt"], "route": "native-control-plane", "exitCode": 1 } diff --git a/test/fixtures/rust-cutover/web-routes.json b/test/fixtures/rust-cutover/web-routes.json index 43a361796..e246fa2e3 100644 --- a/test/fixtures/rust-cutover/web-routes.json +++ b/test/fixtures/rust-cutover/web-routes.json @@ -15,7 +15,6 @@ "body": "ok" }, { "method": "GET", "path": "/readyz" }, - { "method": "GET", "path": "/.well-known/evalops/remote-runner/identity" }, { "method": "POST", "path": "/.well-known/evalops/remote-runner/drain" }, { "method": "POST", "path": "/api/platform/a2a/push" }, { "method": "GET", "path": "/api/a2a/cockpit" }, @@ -33,7 +32,6 @@ { "method": "POST", "path": "/api/headless/sessions/:id/heartbeat" }, { "method": "POST", "path": "/api/headless/sessions/:id/disconnect" }, { "method": "POST", "path": "/api/headless/sessions/:id/messages" }, - { "method": "GET", "path": "/debug/z" }, { "method": "GET", "path": "/api/files" }, { "method": "GET", "path": "/api/commands" }, { "method": "GET", "path": "/api/command-prefs" },