diff --git a/.github/actions/setup-sccache/action.yml b/.github/actions/setup-sccache/action.yml index 796f731bd..802029107 100644 --- a/.github/actions/setup-sccache/action.yml +++ b/.github/actions/setup-sccache/action.yml @@ -62,3 +62,6 @@ runs: SCCACHE_RUST_TOOLCHAIN: ${{ inputs.rust-toolchain }} SCCACHE_ALLOW_GHA_FALLBACK: ${{ inputs.allow-gha-fallback }} run: bash "${GITHUB_ACTION_PATH}/configure.sh" + - name: Link prebuilt AWS-LC when the runner has one + shell: bash + run: bash "${GITHUB_ACTION_PATH}/../../../scripts/ci/aws-lc-system-env.sh" diff --git a/.repository-projection.json b/.repository-projection.json index 966f7a75a..5ee1243b1 100644 --- a/.repository-projection.json +++ b/.repository-projection.json @@ -3,11 +3,11 @@ "projection": "deixic-code", "projectionSchemaVersion": 1, "sourceRepository": "dx-corp/mono", - "sourceSha": "de2c24eb54f696843ccd61a60bced5ca39860a6e", + "sourceSha": "73a8f29245de08bc9f819da2fe05c2bf98e0e069", "destinationRepository": "dx-corp/code", - "priorProjectedBase": "63e975db238b59b2fd281ee8cc76eb458e1f4f3f", + "priorProjectedBase": "fc3948a803dadacca5e0ab2ebed6eb571a80af7d", "definitionDigest": "cb9d429542ebb0a2de9b42a7aad60d9d8696a648ceba47c30f05c0b285ca0db7", "toolDigest": "f6d8b2a0b6f867d1db845a086635357eed196054b6a47f0695e49fdde616d6e6", - "contentDigest": "cce615766ed887df61c04a774e5af7fcccfbcc19ba9f99277f6e8273e587b95e", + "contentDigest": "ea1fbe3360c4bf405b4ede36d83c96bef235822a2db1ceec660462208518e3f3", "publicationEligible": true } diff --git a/Cargo.lock b/Cargo.lock index bff7e6dac..327488b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ dependencies = [ "octocrab", "rand 0.10.2", "regex", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "serde_yaml", @@ -3104,7 +3104,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.9", ] [[package]] @@ -3884,6 +3883,7 @@ dependencies = [ "anyhow", "ctor", "dunce", + "maestro-hosted-runner-lib", "maestro-local-host", "maestro-runtime", "maestro-runtime-contracts", @@ -3894,6 +3894,31 @@ dependencies = [ "tokio", ] +[[package]] +name = "maestro-a2a-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "chrono", + "dirs", + "dunce", + "futures", + "libc", + "maestro-a2a-ledger", + "maestro-local-host", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2 0.10.9", + "tempfile", + "tokio", + "url", + "urlencoding", + "uuid", +] + [[package]] name = "maestro-a2a-ledger" version = "0.1.0" @@ -3915,11 +3940,11 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-smithy-types", "aws-types", - "base64 0.23.1", + "base64 0.22.1", "eventsource-stream", "futures", "maestro-runtime-contracts", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "sha2 0.10.9", @@ -3930,6 +3955,39 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "maestro-cli-commands" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "chrono", + "dirs", + "dunce", + "ed25519-dalek 3.0.0", + "fd-lock", + "libc", + "maestro-ai", + "maestro-execpolicy", + "maestro-local-host", + "maestro-runtime-contracts", + "pathdiff", + "prost", + "regex", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "tempfile", + "tokio", + "toml 0.9.12+spec-1.1.0", + "url", + "urlencoding", + "uuid", + "wait-timeout", +] + [[package]] name = "maestro-codex" version = "0.1.0" @@ -3969,7 +4027,7 @@ name = "maestro-dex-host" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.23.1", + "base64 0.22.1", "dex-loop", "fd-lock", "futures-util", @@ -3996,6 +4054,45 @@ dependencies = [ "tempfile", ] +[[package]] +name = "maestro-hosted-runner-lib" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "chrono", + "clap", + "dunce", + "fd-lock", + "futures", + "libc", + "maestro-ai", + "maestro-local-host", + "maestro-runtime", + "maestro-runtime-contracts", + "prost", + "rand 0.10.2", + "rcgen", + "reqwest", + "ring", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "shlex 1.3.0", + "tempfile", + "thiserror 2.0.20", + "time", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "url", + "urlencoding", + "uuid", + "x509-parser", +] + [[package]] name = "maestro-interaction" version = "0.1.0" @@ -4006,7 +4103,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "base64 0.23.1", + "base64 0.22.1", "chrono", "clap", "dirs", @@ -4045,7 +4142,7 @@ dependencies = [ "rand 0.10.2", "rcgen", "regex", - "reqwest 0.12.28", + "reqwest", "ring", "rustls", "semver", @@ -4059,7 +4156,6 @@ dependencies = [ "thiserror 2.0.20", "time", "tokio", - "tokio-rustls", "tokio-util", "toml 0.8.23", "tracing", @@ -4072,7 +4168,6 @@ dependencies = [ "wasmtime", "wat", "windows-sys 0.61.2", - "x509-parser", "zeroize", "zip", ] @@ -4094,7 +4189,7 @@ version = "0.1.0" dependencies = [ "agent-codemode", "anyhow", - "base64 0.23.1", + "base64 0.22.1", "chrono", "dirs", "futures", @@ -4142,7 +4237,7 @@ name = "maestro-runtime-gateway" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.23.1", + "base64 0.22.1", "chrono", "dunce", "fs2", @@ -4157,7 +4252,7 @@ dependencies = [ "maestro-runtime-contracts", "pdf-extract", "prost", - "reqwest 0.12.28", + "reqwest", "serde", "serde_json", "sha1 0.11.0", @@ -4263,7 +4358,7 @@ dependencies = [ "anyhow", "arboard", "async-trait", - "base64 0.23.1", + "base64 0.22.1", "chrono", "clap", "criterion", @@ -4280,12 +4375,15 @@ dependencies = [ "kani-verifier", "keyring", "libc", + "maestro-a2a-cli", "maestro-a2a-ledger", "maestro-ai", + "maestro-cli-commands", "maestro-codex", "maestro-context", "maestro-dex-host", "maestro-execpolicy", + "maestro-hosted-runner-lib", "maestro-interaction", "maestro-local-host", "maestro-presentation", @@ -4295,6 +4393,7 @@ dependencies = [ "maestro-session", "maestro-session-history", "maestro-swarm", + "maestro-tui-kit", "maestro-ui", "maestro-ui-preview", "maestro-workspace", @@ -4313,7 +4412,7 @@ dependencies = [ "ratatui", "rcgen", "regex", - "reqwest 0.12.28", + "reqwest", "ring", "rpassword", "rustls", @@ -4355,6 +4454,48 @@ dependencies = [ "zip", ] +[[package]] +name = "maestro-tui-kit" +version = "0.1.0" +dependencies = [ + "anyhow", + "arboard", + "base64 0.22.1", + "chrono", + "crossterm", + "dirs", + "dunce", + "image", + "libc", + "maestro-ai", + "maestro-codex", + "maestro-interaction", + "maestro-local-host", + "maestro-presentation", + "maestro-sandbox", + "maestro-swarm", + "maestro-ui", + "notify", + "pretty_assertions", + "pulldown-cmark", + "rand 0.10.2", + "ratatui", + "regex", + "reqwest", + "serde", + "serde_json", + "supports-color", + "syntect", + "tempfile", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "uncurses", + "url", + "vt100", +] + [[package]] name = "maestro-ui" version = "0.1.0" @@ -4470,6 +4611,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5073,7 +5230,7 @@ dependencies = [ "bytes", "http 1.5.0", "opentelemetry", - "reqwest 0.13.5", + "reqwest", ] [[package]] @@ -5088,7 +5245,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest 0.13.5", + "reqwest", "thiserror 2.0.20", ] @@ -6310,11 +6467,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-channel", "futures-core", @@ -6327,11 +6484,13 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", @@ -6347,44 +6506,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.9", -] - -[[package]] -name = "reqwest" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" -dependencies = [ - "base64 0.23.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.5.0", - "http-body 1.1.0", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", ] [[package]] @@ -8346,9 +8467,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 4798024cb..b4a0c2cc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,11 @@ members = [ "packages/a2a-ledger-rs", "packages/execpolicy-rs", "packages/tui-rs", + "packages/tui-kit-rs", + "packages/cli-commands-rs", + "packages/a2a-cli-rs", "packages/local-host-rs", + "packages/hosted-runner-rs", "packages/sandbox-rs", "packages/workspace-rs", "packages/codex-rs", @@ -49,7 +53,7 @@ aws-sdk-bedrockruntime = { version = "1.139.0", default-features = false, featur ] } aws-smithy-types = "1.6.1" aws-types = "1.5.0" -base64 = "0.23" +base64 = "0.22" chrono = "0.4" clap = { version = "4", features = ["derive"] } ctor = "1.0.11" @@ -77,7 +81,11 @@ maestro-context = { path = "packages/context-rs" } maestro-session-history = { path = "packages/session-history-rs" } maestro-session = { path = "packages/session-rs" } maestro-tui = { path = "packages/tui-rs" } +maestro-tui-kit = { path = "packages/tui-kit-rs" } +maestro-cli-commands = { path = "packages/cli-commands-rs" } +maestro-a2a-cli = { path = "packages/a2a-cli-rs" } maestro-local-host = { path = "packages/local-host-rs" } +maestro-hosted-runner-lib = { path = "packages/hosted-runner-rs" } maestro-sandbox = { path = "packages/sandbox-rs" } maestro-workspace = { path = "packages/workspace-rs" } maestro-codex = { path = "packages/codex-rs" } @@ -97,7 +105,7 @@ prost = "0.14" prost-types = "0.14" regex = "1" rusqlite = { version = "0.32", features = ["bundled"] } -reqwest = { version = "0.12", default-features = false } +reqwest = { version = "0.13", default-features = false, features = ["query", "form", "multipart"] } rcgen = { version = "0.14", features = ["x509-parser"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } ring = "0.17" diff --git a/Dockerfile b/Dockerfile index 074ed3175..f5c85690b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,10 @@ COPY packages/context-rs ./packages/context-rs COPY packages/tui-rs ./packages/tui-rs COPY packages/dex-host-rs ./packages/dex-host-rs COPY packages/local-host-rs ./packages/local-host-rs +COPY packages/tui-kit-rs ./packages/tui-kit-rs +COPY packages/cli-commands-rs ./packages/cli-commands-rs +COPY packages/hosted-runner-rs ./packages/hosted-runner-rs +COPY packages/a2a-cli-rs ./packages/a2a-cli-rs COPY packages/sandbox-rs ./packages/sandbox-rs COPY packages/workspace-rs ./packages/workspace-rs COPY packages/codex-rs ./packages/codex-rs @@ -63,6 +67,10 @@ COPY packages/context-rs ./packages/context-rs COPY packages/tui-rs ./packages/tui-rs COPY packages/dex-host-rs ./packages/dex-host-rs COPY packages/local-host-rs ./packages/local-host-rs +COPY packages/tui-kit-rs ./packages/tui-kit-rs +COPY packages/cli-commands-rs ./packages/cli-commands-rs +COPY packages/hosted-runner-rs ./packages/hosted-runner-rs +COPY packages/a2a-cli-rs ./packages/a2a-cli-rs COPY packages/sandbox-rs ./packages/sandbox-rs COPY packages/workspace-rs ./packages/workspace-rs COPY packages/codex-rs ./packages/codex-rs diff --git a/docs/AGENT_PROFILES.md b/docs/AGENT_PROFILES.md index e03cc7daa..9de7a35ff 100644 --- a/docs/AGENT_PROFILES.md +++ b/docs/AGENT_PROFILES.md @@ -43,7 +43,7 @@ budgets: maxCostUsd: 5 ``` -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. +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-kit-rs/src/mode_cli.rs`). Runtime reviewer subagent dispatch for the canonical levels still uses the parent provider; see evalops/mono#9321. ## Routing evidence diff --git a/docs/design/TELEMETRY_COST.md b/docs/design/TELEMETRY_COST.md index 3693d4100..0a55fdc76 100644 --- a/docs/design/TELEMETRY_COST.md +++ b/docs/design/TELEMETRY_COST.md @@ -413,7 +413,7 @@ function computeSessionAnalytics(tracker: UsageTracker): SessionAnalytics { ## Display Components > **Historical note:** `src/cli-tui/...` paths below refer to the removed TypeScript -> TUI (PR #2891). Native cost/usage UI lives under `packages/tui-rs/src/usage/` and +> TUI (PR #2891). Native cost/usage UI lives under `packages/tui-kit-rs/src/usage/` and > related status components. ### TUI Footer Cost Display diff --git a/docs/doc-path-allowlist.json b/docs/doc-path-allowlist.json index d33ffd3c5..cd6957655 100644 --- a/docs/doc-path-allowlist.json +++ b/docs/doc-path-allowlist.json @@ -591,7 +591,7 @@ }, { "source": "docs/protocols/a2a-fleet-delegation.md", - "path": "packages/tui-rs/src/a2a_cli.rs", + "path": "packages/a2a-cli-rs/src/a2a_cli.rs", "reason": "[docs/protocols/a2a-fleet-delegation.md] Design/reference doc predating the Rust-only runtime migration (#3016, #3017); see the \"Status\" banner in the file for the current Rust equivalent (or an explicit \"not found\" note) before this list of stale paths." }, { diff --git a/docs/protocols/a2a-fleet-delegation.md b/docs/protocols/a2a-fleet-delegation.md index 8655f87b0..92225e21b 100644 --- a/docs/protocols/a2a-fleet-delegation.md +++ b/docs/protocols/a2a-fleet-delegation.md @@ -1,6 +1,6 @@ # A2A Fleet And Delegation -> **Status:** This document predates the Rust-only runtime migration (#3016, #3017, merged 2026-07-22), which deleted Maestro's TypeScript agent runtime and SDK. Native A2A now lives in `packages/tui-rs/src/a2a_cli.rs`; the TS test paths below have no confirmed Rust replacement. Some file paths below may be stale; they are kept for design context and updated only where a corresponding Rust module was confirmed. +> **Status:** This document predates the Rust-only runtime migration (#3016, #3017, merged 2026-07-22), which deleted Maestro's TypeScript agent runtime and SDK. Native A2A now lives in `packages/a2a-cli-rs/src/a2a_cli.rs`; the TS test paths below have no confirmed Rust replacement. Some file paths below may be stale; they are kept for design context and updated only where a corresponding Rust module was confirmed. Native A2A pairing makes peers discoverable. The fleet layer turns those peers diff --git a/docs/protocols/codex-operating-layer.json b/docs/protocols/codex-operating-layer.json index b0c5fdc3e..778011b3c 100644 --- a/docs/protocols/codex-operating-layer.json +++ b/docs/protocols/codex-operating-layer.json @@ -363,7 +363,7 @@ { "area": "remote-runner-continuity", "evidenceType": "source", - "path": "packages/local-host-rs/src/hosted_runner.rs", + "path": "packages/hosted-runner-rs/src/hosted_runner.rs", "anchors": [ "evalops.remote-runner.work-continuity.v1", "evalops.remote-runner.runtime-continuity.v1" @@ -372,7 +372,7 @@ { "area": "remote-runner-continuity", "evidenceType": "source", - "path": "packages/local-host-rs/src/hosted_runner/manifests.rs", + "path": "packages/hosted-runner-rs/src/hosted_runner/manifests.rs", "anchors": [ "RuntimeContinuityManifest", "CodexSubagentContinuityEdge", @@ -383,7 +383,7 @@ { "area": "remote-runner-continuity", "evidenceType": "test", - "path": "packages/local-host-rs/src/hosted_runner/tests.rs", + "path": "packages/hosted-runner-rs/src/hosted_runner/tests.rs", "anchors": [ "work_continuity_manifest_extracts_codex_subagent_ids", "work_continuity_manifest_keeps_spawned_and_resumed_codex_subagents_active", @@ -393,7 +393,7 @@ { "area": "remote-runner-continuity", "evidenceType": "source", - "path": "packages/local-host-rs/src/hosted_runner_cli.rs", + "path": "packages/hosted-runner-rs/src/hosted_runner_cli.rs", "anchors": [ "start_hosted_runner_cli_runtime", "AgentSupervisorHostedRunnerMessageExecutor", @@ -426,7 +426,7 @@ { "area": "remote-runner-continuity", "evidenceType": "test", - "path": "packages/local-host-rs/src/hosted_runner_cli.rs", + "path": "packages/hosted-runner-rs/src/hosted_runner_cli.rs", "anchors": [ "resolves_cli_flags_into_runner_and_supervisor_config", "starts_real_hosted_runner_with_supervised_headless_child" diff --git a/docs/protocols/codex-operating-layer.md b/docs/protocols/codex-operating-layer.md index 4a9f752a7..fa495bc38 100644 --- a/docs/protocols/codex-operating-layer.md +++ b/docs/protocols/codex-operating-layer.md @@ -64,7 +64,7 @@ npm run check:codex-operating-layer | approvals and sandbox policy | `approvals-sandbox-policy` | `src/agent/transport.ts`, `test/agent/provider-transport-provider-tools.test.ts`, `packages/runtime-gateway-rs/src/main.rs`, `docs/protocols/pending-requests.md` | | subagents | `subagents` | `src/agent/providers/codex-app-server.ts`, `test/agent/provider-transport-provider-tools.test.ts` | | multi-agent work graph | `multi-agent-workgraph` | `docs/protocols/codex-subagent-workgraph-v1.json`, `src/platform/agent-runtime-client.ts`, `src/platform/agent-registry-client.ts`, `src/agent/providers/codex-app-server.ts`, `packages/runtime-gateway-rs/src/main.rs`, `src/server/hosted-agent-runtime-progress.ts`, `test/server/hosted-agent-runtime-progress.test.ts` | -| remote runner continuity | `remote-runner-continuity` | `src/server/handlers/hosted-runner-drain.ts`, `packages/local-host-rs/src/hosted_runner.rs`, `packages/local-host-rs/src/hosted_runner/manifests.rs`, `packages/local-host-rs/src/hosted_runner_cli.rs`, `packages/local-host-rs/src/headless/messages/state.rs`, `test/server/hosted-runner-drain.test.ts` | +| remote runner continuity | `remote-runner-continuity` | `src/server/handlers/hosted-runner-drain.ts`, `packages/hosted-runner-rs/src/hosted_runner.rs`, `packages/hosted-runner-rs/src/hosted_runner/manifests.rs`, `packages/hosted-runner-rs/src/hosted_runner_cli.rs`, `packages/local-host-rs/src/headless/messages/state.rs`, `test/server/hosted-runner-drain.test.ts` | | realtime streaming | `realtime-streaming` | `src/server/handlers/runtime-app-server-ws.ts`, `test/server/runtime-app-server-ws.test.ts` | | TypeScript runtime | `typescript-runtime` | `src/agent/providers/codex-app-server.ts`, `test/agent/codex-app-server.test.ts` | | Rust runtime | `rust-control-plane` | `packages/runtime-gateway-rs/src/model_catalog.rs`, `packages/runtime-gateway-rs/src/main.rs`, `packages/runtime-gateway-rs/src/tests.rs` | diff --git a/docs/protocols/hosted-runner-contract.md b/docs/protocols/hosted-runner-contract.md index a5e2755bf..88d2a49aa 100644 --- a/docs/protocols/hosted-runner-contract.md +++ b/docs/protocols/hosted-runner-contract.md @@ -1,6 +1,6 @@ # Hosted Runner Contract -> **Status:** This document predates the Rust-only runtime migration (#3016, #3017, merged 2026-07-22), which deleted Maestro's TypeScript agent runtime and SDK. Hosted runner code now lives in `packages/local-host-rs/src/hosted_runner/` (config, handle, manifests, snapshots, rendezvous, workload identity) and `packages/local-host-rs/src/hosted_runner_cli.rs` (launch resolution and the `hosted-runner` entrypoint). The startup coordinates in [Configuration](#configuration) are executable: `packages/local-host-rs/tests/hosted_runner_startup_contract.rs` runs every case in `packages/local-host-rs/tests/fixtures/hosted-runner-startup-contract.json` through the real CLI resolver, so a documented coordinate that stops being honored fails `cargo test -p maestro-local-host`. Other file paths below may be stale; they are kept for design context and updated only where a corresponding Rust module was confirmed. +> **Status:** This document predates the Rust-only runtime migration (#3016, #3017, merged 2026-07-22), which deleted Maestro's TypeScript agent runtime and SDK. Hosted runner code now lives in `packages/hosted-runner-rs/src/hosted_runner/` (config, handle, manifests, snapshots, rendezvous, workload identity) and `packages/hosted-runner-rs/src/hosted_runner_cli.rs` (launch resolution and the `hosted-runner` entrypoint). The startup coordinates in [Configuration](#configuration) are executable: `packages/hosted-runner-rs/tests/hosted_runner_startup_contract.rs` runs every case in `packages/hosted-runner-rs/tests/fixtures/hosted-runner-startup-contract.json` through the real CLI resolver, so a documented coordinate that stops being honored fails `cargo test -p maestro-local-host`. Other file paths below may be stale; they are kept for design context and updated only where a corresponding Rust module was confirmed. Maestro hosted runners are substrate-neutral runtime pods or sandboxes that @@ -56,7 +56,7 @@ flags or environment variables, but the resolved values are the contract. | Rendezvous | `MAESTRO_RENDEZVOUS_MODE` (`inbound`, `outbound_shadow`, `outbound`); outbound modes require workload identity | optional | Deprecated environment keys are the entries of `DEPRECATED_ENV_ALIASES` in -`packages/local-host-rs/src/hosted_runner/config.rs`. The canonical key always +`packages/hosted-runner-rs/src/hosted_runner/config.rs`. The canonical key always wins when both are set; every deprecated key that carries a value produces one `hosted-runner environment key ... is deprecated` startup warning, and the deprecated keys stop being read after the next Maestro minor release. Platform @@ -484,9 +484,9 @@ the operational model. Two executable suites back this contract; both run in the Maestro CI Rust lane. -Startup and lifecycle (`cargo test -p maestro-local-host --test hosted_runner_startup_contract`): +Startup and lifecycle (`cargo test -p maestro-hosted-runner-lib --test hosted_runner_startup_contract`): -- `packages/local-host-rs/tests/fixtures/hosted-runner-startup-contract.json` +- `packages/hosted-runner-rs/tests/fixtures/hosted-runner-startup-contract.json` is the accept/reject matrix for the [Configuration](#configuration) coordinates. Each case is resolved through `hosted_runner_cli::resolve_hosted_runner_launch_config`, the same code path diff --git a/packages/a2a-cli-rs/Cargo.toml b/packages/a2a-cli-rs/Cargo.toml new file mode 100644 index 000000000..d1a596e49 --- /dev/null +++ b/packages/a2a-cli-rs/Cargo.toml @@ -0,0 +1,100 @@ +[package] +name = "maestro-a2a-cli" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "A2A, value and operating-plane command surfaces for Deixic Code" + +[lib] +name = "maestro_a2a_cli" +path = "src/lib.rs" + +[dependencies] +maestro-local-host.workspace = true +maestro-a2a-ledger.workspace = true + +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "signal", "fs", "net", "process", "time"] } +futures.workspace = true +reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls"] } +sha2.workspace = true +base64.workspace = true +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +uuid.workspace = true +regex.workspace = true +dirs.workspace = true +dunce.workspace = true +chrono.workspace = true +url = "2" +urlencoding = "2" +libc.workspace = true + +[dev-dependencies] +tempfile.workspace = true +tokio = { workspace = true, features = ["test-util"] } + +[lints.clippy] +# Enable pedantic lints for code quality +pedantic = { level = "warn", priority = -1 } + +# Allow common patterns that are intentional in this codebase +unused_async = "allow" # Async functions for interface consistency +missing_errors_doc = "allow" # Not all error conditions need docs +missing_panics_doc = "allow" # Not all panics need docs +cast_possible_truncation = "allow" # Often unavoidable in systems code +cast_precision_loss = "allow" # Acceptable for display/UI calculations +cast_sign_loss = "allow" # Used intentionally in many places +cast_lossless = "allow" # Allow explicit casts for clarity +must_use_candidate = "allow" # Too noisy for all builder patterns +doc_markdown = "allow" # Too picky about formatting +module_name_repetitions = "allow" # Common Rust naming pattern +similar_names = "allow" # Often intentional (e.g., tx/rx) +too_many_lines = "allow" # Subjective limit +struct_excessive_bools = "allow" # Sometimes needed for state machines +items_after_statements = "allow" # Useful for local helpers +wildcard_imports = "allow" # Used for re-exports +match_same_arms = "allow" # Sometimes clearer than combining +needless_pass_by_value = "allow" # Interface consistency +redundant_closure_for_method_calls = "allow" # Sometimes clearer +uninlined_format_args = "allow" # Older Rust style is still readable +if_not_else = "allow" # Sometimes clearer to negate +single_match_else = "allow" # Sometimes clearer than if-let +return_self_not_must_use = "allow" # Builder patterns returning Self +format_push_string = "allow" # push_str(&format!()) is readable +unnecessary_wraps = "allow" # Sometimes for API consistency +option_if_let_else = "allow" # match is often clearer +match_wildcard_for_single_variants = "allow" # Future-proofing matches +ref_option_ref = "allow" # &Option is sometimes cleaner +trivially_copy_pass_by_ref = "allow" # Sometimes clearer to pass by ref +fn_params_excessive_bools = "allow" # UI functions often have many flags +large_futures = "allow" # TUI app naturally has large futures +iter_without_into_iter = "allow" # Not always needed +unused_self = "allow" # Sometimes for method consistency +option_option = "allow" # Sometimes needed for optional Option +manual_let_else = "allow" # match is often clearer +significant_drop_tightening = "allow" # Sometimes clearer to drop earlier +map_unwrap_or = "allow" # Sometimes clearer than map+unwrap_or_else +implicit_hasher = "allow" # Not generalizing over hashers is fine +struct_field_names = "allow" # prefix/postfix patterns are OK +many_single_char_names = "allow" # Short names in math/UI code +assigning_clones = "allow" # Sometimes clearer than clone_from +case_sensitive_file_extension_comparisons = "allow" # Platform-specific code +cast_possible_wrap = "allow" # Necessary for libc interop +needless_continue = "allow" # Sometimes improves readability +if_then_some_else_none = "allow" # Sometimes clearer than bool::then +ref_option = "allow" # &Option is sometimes appropriate +default_trait_access = "allow" # Type::default() is clear enough +used_underscore_binding = "allow" # Sometimes used for side effects +match_bool = "allow" # if-else is sometimes clearer than match +missing_fields_in_debug = "allow" # Some fields can't be debugged (closures) +naive_bytecount = "allow" # bytecount crate not worth adding +doc_link_with_quotes = "allow" # Quotes in doc examples are OK +comparison_chain = "allow" # if-else chains are often clearer +large_stack_arrays = "allow" # Test fixtures may use large arrays + +[lints.rust] +# Unsafe code is allowed only for low-level system operations +# (libc process management, Darwin APIs, pointer arithmetic) +# Each unsafe block should have a SAFETY comment explaining why it's safe +unexpected_cfgs = { level = "allow", check-cfg = ["cfg(kani)"] } diff --git a/packages/tui-rs/src/a2a_cli/agent_registry.rs b/packages/a2a-cli-rs/src/a2a_cli/agent_registry.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/agent_registry.rs rename to packages/a2a-cli-rs/src/a2a_cli/agent_registry.rs diff --git a/packages/tui-rs/src/a2a_cli/capability_market.rs b/packages/a2a-cli-rs/src/a2a_cli/capability_market.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/capability_market.rs rename to packages/a2a-cli-rs/src/a2a_cli/capability_market.rs diff --git a/packages/tui-rs/src/a2a_cli/client.rs b/packages/a2a-cli-rs/src/a2a_cli/client.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/client.rs rename to packages/a2a-cli-rs/src/a2a_cli/client.rs diff --git a/packages/tui-rs/src/a2a_cli/client/tests.rs b/packages/a2a-cli-rs/src/a2a_cli/client/tests.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/client/tests.rs rename to packages/a2a-cli-rs/src/a2a_cli/client/tests.rs diff --git a/packages/tui-rs/src/a2a_cli/fleet_cockpit.rs b/packages/a2a-cli-rs/src/a2a_cli/fleet_cockpit.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/fleet_cockpit.rs rename to packages/a2a-cli-rs/src/a2a_cli/fleet_cockpit.rs diff --git a/packages/tui-rs/src/a2a_cli/ledger.rs b/packages/a2a-cli-rs/src/a2a_cli/ledger.rs similarity index 99% rename from packages/tui-rs/src/a2a_cli/ledger.rs rename to packages/a2a-cli-rs/src/a2a_cli/ledger.rs index df1d13880..50dc5635a 100644 --- a/packages/tui-rs/src/a2a_cli/ledger.rs +++ b/packages/a2a-cli-rs/src/a2a_cli/ledger.rs @@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use super::client::{A2ATask, A2ATaskStatus, extract_task_text, is_final_state}; -use crate::path_utils::{env_path, maestro_home_dir, resolve_env_path}; +use maestro_local_host::path_utils::{env_path, maestro_home_dir, resolve_env_path}; // Keep these values and metadata names in lockstep with // runtime-gateway-rs/src/a2a/ledger.rs. Both writers claim the same directory diff --git a/packages/tui-rs/src/a2a_cli/maestro_peer.rs b/packages/a2a-cli-rs/src/a2a_cli/maestro_peer.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/maestro_peer.rs rename to packages/a2a-cli-rs/src/a2a_cli/maestro_peer.rs diff --git a/packages/tui-rs/src/a2a_cli/mod.rs b/packages/a2a-cli-rs/src/a2a_cli/mod.rs similarity index 99% rename from packages/tui-rs/src/a2a_cli/mod.rs rename to packages/a2a-cli-rs/src/a2a_cli/mod.rs index 51d2522d3..ae8a3ebd8 100644 --- a/packages/tui-rs/src/a2a_cli/mod.rs +++ b/packages/a2a-cli-rs/src/a2a_cli/mod.rs @@ -19,7 +19,7 @@ mod fleet_cockpit; mod ledger; mod maestro_peer; mod pairing; -pub(crate) mod peer_message; +pub mod peer_message; mod registry; mod telemetry; diff --git a/packages/tui-rs/src/a2a_cli/pairing.rs b/packages/a2a-cli-rs/src/a2a_cli/pairing.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/pairing.rs rename to packages/a2a-cli-rs/src/a2a_cli/pairing.rs diff --git a/packages/tui-rs/src/a2a_cli/pairing/tests.rs b/packages/a2a-cli-rs/src/a2a_cli/pairing/tests.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/pairing/tests.rs rename to packages/a2a-cli-rs/src/a2a_cli/pairing/tests.rs diff --git a/packages/tui-rs/src/a2a_cli/peer_message.rs b/packages/a2a-cli-rs/src/a2a_cli/peer_message.rs similarity index 97% rename from packages/tui-rs/src/a2a_cli/peer_message.rs rename to packages/a2a-cli-rs/src/a2a_cli/peer_message.rs index aa758753e..7866505d7 100644 --- a/packages/tui-rs/src/a2a_cli/peer_message.rs +++ b/packages/a2a-cli-rs/src/a2a_cli/peer_message.rs @@ -10,11 +10,11 @@ use super::ledger::{ }; use super::registry::{ResolvePeerOptions, resolve_peer}; -pub(crate) const DEFAULT_PEER_MESSAGE_WAIT_MS: u64 = 300_000; -pub(crate) const DEFAULT_PEER_MESSAGE_INTERVAL_MS: u64 = 5_000; +pub const DEFAULT_PEER_MESSAGE_WAIT_MS: u64 = 300_000; +pub const DEFAULT_PEER_MESSAGE_INTERVAL_MS: u64 = 5_000; #[derive(Debug, Clone)] -pub(crate) struct PeerMessageInput { +pub struct PeerMessageInput { pub peer: Option, pub text: String, pub request_kind: String, @@ -26,7 +26,7 @@ pub(crate) struct PeerMessageInput { } #[derive(Debug, Clone)] -pub(crate) struct PendingPeerMessage { +pub struct PendingPeerMessage { pub peer: String, pub config: A2AServiceConfig, pub task: A2ATask, @@ -35,20 +35,20 @@ pub(crate) struct PendingPeerMessage { } #[derive(Debug, Clone)] -pub(crate) struct CompletedPeerMessage { +pub struct CompletedPeerMessage { pub task: A2ATask, pub ledger_warning: Option, } /// A reference to immutable bytes already owned by hosted Computer. #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ComputerHandoffPackageReference { +pub struct ComputerHandoffPackageReference { pub package_id: String, pub package_digest: String, pub target_thread_id: String, } -pub(crate) async fn start_peer_message(input: PeerMessageInput) -> Result { +pub async fn start_peer_message(input: PeerMessageInput) -> Result { let peer = resolve_peer( input.peer.as_deref(), ResolvePeerOptions { @@ -112,7 +112,7 @@ pub(crate) async fn start_peer_message(input: PeerMessageInput) -> Result, text: String, package: Option<&ComputerHandoffPackageReference>, diff --git a/packages/tui-rs/src/a2a_cli/registry.rs b/packages/a2a-cli-rs/src/a2a_cli/registry.rs similarity index 99% rename from packages/tui-rs/src/a2a_cli/registry.rs rename to packages/a2a-cli-rs/src/a2a_cli/registry.rs index 87fa30775..c63be8bdf 100644 --- a/packages/tui-rs/src/a2a_cli/registry.rs +++ b/packages/a2a-cli-rs/src/a2a_cli/registry.rs @@ -10,8 +10,8 @@ use serde_json::Value; use super::client::A2AServiceConfig; use super::pairing::{PairingPayload, normalize_a2a_base_url, peer_connection_from_payload}; -use crate::path_utils::{env_path, maestro_home_dir, resolve_env_path}; -use crate::skill_cli::write_atomic; +use maestro_local_host::path_utils::{env_path, maestro_home_dir, resolve_env_path}; +use maestro_local_host::skill_cli::write_atomic; const DEFAULT_TIMEOUT_MS: u64 = 600_000; const DEFAULT_MAX_ATTEMPTS: u64 = 1; diff --git a/packages/tui-rs/src/a2a_cli/registry/tests.rs b/packages/a2a-cli-rs/src/a2a_cli/registry/tests.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/registry/tests.rs rename to packages/a2a-cli-rs/src/a2a_cli/registry/tests.rs diff --git a/packages/tui-rs/src/a2a_cli/telemetry.rs b/packages/a2a-cli-rs/src/a2a_cli/telemetry.rs similarity index 100% rename from packages/tui-rs/src/a2a_cli/telemetry.rs rename to packages/a2a-cli-rs/src/a2a_cli/telemetry.rs diff --git a/packages/a2a-cli-rs/src/lib.rs b/packages/a2a-cli-rs/src/lib.rs new file mode 100644 index 000000000..f0e78c533 --- /dev/null +++ b/packages/a2a-cli-rs/src/lib.rs @@ -0,0 +1,10 @@ +//! A2A, value and operating-plane command-line surfaces. +//! +//! Split out of `maestro-tui` so that edits here recompile this crate +//! instead of the terminal application. + +pub mod a2a_cli; +pub mod operating_plane_cli; +pub mod operating_plane_client; +pub mod operating_plane_summary; +pub mod value_cli; diff --git a/packages/tui-rs/src/operating_plane_cli.rs b/packages/a2a-cli-rs/src/operating_plane_cli.rs similarity index 94% rename from packages/tui-rs/src/operating_plane_cli.rs rename to packages/a2a-cli-rs/src/operating_plane_cli.rs index 7bd16081d..79809615c 100644 --- a/packages/tui-rs/src/operating_plane_cli.rs +++ b/packages/a2a-cli-rs/src/operating_plane_cli.rs @@ -57,7 +57,7 @@ pub async fn run_operating_plane(args: &[String]) -> Result { if parsed.subcommand != "status" && parsed.subcommand != "inspect" { eprintln!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "Unknown operating-plane command: {0}", std::slice::from_ref(&(parsed.subcommand)) ) @@ -125,7 +125,7 @@ pub fn parse_operating_plane_args(args: &[String]) -> Result Ok(Some(false)), _ => bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("{0} must be true or false", &[(name).to_string()]) ), }, @@ -248,14 +248,14 @@ fn flag_non_negative_int( Some(value) => Ok(Some(value)), None => bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("{0} must be a non-negative integer", &[(name).to_string()]) ), } } fn operating_plane_help_text() -> &'static str { - crate::localization::cli_locale().translate("Usage: maestro operating-plane status [filters]\n\nFilters:\n--thread-id Slack/channel thread id\n--artifact-id Runtime artifact ref id\n--auth-subject Gateway-authenticated subject\n--trace-id Trace id\n--session-id Maestro/session id\n--run-id Agent runtime run id\n--workspace-id Workspace id\n--audience agent, channel, audit, system, ...\n--include-gates= Include release/replay gates\n--limit Maximum runs\n--json Emit safe summary JSON") + maestro_local_host::localization::cli_locale().translate("Usage: maestro operating-plane status [filters]\n\nFilters:\n--thread-id Slack/channel thread id\n--artifact-id Runtime artifact ref id\n--auth-subject Gateway-authenticated subject\n--trace-id Trace id\n--session-id Maestro/session id\n--run-id Agent runtime run id\n--workspace-id Workspace id\n--audience agent, channel, audit, system, ...\n--include-gates= Include release/replay gates\n--limit Maximum runs\n--json Emit safe summary JSON") } #[cfg(test)] diff --git a/packages/tui-rs/src/operating_plane_client.rs b/packages/a2a-cli-rs/src/operating_plane_client.rs similarity index 99% rename from packages/tui-rs/src/operating_plane_client.rs rename to packages/a2a-cli-rs/src/operating_plane_client.rs index 0556dc22e..f1220f8b7 100644 --- a/packages/tui-rs/src/operating_plane_client.rs +++ b/packages/a2a-cli-rs/src/operating_plane_client.rs @@ -224,7 +224,9 @@ pub fn resolve_operating_plane_service_config() -> Option let organization_id = get_env_value(ORGANIZATION_ENV_VARS); let token = get_env_value(TOKEN_ENV_VARS); let stored = if organization_id.is_none() || token.is_none() { - crate::init_cli::load_evalops_snapshot().ok().flatten() + maestro_local_host::init_cli::load_evalops_snapshot() + .ok() + .flatten() } else { None }; diff --git a/packages/tui-rs/src/operating_plane_summary.rs b/packages/a2a-cli-rs/src/operating_plane_summary.rs similarity index 100% rename from packages/tui-rs/src/operating_plane_summary.rs rename to packages/a2a-cli-rs/src/operating_plane_summary.rs diff --git a/packages/tui-rs/src/value_cli.rs b/packages/a2a-cli-rs/src/value_cli.rs similarity index 97% rename from packages/tui-rs/src/value_cli.rs rename to packages/a2a-cli-rs/src/value_cli.rs index c513062e0..6cfe9a5c0 100644 --- a/packages/tui-rs/src/value_cli.rs +++ b/packages/a2a-cli-rs/src/value_cli.rs @@ -19,7 +19,7 @@ use crate::a2a_cli::{ TaskLedgerEntry, get_task_ledger_path, is_action_required_state, is_completed_state, is_failed_state, is_final_state, load_task_ledger, }; -use crate::session::{SessionInfo, SessionManager}; +use maestro_local_host::session::{SessionInfo, SessionManager}; const CUSTOMER_VALUE_MANIFEST_VERSION: &str = "maestro.customer-value.manifest.v1"; const NATIVE_REPORT_SCHEMA: &str = "maestro.customer-value.native.v1"; @@ -285,7 +285,7 @@ pub async fn run_value(args: &[String]) -> Result { if let Some(artifacts) = &artifacts { println!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "\nSaved value artifact manifest: {0}", std::slice::from_ref(&(artifacts.manifest_path)) ) @@ -298,28 +298,28 @@ pub async fn run_value(args: &[String]) -> Result { println!(); println!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "Saved value artifacts: {0}", std::slice::from_ref(&(artifacts.output_dir)) ) ); println!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "Manifest: {0}", std::slice::from_ref(&(artifacts.manifest_path)) ) ); println!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "Report JSON: {0}", std::slice::from_ref(&(artifacts.report_json_path)) ) ); println!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "Report Markdown: {0}", std::slice::from_ref(&(artifacts.report_markdown_path)) ) @@ -332,7 +332,7 @@ pub async fn run_value(args: &[String]) -> Result { } fn value_help() -> &'static str { - crate::localization::cli_locale().translate("Usage: maestro value [today|yesterday|week|7d|month|30d|all] [options]\n\nOptions:\n--format json|md|text Output format (default: text)\n--write Persist report JSON, Markdown, and manifest\n--output-dir Artifact directory (default: ~/.maestro/value-reports)\n--session-dir Override session root (lists .jsonl files in this directory)\n--a2a-tasks Override A2A task ledger path (default: ~/.maestro/a2a/tasks.json)\n--help Show this help\n\nNative report uses SessionManager session stats plus the A2A task ledger.\nResidual gaps vs the full TypeScript report are listed in JSON residualGaps.") + maestro_local_host::localization::cli_locale().translate("Usage: maestro value [today|yesterday|week|7d|month|30d|all] [options]\n\nOptions:\n--format json|md|text Output format (default: text)\n--write Persist report JSON, Markdown, and manifest\n--output-dir Artifact directory (default: ~/.maestro/value-reports)\n--session-dir Override session root (lists .jsonl files in this directory)\n--a2a-tasks Override A2A task ledger path (default: ~/.maestro/a2a/tasks.json)\n--help Show this help\n\nNative report uses SessionManager session stats plus the A2A task ledger.\nResidual gaps vs the full TypeScript report are listed in JSON residualGaps.") } fn parse_args(args: &[String]) -> Result { @@ -354,7 +354,7 @@ fn parse_args(args: &[String]) -> Result { let Some(value) = args.get(i) else { bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("--format requires a value (json|md|text)", &[]) ); }; @@ -368,7 +368,7 @@ fn parse_args(args: &[String]) -> Result { let Some(value) = args.get(i) else { bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("--output-dir requires a path", &[]) ); }; @@ -382,7 +382,7 @@ fn parse_args(args: &[String]) -> Result { let Some(value) = args.get(i) else { bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("--session-dir requires a path", &[]) ); }; @@ -396,7 +396,7 @@ fn parse_args(args: &[String]) -> Result { let Some(value) = args.get(i) else { bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("--a2a-tasks requires a path", &[]) ); }; @@ -411,14 +411,14 @@ fn parse_args(args: &[String]) -> Result { other if other.starts_with('-') => { bail!( "{}", - crate::localization::cli_locale() + maestro_local_host::localization::cli_locale() .format("unknown value flag: {0}", &[(other).to_string()]) ); } other => { bail!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "unknown value period: {0} (use today|yesterday|week|7d|month|30d|all)", &[(other).to_string()] ) @@ -437,7 +437,7 @@ fn normalize_format(value: &str) -> Result { "text" | "txt" | "plain" => Ok("text".to_string()), other => bail!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "unsupported --format: {0} (use json|md|text)", &[(other).to_string()] ) @@ -1155,7 +1155,7 @@ fn format_text(report: &ValueReport) -> String { format!("Trust cards: {}", report.summary.trust_card_count), format!("Messages: {}", report.summary.message_count), format!("Tool calls: {}", report.summary.tool_call_count), - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "Usage: {0} tokens, ${1}", &[ (report.summary.total_tokens).to_string(), @@ -1546,7 +1546,7 @@ fn unique_base_name(output_dir: &Path, base_name: &str) -> Result { } bail!( "{}", - crate::localization::cli_locale().format( + maestro_local_host::localization::cli_locale().format( "could not allocate unique value artifact base name in {0}", &[(output_dir.display()).to_string()] ) @@ -1615,7 +1615,7 @@ mod tests { path: PathBuf::from("/tmp/s.jsonl"), cwd: "/tmp".into(), model: "test".into(), - thinking_level: crate::session::ThinkingLevel::Medium, + thinking_level: maestro_local_host::session::ThinkingLevel::Medium, timestamp: "not-a-date".into(), stats: Default::default(), meta: None, diff --git a/packages/ai-rs/Cargo.toml b/packages/ai-rs/Cargo.toml index 5794c8cd1..218a9ae92 100644 --- a/packages/ai-rs/Cargo.toml +++ b/packages/ai-rs/Cargo.toml @@ -20,7 +20,7 @@ aws-types = { workspace = true, optional = true } base64.workspace = true futures.workspace = true maestro-runtime-contracts.workspace = true -reqwest = { workspace = true, features = ["json", "stream", "rustls-tls"] } +reqwest = { workspace = true, features = ["json", "stream", "rustls"] } serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "time", "process", "io-util"] } diff --git a/packages/ai-rs/src/openai.rs b/packages/ai-rs/src/openai.rs index b1e00a385..39cd5c9ec 100644 --- a/packages/ai-rs/src/openai.rs +++ b/packages/ai-rs/src/openai.rs @@ -6834,39 +6834,6 @@ data: {"type":"response.completed","response":{"output":[{"type":"message","cont assert_eq!(direct.response_open_timeout(), None); } - #[tokio::test] - async fn local_model_response_open_timeout_stops_stalled_headers() { - use std::io::{Read, Write}; - use std::net::TcpListener; - - let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock local model"); - let address = listener.local_addr().expect("mock local model address"); - let server = std::thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("accept local model request"); - let mut request = [0_u8; 4096]; - let _ = stream.read(&mut request).expect("read local model request"); - std::thread::sleep(std::time::Duration::from_millis(150)); - let _ = stream - .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); - }); - - let request = reqwest::Client::new().get(format!("http://{address}/chat/completions")); - let error = send_with_response_open_timeout( - request, - Some(std::time::Duration::from_millis(25)), - "local model", - ) - .await - .expect_err("stalled local model response must time out"); - assert!( - error - .to_string() - .contains("local model response headers timed out"), - "unexpected error: {error:#}" - ); - server.join().expect("mock local model server"); - } - #[tokio::test] async fn managed_gateway_open_waits_for_its_provider_attempt_to_finish() { use tokio::io::{AsyncReadExt, AsyncWriteExt}; diff --git a/packages/ai-rs/src/openai/response_open_timeout_tests.rs b/packages/ai-rs/src/openai/response_open_timeout_tests.rs index 8d6ebd69f..211b53957 100644 --- a/packages/ai-rs/src/openai/response_open_timeout_tests.rs +++ b/packages/ai-rs/src/openai/response_open_timeout_tests.rs @@ -141,3 +141,79 @@ async fn managed_gateway_response_open_timeout_does_not_cover_stream_body() { server.await.expect("mock gateway server"); clock.abort(); } + +async fn join_mock_local_model_server(server: std::thread::JoinHandle<()>) { + tokio::task::spawn_blocking(move || server.join()) + .await + .expect("mock local model join task") + .expect("mock local model server"); +} + +#[tokio::test] +async fn local_model_mock_server_join_keeps_request_io_running() { + use std::io::Read; + use std::net::TcpListener; + use tokio::io::AsyncWriteExt; + + let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock local model"); + let address = listener.local_addr().expect("mock local model address"); + let (accepted, received) = tokio::sync::oneshot::channel(); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept local model request"); + accepted.send(()).expect("notify accepted request"); + let mut request = [0_u8; 1]; + assert_eq!( + stream.read(&mut request).expect("read local model request"), + 1 + ); + assert_eq!(request, *b"X"); + }); + + let mut stream = tokio::net::TcpStream::connect(address) + .await + .expect("connect local model"); + received.await.expect("server accepted local model request"); + let writer = tokio::spawn(async move { + tokio::task::yield_now().await; + stream + .write_all(b"X") + .await + .expect("write local model request"); + }); + + join_mock_local_model_server(server).await; + writer.await.expect("local model request writer"); +} + +#[tokio::test] +async fn local_model_response_open_timeout_stops_stalled_headers() { + use std::io::{Read, Write}; + use std::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock local model"); + let address = listener.local_addr().expect("mock local model address"); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept local model request"); + let mut request = [0_u8; 4096]; + let _ = stream.read(&mut request).expect("read local model request"); + std::thread::sleep(std::time::Duration::from_millis(150)); + let _ = + stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + }); + + let request = reqwest::Client::new().get(format!("http://{address}/chat/completions")); + let error = send_with_response_open_timeout( + request, + Some(std::time::Duration::from_millis(25)), + "local model", + ) + .await + .expect_err("stalled local model response must time out"); + assert!( + error + .to_string() + .contains("local model response headers timed out"), + "unexpected error: {error:#}" + ); + join_mock_local_model_server(server).await; +} diff --git a/packages/ambient-agent-rs/Cargo.toml b/packages/ambient-agent-rs/Cargo.toml index d34a5b80a..0f0259050 100644 --- a/packages/ambient-agent-rs/Cargo.toml +++ b/packages/ambient-agent-rs/Cargo.toml @@ -22,7 +22,7 @@ tokio-stream.workspace = true futures.workspace = true # HTTP client for GitHub API and AI -reqwest = { workspace = true, features = ["json", "stream", "rustls-tls"] } +reqwest = { workspace = true, features = ["json", "stream", "rustls"] } async-nats = "0.48" # Serialization diff --git a/packages/cli-commands-rs/Cargo.toml b/packages/cli-commands-rs/Cargo.toml new file mode 100644 index 000000000..71b598744 --- /dev/null +++ b/packages/cli-commands-rs/Cargo.toml @@ -0,0 +1,108 @@ +[package] +name = "maestro-cli-commands" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Update, memory, search, plugins, MCP config, config, context, ACP, remote, cloud and thread commands for Deixic Code" + +[lib] +name = "maestro_cli_commands" +path = "src/lib.rs" + +[dependencies] +maestro-local-host.workspace = true +maestro-execpolicy.workspace = true +maestro-runtime-contracts.workspace = true + +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "signal", "fs", "net", "process", "time"] } +reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls"] } +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +toml = "0.9" +tempfile.workspace = true +dunce.workspace = true +chrono.workspace = true +urlencoding = "2" +dirs.workspace = true +base64.workspace = true +sha2.workspace = true +regex.workspace = true +uuid.workspace = true +url = "2" +wait-timeout.workspace = true +semver = "1" +pathdiff = "0.2" +fd-lock.workspace = true +ed25519-dalek = { version = "3", default-features = false } +libc.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util"] } +maestro-ai = { workspace = true, features = ["test-support"] } +prost = "0.14" + +[lints.clippy] +# Enable pedantic lints for code quality +pedantic = { level = "warn", priority = -1 } + +# Allow common patterns that are intentional in this codebase +unused_async = "allow" # Async functions for interface consistency +missing_errors_doc = "allow" # Not all error conditions need docs +missing_panics_doc = "allow" # Not all panics need docs +cast_possible_truncation = "allow" # Often unavoidable in systems code +cast_precision_loss = "allow" # Acceptable for display/UI calculations +cast_sign_loss = "allow" # Used intentionally in many places +cast_lossless = "allow" # Allow explicit casts for clarity +must_use_candidate = "allow" # Too noisy for all builder patterns +doc_markdown = "allow" # Too picky about formatting +module_name_repetitions = "allow" # Common Rust naming pattern +similar_names = "allow" # Often intentional (e.g., tx/rx) +too_many_lines = "allow" # Subjective limit +struct_excessive_bools = "allow" # Sometimes needed for state machines +items_after_statements = "allow" # Useful for local helpers +wildcard_imports = "allow" # Used for re-exports +match_same_arms = "allow" # Sometimes clearer than combining +needless_pass_by_value = "allow" # Interface consistency +redundant_closure_for_method_calls = "allow" # Sometimes clearer +uninlined_format_args = "allow" # Older Rust style is still readable +if_not_else = "allow" # Sometimes clearer to negate +single_match_else = "allow" # Sometimes clearer than if-let +return_self_not_must_use = "allow" # Builder patterns returning Self +format_push_string = "allow" # push_str(&format!()) is readable +unnecessary_wraps = "allow" # Sometimes for API consistency +option_if_let_else = "allow" # match is often clearer +match_wildcard_for_single_variants = "allow" # Future-proofing matches +ref_option_ref = "allow" # &Option is sometimes cleaner +trivially_copy_pass_by_ref = "allow" # Sometimes clearer to pass by ref +fn_params_excessive_bools = "allow" # UI functions often have many flags +large_futures = "allow" # TUI app naturally has large futures +iter_without_into_iter = "allow" # Not always needed +unused_self = "allow" # Sometimes for method consistency +option_option = "allow" # Sometimes needed for optional Option +manual_let_else = "allow" # match is often clearer +significant_drop_tightening = "allow" # Sometimes clearer to drop earlier +map_unwrap_or = "allow" # Sometimes clearer than map+unwrap_or_else +implicit_hasher = "allow" # Not generalizing over hashers is fine +struct_field_names = "allow" # prefix/postfix patterns are OK +many_single_char_names = "allow" # Short names in math/UI code +assigning_clones = "allow" # Sometimes clearer than clone_from +case_sensitive_file_extension_comparisons = "allow" # Platform-specific code +cast_possible_wrap = "allow" # Necessary for libc interop +needless_continue = "allow" # Sometimes improves readability +if_then_some_else_none = "allow" # Sometimes clearer than bool::then +ref_option = "allow" # &Option is sometimes appropriate +default_trait_access = "allow" # Type::default() is clear enough +used_underscore_binding = "allow" # Sometimes used for side effects +match_bool = "allow" # if-else is sometimes clearer than match +missing_fields_in_debug = "allow" # Some fields can't be debugged (closures) +naive_bytecount = "allow" # bytecount crate not worth adding +doc_link_with_quotes = "allow" # Quotes in doc examples are OK +comparison_chain = "allow" # if-else chains are often clearer +large_stack_arrays = "allow" # Test fixtures may use large arrays + +[lints.rust] +# Unsafe code is allowed only for low-level system operations +# (libc process management, Darwin APIs, pointer arithmetic) +# Each unsafe block should have a SAFETY comment explaining why it's safe +unexpected_cfgs = { level = "allow", check-cfg = ["cfg(kani)"] } diff --git a/packages/tui-rs/src/acp_cli.rs b/packages/cli-commands-rs/src/acp_cli.rs similarity index 100% rename from packages/tui-rs/src/acp_cli.rs rename to packages/cli-commands-rs/src/acp_cli.rs diff --git a/packages/tui-rs/src/cloud_cli.rs b/packages/cli-commands-rs/src/cloud_cli.rs similarity index 100% rename from packages/tui-rs/src/cloud_cli.rs rename to packages/cli-commands-rs/src/cloud_cli.rs diff --git a/packages/tui-rs/src/config_cli.rs b/packages/cli-commands-rs/src/config_cli.rs similarity index 100% rename from packages/tui-rs/src/config_cli.rs rename to packages/cli-commands-rs/src/config_cli.rs diff --git a/packages/tui-rs/src/context_cli.rs b/packages/cli-commands-rs/src/context_cli.rs similarity index 99% rename from packages/tui-rs/src/context_cli.rs rename to packages/cli-commands-rs/src/context_cli.rs index 98efd4698..d9646c4ef 100644 --- a/packages/tui-rs/src/context_cli.rs +++ b/packages/cli-commands-rs/src/context_cli.rs @@ -1316,7 +1316,7 @@ fn finalize_context_manifest(mut manifest: UnifiedContextManifest) -> UnifiedCon } /// Build the exact redacted context manifest persisted in a native session header. -pub(crate) fn load_unified_context_manifest_json(cwd: &Path) -> Result { +pub fn load_unified_context_manifest_json(cwd: &Path) -> Result { serde_json::to_value(load_unified_context_manifest(cwd)?) .context("failed to serialize unified context manifest") } diff --git a/packages/tui-rs/src/deixic_operating_client.rs b/packages/cli-commands-rs/src/deixic_operating_client.rs similarity index 99% rename from packages/tui-rs/src/deixic_operating_client.rs rename to packages/cli-commands-rs/src/deixic_operating_client.rs index 28b49c9de..20ef79352 100644 --- a/packages/tui-rs/src/deixic_operating_client.rs +++ b/packages/cli-commands-rs/src/deixic_operating_client.rs @@ -13,7 +13,7 @@ const PLATFORM_BASE_URL_ENV: &[&str] = &[ "EVALOPS_BASE_URL", ]; -pub(crate) mod request_type { +pub mod request_type { pub const APPROVAL: &str = "approval"; pub const USER_INPUT: &str = "user_input"; } diff --git a/packages/tui-rs/src/evidence.rs b/packages/cli-commands-rs/src/evidence.rs similarity index 96% rename from packages/tui-rs/src/evidence.rs rename to packages/cli-commands-rs/src/evidence.rs index 933b4628d..e34cbc28a 100644 --- a/packages/tui-rs/src/evidence.rs +++ b/packages/cli-commands-rs/src/evidence.rs @@ -8,7 +8,7 @@ use sha2::{Digest, Sha256}; /// Object keys are sorted recursively before serialization so the digest does /// not depend on insertion order. Callers remain responsible for removing /// secret-derived values before hashing. -pub(crate) fn canonical_json_sha256(value: &Value) -> String { +pub fn canonical_json_sha256(value: &Value) -> String { let canonical = canonical_json(value); let bytes = serde_json::to_vec(&canonical).expect("JSON values are always serializable"); format!("sha256:{:x}", Sha256::digest(bytes)) diff --git a/packages/tui-rs/src/import_claude_cli.rs b/packages/cli-commands-rs/src/import_claude_cli.rs similarity index 100% rename from packages/tui-rs/src/import_claude_cli.rs rename to packages/cli-commands-rs/src/import_claude_cli.rs diff --git a/packages/cli-commands-rs/src/lib.rs b/packages/cli-commands-rs/src/lib.rs new file mode 100644 index 000000000..b6928aa92 --- /dev/null +++ b/packages/cli-commands-rs/src/lib.rs @@ -0,0 +1,27 @@ +//! Command-line surfaces split out of `maestro-tui`: update, memory, search, +//! plugins, MCP config, config, context, ACP, remote, cloud and thread commands. +//! +//! Edits here recompile this crate instead of the terminal application. + +pub(crate) use maestro_execpolicy as execpolicy; +pub(crate) use maestro_local_host::{ + config, headless, init_cli, local_models, localization, mcp, model_catalog, orb_connection, + output_sanitize, path_utils, plugins, session, skill_cli, tools, +}; + +pub mod acp_cli; +pub mod cloud_cli; +pub mod config_cli; +pub mod context_cli; +pub mod deixic_operating_client; +pub mod evidence; +pub mod import_claude_cli; +pub mod mcp_catalog; +pub mod mcp_config_cli; +pub mod memory_cli; +pub mod plugins_cli; +pub mod remote_attach; +pub mod remote_cli; +pub mod search_cli; +pub mod thread_cli; +pub mod update_cli; diff --git a/packages/tui-rs/src/mcp_catalog.rs b/packages/cli-commands-rs/src/mcp_catalog.rs similarity index 100% rename from packages/tui-rs/src/mcp_catalog.rs rename to packages/cli-commands-rs/src/mcp_catalog.rs diff --git a/packages/tui-rs/src/mcp_config_cli.rs b/packages/cli-commands-rs/src/mcp_config_cli.rs similarity index 99% rename from packages/tui-rs/src/mcp_config_cli.rs rename to packages/cli-commands-rs/src/mcp_config_cli.rs index db03d7fdc..8cc9b5b5c 100644 --- a/packages/tui-rs/src/mcp_config_cli.rs +++ b/packages/cli-commands-rs/src/mcp_config_cli.rs @@ -22,7 +22,7 @@ pub async fn apply_mcp_config_async(args: &[String]) -> Result { apply_mcp_config_async_with_output(args, true).await } -pub(crate) async fn apply_mcp_config_async_quiet(args: &[String]) -> Result { +pub async fn apply_mcp_config_async_quiet(args: &[String]) -> Result { apply_mcp_config_async_with_output(args, false).await } diff --git a/packages/tui-rs/src/memory_cli.rs b/packages/cli-commands-rs/src/memory_cli.rs similarity index 99% rename from packages/tui-rs/src/memory_cli.rs rename to packages/cli-commands-rs/src/memory_cli.rs index bf51cc50f..ebd24618c 100644 --- a/packages/tui-rs/src/memory_cli.rs +++ b/packages/cli-commands-rs/src/memory_cli.rs @@ -432,7 +432,7 @@ fn format_local_account_memory_status(snapshot: Option<&EvalOpsCredentialSnapsho } } -pub(crate) fn local_account_memory_status() -> String { +pub fn local_account_memory_status() -> String { match load_evalops_snapshot() { Ok(snapshot) => format_local_account_memory_status(snapshot.as_ref()), Err(error) => format!("Account memory: not working. {error}"), diff --git a/packages/tui-rs/src/plugins_cli.rs b/packages/cli-commands-rs/src/plugins_cli.rs similarity index 100% rename from packages/tui-rs/src/plugins_cli.rs rename to packages/cli-commands-rs/src/plugins_cli.rs diff --git a/packages/tui-rs/src/remote_attach.rs b/packages/cli-commands-rs/src/remote_attach.rs similarity index 100% rename from packages/tui-rs/src/remote_attach.rs rename to packages/cli-commands-rs/src/remote_attach.rs diff --git a/packages/tui-rs/src/remote_cli.rs b/packages/cli-commands-rs/src/remote_cli.rs similarity index 93% rename from packages/tui-rs/src/remote_cli.rs rename to packages/cli-commands-rs/src/remote_cli.rs index 7e0c2a2b3..8c4ffbe64 100644 --- a/packages/tui-rs/src/remote_cli.rs +++ b/packages/cli-commands-rs/src/remote_cli.rs @@ -22,24 +22,24 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value, json}; use uuid::Uuid; -pub(crate) const DEFAULT_BASE_URL: &str = "https://runner.evalops.dev"; +pub const DEFAULT_BASE_URL: &str = "https://runner.evalops.dev"; const DEFAULT_TIMEOUT_MS: u64 = 5_000; const DEFAULT_MAX_ATTEMPTS: usize = 2; const VERIFY_ERROR_BODY_MAX_CHARS: usize = 512; const DEFAULT_WAIT_TIMEOUT_MS: u64 = 5 * 60 * 1000; const DEFAULT_POLL_MS: u64 = 5_000; const CONNECT_VERSION: &str = "1"; -pub(crate) const SERVICE: &str = "remote runner service"; +pub const SERVICE: &str = "remote runner service"; const SERVICE_PATH: &str = "/remoterunner.v1.RemoteRunnerService"; -pub(crate) const CREATE_PATH: &str = "/remoterunner.v1.RemoteRunnerService/CreateRunnerSession"; -pub(crate) const GET_PATH: &str = "/remoterunner.v1.RemoteRunnerService/GetRunnerSession"; -pub(crate) const LIST_PATH: &str = "/remoterunner.v1.RemoteRunnerService/ListRunnerSessions"; -pub(crate) const STOP_PATH: &str = "/remoterunner.v1.RemoteRunnerService/StopRunnerSession"; +pub const CREATE_PATH: &str = "/remoterunner.v1.RemoteRunnerService/CreateRunnerSession"; +pub const GET_PATH: &str = "/remoterunner.v1.RemoteRunnerService/GetRunnerSession"; +pub const LIST_PATH: &str = "/remoterunner.v1.RemoteRunnerService/ListRunnerSessions"; +pub const STOP_PATH: &str = "/remoterunner.v1.RemoteRunnerService/StopRunnerSession"; const EXTEND_PATH: &str = "/remoterunner.v1.RemoteRunnerService/ExtendRunnerSession"; -pub(crate) const MINT_PATH: &str = "/remoterunner.v1.RemoteRunnerService/MintAttachToken"; +pub const MINT_PATH: &str = "/remoterunner.v1.RemoteRunnerService/MintAttachToken"; const REVOKE_PATH: &str = "/remoterunner.v1.RemoteRunnerService/RevokeAttachToken"; -pub(crate) const EVENTS_PATH: &str = "/remoterunner.v1.RemoteRunnerService/ListRunnerSessionEvents"; +pub const EVENTS_PATH: &str = "/remoterunner.v1.RemoteRunnerService/ListRunnerSessionEvents"; const STATUS_PATH: &str = "/remoterunner.v1.RemoteRunnerService/GetStatus"; const BASE_URL_ENV: &[&str] = &[ @@ -100,14 +100,14 @@ Notes: On a TTY (without --json/--print-env), attach mints a token and opens an interactive REPL. Otherwise it prints transport env handoff instructions."; -pub(crate) const STATE_RUNNING: &str = "RUNNER_SESSION_STATE_RUNNING"; -pub(crate) const STATE_IDLE: &str = "RUNNER_SESSION_STATE_IDLE"; -pub(crate) const STATE_STOPPED: &str = "RUNNER_SESSION_STATE_STOPPED"; -pub(crate) const STATE_EXPIRED: &str = "RUNNER_SESSION_STATE_EXPIRED"; -pub(crate) const STATE_FAILED: &str = "RUNNER_SESSION_STATE_FAILED"; -pub(crate) const STATE_LOST: &str = "RUNNER_SESSION_STATE_LOST"; -pub(crate) const ROLE_VIEWER: &str = "RUNNER_ATTACH_ROLE_VIEWER"; -pub(crate) const ROLE_CONTROLLER: &str = "RUNNER_ATTACH_ROLE_CONTROLLER"; +pub const STATE_RUNNING: &str = "RUNNER_SESSION_STATE_RUNNING"; +pub const STATE_IDLE: &str = "RUNNER_SESSION_STATE_IDLE"; +pub const STATE_STOPPED: &str = "RUNNER_SESSION_STATE_STOPPED"; +pub const STATE_EXPIRED: &str = "RUNNER_SESSION_STATE_EXPIRED"; +pub const STATE_FAILED: &str = "RUNNER_SESSION_STATE_FAILED"; +pub const STATE_LOST: &str = "RUNNER_SESSION_STATE_LOST"; +pub const ROLE_VIEWER: &str = "RUNNER_ATTACH_ROLE_VIEWER"; +pub const ROLE_CONTROLLER: &str = "RUNNER_ATTACH_ROLE_CONTROLLER"; const ROLE_ADMIN: &str = "RUNNER_ATTACH_ROLE_ADMIN"; #[derive(Debug, Clone)] @@ -123,72 +123,72 @@ enum FlagVal { } #[derive(Debug, Clone)] -pub(crate) struct ClientOpts { - pub(crate) base_url: Option, - pub(crate) token: Option, - pub(crate) organization_id: Option, - pub(crate) workspace_id: Option, +pub struct ClientOpts { + pub base_url: Option, + pub token: Option, + pub organization_id: Option, + pub workspace_id: Option, } #[derive(Debug, Clone)] -pub(crate) struct Config { - pub(crate) base_url: String, - pub(crate) token: String, - pub(crate) organization_id: String, - pub(crate) workspace_id: Option, - pub(crate) timeout_ms: u64, - pub(crate) max_attempts: usize, +pub struct Config { + pub base_url: String, + pub token: String, + pub organization_id: String, + pub workspace_id: Option, + pub timeout_ms: u64, + pub max_attempts: usize, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub(crate) struct Session { - pub(crate) id: String, +pub struct Session { + pub id: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) workspace_id: Option, + pub workspace_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) state: Option, + pub state: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) runner_profile: Option, + pub runner_profile: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) maestro_session_id: Option, + pub maestro_session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) repo_url: Option, + pub repo_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) branch: Option, + pub branch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) expires_at: Option, + pub expires_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) idle_expires_at: Option, + pub idle_expires_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) stop_reason: Option, + pub stop_reason: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub(crate) struct Event { +pub struct Event { #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) sequence: Option, + pub sequence: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) event_type: Option, + pub event_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) occurred_at: Option, + pub occurred_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub(crate) struct AttachToken { - pub(crate) id: String, +pub struct AttachToken { + pub id: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) expires_at: Option, + pub expires_at: Option, } #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct Minted { - pub(crate) token: AttachToken, - pub(crate) token_secret: String, - pub(crate) gateway_base_url: String, +pub struct Minted { + pub token: AttachToken, + pub token_secret: String, + pub gateway_base_url: String, } pub async fn run_remote(args: &[String]) -> Result { @@ -699,7 +699,7 @@ async fn cmd_target(o: &Opts) -> Result<()> { Ok(()) } -pub(crate) async fn mint( +pub async fn mint( session_id: &str, roles: Vec, ttl_minutes: u64, @@ -736,13 +736,13 @@ pub(crate) async fn mint( }) } -pub(crate) async fn get_session(id: &str, co: &ClientOpts) -> Result { +pub async fn get_session(id: &str, co: &ClientOpts) -> Result { let config = require_config(co)?; let payload = post(&config, GET_PATH, json!({"sessionId": id})).await?; require_session(&payload) } -pub(crate) async fn wait_ready( +pub async fn wait_ready( id: &str, co: &ClientOpts, timeout_ms: u64, @@ -926,7 +926,7 @@ fn ensure_verify_disconnect_success(status: StatusCode, body: &str) -> Result<() ) } -pub(crate) async fn post(config: &Config, path: &str, body: Value) -> Result { +pub async fn post(config: &Config, path: &str, body: Value) -> Result { let client = Client::builder() .timeout(Duration::from_millis(config.timeout_ms)) .build() @@ -993,13 +993,13 @@ fn retryable(status: StatusCode) -> bool { matches!(status.as_u16(), 408 | 429) || status.is_server_error() } -pub(crate) fn require_config(o: &ClientOpts) -> Result { +pub fn require_config(o: &ClientOpts) -> Result { resolve_config(o)?.ok_or_else(|| { anyhow!("{}", crate::localization::cli_locale().format("Remote runner requires EvalOps organization and access token. Set MAESTRO_REMOTE_RUNNER_ORG_ID/MAESTRO_EVALOPS_ORG_ID and MAESTRO_REMOTE_RUNNER_TOKEN/MAESTRO_EVALOPS_ACCESS_TOKEN, or run EvalOps login.", &[])) }) } -pub(crate) fn resolve_config(o: &ClientOpts) -> Result> { +pub fn resolve_config(o: &ClientOpts) -> Result> { let organization_id = trim(o.organization_id.clone().or_else(|| env_first(ORG_ENV))); let token = trim(o.token.clone().or_else(|| env_first(TOKEN_ENV))); let stored = if organization_id.is_none() || token.is_none() { @@ -1034,7 +1034,7 @@ fn workspace_required(cli: Option, co: &ClientOpts, verb: &str) -> Resul }) } -pub(crate) fn gateway_url(base: &str, session_id: &str) -> String { +pub fn gateway_url(base: &str, session_id: &str) -> String { format!( "{}/v1/runner-sessions/{}/headless", base.trim_end_matches('/'), @@ -1042,7 +1042,7 @@ pub(crate) fn gateway_url(base: &str, session_id: &str) -> String { ) } -pub(crate) fn normalize_base(base: &str) -> String { +pub fn normalize_base(base: &str) -> String { let mut n = base.trim().trim_end_matches('/').to_owned(); for suffix in [ CREATE_PATH, @@ -1182,7 +1182,7 @@ pub fn parse_remote_duration_minutes(raw: Option<&str>, fallback: u64) -> Result parse_minutes(raw, fallback) } -pub(crate) fn parse_minutes(raw: Option<&str>, fallback: u64) -> Result { +pub fn parse_minutes(raw: Option<&str>, fallback: u64) -> Result { let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else { return Ok(fallback); }; @@ -1334,7 +1334,7 @@ fn role_values(o: &Opts) -> Result> { /// Map minted attach roles to the headless connection role. /// Viewer-only role sets attach as viewer; any controller/admin capability attaches as controller. -pub(crate) fn attach_connection_role(roles: &[String]) -> AttachRole { +pub fn attach_connection_role(roles: &[String]) -> AttachRole { if !roles.is_empty() && roles.iter().all(|role| role == ROLE_VIEWER) { AttachRole::Viewer } else { @@ -1397,7 +1397,7 @@ fn state_label(state: Option<&str>) -> String { .to_ascii_lowercase() .replace('_', "-") } -pub(crate) fn require_session(payload: &Value) -> Result { +pub fn require_session(payload: &Value) -> Result { payload .get("session") .and_then(norm_session) @@ -1440,14 +1440,14 @@ fn norm_token(v: &Value) -> Option { expires_at: first_str_map(o, &["expiresAt", "expires_at"]), }) } -pub(crate) fn array_sessions(payload: &Value) -> Vec { +pub fn array_sessions(payload: &Value) -> Vec { payload .get("sessions") .and_then(Value::as_array) .map(|a| a.iter().filter_map(norm_session).collect()) .unwrap_or_default() } -pub(crate) fn array_events(payload: &Value) -> Vec { +pub fn array_events(payload: &Value) -> Vec { payload .get("events") .and_then(Value::as_array) @@ -1465,7 +1465,7 @@ pub(crate) fn array_events(payload: &Value) -> Vec { }) .unwrap_or_default() } -pub(crate) fn first_str(v: &Value, names: &[&str]) -> Option { +pub fn first_str(v: &Value, names: &[&str]) -> Option { v.as_object().and_then(|m| first_str_map(m, names)) } fn first_str_map(m: &Map, names: &[&str]) -> Option { @@ -1479,7 +1479,7 @@ fn first_str_map(m: &Map, names: &[&str]) -> Option { } None } -pub(crate) fn first_num(v: &Value, names: &[&str]) -> Option { +pub fn first_num(v: &Value, names: &[&str]) -> Option { v.as_object().and_then(|m| first_num_map(m, names)) } fn first_num_map(m: &Map, names: &[&str]) -> Option { @@ -1523,7 +1523,7 @@ fn ensure_nonneg(v: Option, field: &str, max: u64) -> Result> { Some(x) => Ok(Some(x)), } } -pub(crate) fn strip_null(v: Value) -> Value { +pub fn strip_null(v: Value) -> Value { match v { Value::Object(map) => { let mut out = Map::new(); @@ -1538,10 +1538,10 @@ pub(crate) fn strip_null(v: Value) -> Value { other => other, } } -pub(crate) fn trim(v: Option) -> Option { +pub fn trim(v: Option) -> Option { v.map(|s| s.trim().to_owned()).filter(|s| !s.is_empty()) } -pub(crate) fn env_first(names: &[&str]) -> Option { +pub fn env_first(names: &[&str]) -> Option { names.iter().find_map(|n| { std::env::var(n) .ok() @@ -1549,16 +1549,16 @@ pub(crate) fn env_first(names: &[&str]) -> Option { .filter(|v| !v.is_empty()) }) } -pub(crate) fn pkg_version() -> String { +pub fn pkg_version() -> String { std::env::var("MAESTRO_VERSION") .or_else(|_| std::env::var("CARGO_PKG_VERSION")) .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_owned()) } -pub(crate) fn print_json(v: &impl Serialize) -> Result<()> { +pub fn print_json(v: &impl Serialize) -> Result<()> { println!("{}", serde_json::to_string_pretty(v)?); Ok(()) } -pub(crate) fn print_session(s: &Session) { +pub fn print_session(s: &Session) { println!("{}", s.id); println!(" state: {}", state_label(s.state.as_deref())); println!(" workspace: {}", s.workspace_id.as_deref().unwrap_or("-")); @@ -1577,7 +1577,7 @@ pub(crate) fn print_session(s: &Session) { println!(" stopped: {r}"); } } -pub(crate) fn print_table(sessions: &[Session]) { +pub fn print_table(sessions: &[Session]) { if sessions.is_empty() { println!( "{}", @@ -1693,7 +1693,7 @@ fn print_attach_instr( fn shell_quote(v: &str) -> String { serde_json::to_string(v).unwrap_or_else(|_| format!("\"{v}\"")) } -pub(crate) fn format_elapsed(ms: u64) -> String { +pub fn format_elapsed(ms: u64) -> String { if ms < 1000 { format!("{ms}ms") } else if ms < 60_000 { diff --git a/packages/tui-rs/src/search_cli.rs b/packages/cli-commands-rs/src/search_cli.rs similarity index 100% rename from packages/tui-rs/src/search_cli.rs rename to packages/cli-commands-rs/src/search_cli.rs diff --git a/packages/tui-rs/src/thread_cli.rs b/packages/cli-commands-rs/src/thread_cli.rs similarity index 100% rename from packages/tui-rs/src/thread_cli.rs rename to packages/cli-commands-rs/src/thread_cli.rs diff --git a/packages/tui-rs/src/update_cli.rs b/packages/cli-commands-rs/src/update_cli.rs similarity index 99% rename from packages/tui-rs/src/update_cli.rs rename to packages/cli-commands-rs/src/update_cli.rs index 58619af7f..16b778d24 100644 --- a/packages/tui-rs/src/update_cli.rs +++ b/packages/cli-commands-rs/src/update_cli.rs @@ -2152,7 +2152,7 @@ fn persist_rollback_suppression(path: &Path, version: &str) -> Result<()> { /// /// Read from `MAESTRO_AUTO_UPDATE`, then `MAESTRO_STARTUP_UPDATE`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum StartupUpdateMode { +pub enum StartupUpdateMode { /// `0`, `false`, `off`, `skip`, or `disabled`: no startup check at all. Off, /// The default, and every value other than the off and apply spellings. @@ -2165,7 +2165,7 @@ pub(crate) enum StartupUpdateMode { ApplyBeforeTui, } -pub(crate) fn startup_update_mode_from(value: Option<&str>) -> StartupUpdateMode { +pub fn startup_update_mode_from(value: Option<&str>) -> StartupUpdateMode { let mode = value.unwrap_or_default().trim().to_ascii_lowercase(); if matches!(mode.as_str(), "0" | "false" | "off" | "skip" | "disabled") { StartupUpdateMode::Off @@ -2198,7 +2198,7 @@ fn startup_update_enabled() -> bool { /// (default 350 ms); it applies to the whole check, and each source gets an /// equal share of it. #[derive(Debug, Clone)] -pub(crate) struct StartupUpdateCheckRequest { +pub struct StartupUpdateCheckRequest { current: String, urls: Vec, channel: UpdateChannel, @@ -2273,9 +2273,9 @@ fn startup_update_check_request() -> Option<(InstallContext, StartupUpdateCheckR /// A newer release the TUI should mention. Carries raw values; the UI /// formats them with its own locale. #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct StartupUpdateNotice { - pub(crate) latest: String, - pub(crate) current: String, +pub struct StartupUpdateNotice { + pub latest: String, + pub current: String, } /// Background startup check for the interactive TUI. @@ -2285,7 +2285,7 @@ pub(crate) struct StartupUpdateNotice { /// newer release while the mode is [`StartupUpdateMode::Notice`]. The /// [`StartupUpdateMode::ApplyBeforeTui`] path already ran (or restarted the /// process) before the terminal was set up, so it is not repeated here. -pub(crate) async fn startup_update_notice() -> Option { +pub async fn startup_update_notice() -> Option { // The gate reads the environment, checks the tty, and resolves the // install layout from disk. Keep those syscalls off the async worker. let request = tokio::task::spawn_blocking(|| { diff --git a/packages/hosted-runner-rs/Cargo.toml b/packages/hosted-runner-rs/Cargo.toml new file mode 100644 index 000000000..ec184fed7 --- /dev/null +++ b/packages/hosted-runner-rs/Cargo.toml @@ -0,0 +1,117 @@ +[package] +name = "maestro-hosted-runner-lib" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Hosted runner HTTP surface, rendezvous carrier and CLI startup for Deixic Code" + +[lib] +name = "maestro_hosted_runner_lib" +path = "src/lib.rs" + +[dependencies] +maestro-local-host.workspace = true +maestro-ai = { workspace = true, features = ["bedrock"] } +maestro-runtime.workspace = true +maestro-runtime-contracts.workspace = true + +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "signal", "fs", "net", "process", "time"] } +tokio-rustls.workspace = true +tokio-util = { workspace = true, features = ["rt"] } +futures.workspace = true +reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls"] } +rcgen.workspace = true +rustls.workspace = true +x509-parser.workspace = true +sha2.workspace = true +base64.workspace = true +rand.workspace = true +serde.workspace = true +serde_json.workspace = true +prost = "0.14" +anyhow.workspace = true +thiserror.workspace = true +uuid.workspace = true +tracing.workspace = true +clap.workspace = true +chrono.workspace = true +dunce.workspace = true +fd-lock.workspace = true +urlencoding = "2" +shlex = "1" +url = "2" + +[dev-dependencies] +maestro-local-host = { workspace = true, features = ["test-support"] } +maestro-runtime = { workspace = true, features = ["test-support"] } +maestro-ai = { workspace = true, features = ["test-support"] } +tokio = { workspace = true, features = ["test-util"] } +tempfile.workspace = true +libc.workspace = true +time.workspace = true +ring.workspace = true + +[lints.clippy] +# Enable pedantic lints for code quality +pedantic = { level = "warn", priority = -1 } + +# Allow common patterns that are intentional in this codebase +unused_async = "allow" # Async functions for interface consistency +missing_errors_doc = "allow" # Not all error conditions need docs +missing_panics_doc = "allow" # Not all panics need docs +cast_possible_truncation = "allow" # Often unavoidable in systems code +cast_precision_loss = "allow" # Acceptable for display/UI calculations +cast_sign_loss = "allow" # Used intentionally in many places +cast_lossless = "allow" # Allow explicit casts for clarity +must_use_candidate = "allow" # Too noisy for all builder patterns +doc_markdown = "allow" # Too picky about formatting +module_name_repetitions = "allow" # Common Rust naming pattern +similar_names = "allow" # Often intentional (e.g., tx/rx) +too_many_lines = "allow" # Subjective limit +struct_excessive_bools = "allow" # Sometimes needed for state machines +items_after_statements = "allow" # Useful for local helpers +wildcard_imports = "allow" # Used for re-exports +match_same_arms = "allow" # Sometimes clearer than combining +needless_pass_by_value = "allow" # Interface consistency +redundant_closure_for_method_calls = "allow" # Sometimes clearer +uninlined_format_args = "allow" # Older Rust style is still readable +if_not_else = "allow" # Sometimes clearer to negate +single_match_else = "allow" # Sometimes clearer than if-let +return_self_not_must_use = "allow" # Builder patterns returning Self +format_push_string = "allow" # push_str(&format!()) is readable +unnecessary_wraps = "allow" # Sometimes for API consistency +option_if_let_else = "allow" # match is often clearer +match_wildcard_for_single_variants = "allow" # Future-proofing matches +ref_option_ref = "allow" # &Option is sometimes cleaner +trivially_copy_pass_by_ref = "allow" # Sometimes clearer to pass by ref +fn_params_excessive_bools = "allow" # UI functions often have many flags +large_futures = "allow" # TUI app naturally has large futures +iter_without_into_iter = "allow" # Not always needed +unused_self = "allow" # Sometimes for method consistency +option_option = "allow" # Sometimes needed for optional Option +manual_let_else = "allow" # match is often clearer +significant_drop_tightening = "allow" # Sometimes clearer to drop earlier +map_unwrap_or = "allow" # Sometimes clearer than map+unwrap_or_else +implicit_hasher = "allow" # Not generalizing over hashers is fine +struct_field_names = "allow" # prefix/postfix patterns are OK +many_single_char_names = "allow" # Short names in math/UI code +assigning_clones = "allow" # Sometimes clearer than clone_from +case_sensitive_file_extension_comparisons = "allow" # Platform-specific code +cast_possible_wrap = "allow" # Necessary for libc interop +needless_continue = "allow" # Sometimes improves readability +if_then_some_else_none = "allow" # Sometimes clearer than bool::then +ref_option = "allow" # &Option is sometimes appropriate +default_trait_access = "allow" # Type::default() is clear enough +used_underscore_binding = "allow" # Sometimes used for side effects +match_bool = "allow" # if-else is sometimes clearer than match +missing_fields_in_debug = "allow" # Some fields can't be debugged (closures) +naive_bytecount = "allow" # bytecount crate not worth adding +doc_link_with_quotes = "allow" # Quotes in doc examples are OK +comparison_chain = "allow" # if-else chains are often clearer +large_stack_arrays = "allow" # Test fixtures may use large arrays + +[lints.rust] +# Unsafe code is allowed only for low-level system operations +# (libc process management, Darwin APIs, pointer arithmetic) +# Each unsafe block should have a SAFETY comment explaining why it's safe +unexpected_cfgs = { level = "allow", check-cfg = ["cfg(kani)"] } diff --git a/packages/local-host-rs/src/hosted_runner.rs b/packages/hosted-runner-rs/src/hosted_runner.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner.rs rename to packages/hosted-runner-rs/src/hosted_runner.rs diff --git a/packages/local-host-rs/src/hosted_runner/config.rs b/packages/hosted-runner-rs/src/hosted_runner/config.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/config.rs rename to packages/hosted-runner-rs/src/hosted_runner/config.rs diff --git a/packages/local-host-rs/src/hosted_runner/env_aliases.rs b/packages/hosted-runner-rs/src/hosted_runner/env_aliases.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/env_aliases.rs rename to packages/hosted-runner-rs/src/hosted_runner/env_aliases.rs diff --git a/packages/local-host-rs/src/hosted_runner/handle.rs b/packages/hosted-runner-rs/src/hosted_runner/handle.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/handle.rs rename to packages/hosted-runner-rs/src/hosted_runner/handle.rs diff --git a/packages/local-host-rs/src/hosted_runner/http_types.rs b/packages/hosted-runner-rs/src/hosted_runner/http_types.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/http_types.rs rename to packages/hosted-runner-rs/src/hosted_runner/http_types.rs diff --git a/packages/local-host-rs/src/hosted_runner/initial_actions.rs b/packages/hosted-runner-rs/src/hosted_runner/initial_actions.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/initial_actions.rs rename to packages/hosted-runner-rs/src/hosted_runner/initial_actions.rs diff --git a/packages/local-host-rs/src/hosted_runner/manifests.rs b/packages/hosted-runner-rs/src/hosted_runner/manifests.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/manifests.rs rename to packages/hosted-runner-rs/src/hosted_runner/manifests.rs diff --git a/packages/local-host-rs/src/hosted_runner/native_code.rs b/packages/hosted-runner-rs/src/hosted_runner/native_code.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/native_code.rs rename to packages/hosted-runner-rs/src/hosted_runner/native_code.rs diff --git a/packages/local-host-rs/src/hosted_runner/native_code/tests.rs b/packages/hosted-runner-rs/src/hosted_runner/native_code/tests.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/native_code/tests.rs rename to packages/hosted-runner-rs/src/hosted_runner/native_code/tests.rs diff --git a/packages/local-host-rs/src/hosted_runner/native_code_credentials.rs b/packages/hosted-runner-rs/src/hosted_runner/native_code_credentials.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/native_code_credentials.rs rename to packages/hosted-runner-rs/src/hosted_runner/native_code_credentials.rs diff --git a/packages/local-host-rs/src/hosted_runner/native_code_credentials/tests.rs b/packages/hosted-runner-rs/src/hosted_runner/native_code_credentials/tests.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/native_code_credentials/tests.rs rename to packages/hosted-runner-rs/src/hosted_runner/native_code_credentials/tests.rs diff --git a/packages/local-host-rs/src/hosted_runner/rendezvous_carrier.rs b/packages/hosted-runner-rs/src/hosted_runner/rendezvous_carrier.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/rendezvous_carrier.rs rename to packages/hosted-runner-rs/src/hosted_runner/rendezvous_carrier.rs diff --git a/packages/local-host-rs/src/hosted_runner/rendezvous_carrier/tests.rs b/packages/hosted-runner-rs/src/hosted_runner/rendezvous_carrier/tests.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/rendezvous_carrier/tests.rs rename to packages/hosted-runner-rs/src/hosted_runner/rendezvous_carrier/tests.rs diff --git a/packages/local-host-rs/src/hosted_runner/rendezvous_protocol.rs b/packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/rendezvous_protocol.rs rename to packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol.rs diff --git a/packages/local-host-rs/src/hosted_runner/rendezvous_protocol/tests.rs b/packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol/tests.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/rendezvous_protocol/tests.rs rename to packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol/tests.rs diff --git a/packages/local-host-rs/src/hosted_runner/rendezvous_runtime.rs b/packages/hosted-runner-rs/src/hosted_runner/rendezvous_runtime.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/rendezvous_runtime.rs rename to packages/hosted-runner-rs/src/hosted_runner/rendezvous_runtime.rs diff --git a/packages/local-host-rs/src/hosted_runner/shared.rs b/packages/hosted-runner-rs/src/hosted_runner/shared.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/shared.rs rename to packages/hosted-runner-rs/src/hosted_runner/shared.rs diff --git a/packages/local-host-rs/src/hosted_runner/snapshots.rs b/packages/hosted-runner-rs/src/hosted_runner/snapshots.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/snapshots.rs rename to packages/hosted-runner-rs/src/hosted_runner/snapshots.rs diff --git a/packages/local-host-rs/src/hosted_runner/sse_stream.rs b/packages/hosted-runner-rs/src/hosted_runner/sse_stream.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/sse_stream.rs rename to packages/hosted-runner-rs/src/hosted_runner/sse_stream.rs diff --git a/packages/local-host-rs/src/hosted_runner/tests.rs b/packages/hosted-runner-rs/src/hosted_runner/tests.rs similarity index 99% rename from packages/local-host-rs/src/hosted_runner/tests.rs rename to packages/hosted-runner-rs/src/hosted_runner/tests.rs index e79c5144c..1f7aa9a7f 100644 --- a/packages/local-host-rs/src/hosted_runner/tests.rs +++ b/packages/hosted-runner-rs/src/hosted_runner/tests.rs @@ -1,14 +1,17 @@ +mod blocked_executor_cleanup; +use blocked_executor_cleanup::{ + BlockedPromptRuntimeExecutor, BlockedPromptRuntimeExecutorReleaseGuard, +}; mod request_routing; use maestro_runtime_contracts::tool_wire; use reqwest::StatusCode; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::{fs, sync::Condvar}; use tempfile::tempdir; use tokio::sync::Notify; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; - use super::config::HostedRunnerWorkloadIdentityConfig; use super::*; use crate::headless::messages::{CodexSubagentContinuityEdge, ToolRetryDecisionAction}; @@ -2481,58 +2484,6 @@ struct PumpOnlyRuntimeExecutor { queued: Arc>>, } -#[derive(Debug)] -struct BlockedPromptRuntimeExecutor { - calls: Arc, - started: Arc, - release: Arc<(Mutex, Condvar)>, -} - -impl BlockedPromptRuntimeExecutor { - fn new() -> Self { - Self { - calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)), - started: Arc::new(AtomicBool::new(false)), - release: Arc::new((Mutex::new(false), Condvar::new())), - } - } - - fn release(&self) { - let (released, wake) = &*self.release; - *released.lock().expect("release state") = true; - wake.notify_all(); - } -} - -impl HostedRunnerHeadlessMessageExecutor for BlockedPromptRuntimeExecutor { - fn execute( - &self, - _context: &HostedRunnerHeadlessMessageContext, - message: ToAgentMessage, - ) -> Result { - if matches!(message, ToAgentMessage::Prompt { .. }) || is_control_response_message(&message) - { - self.calls.fetch_add(1, Ordering::SeqCst); - self.started.store(true, Ordering::SeqCst); - let (released, wake) = &*self.release; - let mut released = released.lock().expect("release state"); - while !*released { - released = wake.wait(released).expect("release state"); - } - return Ok(HostedRunnerHeadlessMessageResult::runtime_handled( - vec![FromAgentMessage::ResponseStart { - response_id: "response-blocked".to_string(), - }], - "released blocked prompt", - )); - } - Ok(HostedRunnerHeadlessMessageResult::transport_only( - Vec::new(), - "ignored by blocked prompt executor", - )) - } -} - #[derive(Debug)] struct FailingPumpRuntimeExecutor; @@ -3231,6 +3182,7 @@ async fn hosted_runner_forwards_resident_binding_to_native_workspace_activation( supervisor_config.transport.cwd = Some(workspace.path().display().to_string()); supervisor_config.transport.env = vec![ (CHILD_ENV.to_string(), "1".to_string()), + (crate::ID_AUTH.to_string(), "1".to_string()), ( "MAESTRO_HOSTED_WORKSPACE_BINDING_INPUT_LOG".to_string(), child_input_log.display().to_string(), @@ -8074,6 +8026,7 @@ async fn shutdown_awaits_event_pump() { async fn drain_fences_in_flight_prompts_and_serializes_repeated_drains() { let workspace = tempdir().expect("workspace"); let executor = Arc::new(BlockedPromptRuntimeExecutor::new()); + let _release_guard = BlockedPromptRuntimeExecutorReleaseGuard(executor.clone()); let handle = start_hosted_runner_with_message_executor( test_config(workspace.path().to_path_buf()), executor.clone(), @@ -8188,6 +8141,7 @@ async fn drain_fences_in_flight_prompts_and_serializes_repeated_drains() { async fn concurrent_idempotent_messages_wait_for_the_original_attempt() { let workspace = tempdir().expect("workspace"); let executor = Arc::new(BlockedPromptRuntimeExecutor::new()); + let _release_guard = BlockedPromptRuntimeExecutorReleaseGuard(executor.clone()); let handle = start_hosted_runner_with_message_executor( test_config(workspace.path().to_path_buf()), executor.clone(), diff --git a/packages/hosted-runner-rs/src/hosted_runner/tests/blocked_executor_cleanup.rs b/packages/hosted-runner-rs/src/hosted_runner/tests/blocked_executor_cleanup.rs new file mode 100644 index 000000000..5b34f688e --- /dev/null +++ b/packages/hosted-runner-rs/src/hosted_runner/tests/blocked_executor_cleanup.rs @@ -0,0 +1,104 @@ +use super::*; + +#[derive(Debug)] +pub(super) struct BlockedPromptRuntimeExecutor { + pub(super) calls: Arc, + pub(super) started: Arc, + release: Arc<(Mutex, Condvar)>, +} + +// Release blocked fake work before a failing fixture tears down its runtime. +pub(super) struct BlockedPromptRuntimeExecutorReleaseGuard( + pub(super) Arc, +); + +impl Drop for BlockedPromptRuntimeExecutorReleaseGuard { + fn drop(&mut self) { + self.0.release(); + } +} + +impl BlockedPromptRuntimeExecutor { + pub(super) fn new() -> Self { + Self { + calls: Arc::new(std::sync::atomic::AtomicUsize::new(0)), + started: Arc::new(AtomicBool::new(false)), + release: Arc::new((Mutex::new(false), Condvar::new())), + } + } + + pub(super) fn release(&self) { + let (released, wake) = &*self.release; + *released.lock().expect("release state") = true; + wake.notify_all(); + } +} + +impl HostedRunnerHeadlessMessageExecutor for BlockedPromptRuntimeExecutor { + fn execute( + &self, + _context: &HostedRunnerHeadlessMessageContext, + message: ToAgentMessage, + ) -> Result { + if matches!(message, ToAgentMessage::Prompt { .. }) || is_control_response_message(&message) + { + self.calls.fetch_add(1, Ordering::SeqCst); + self.started.store(true, Ordering::SeqCst); + let (released, wake) = &*self.release; + let mut released = released.lock().expect("release state"); + while !*released { + released = wake.wait(released).expect("release state"); + } + return Ok(HostedRunnerHeadlessMessageResult::runtime_handled( + vec![FromAgentMessage::ResponseStart { + response_id: "response-blocked".to_string(), + }], + "released blocked prompt", + )); + } + Ok(HostedRunnerHeadlessMessageResult::transport_only( + Vec::new(), + "ignored by blocked prompt executor", + )) + } +} + +#[tokio::test] +async fn blocked_prompt_executor_cleanup_preserves_an_early_fixture_failure() { + let workspace = tempdir().expect("workspace"); + let executor = Arc::new(BlockedPromptRuntimeExecutor::new()); + let context = executor_context_with_response_key(workspace.path(), "cleanup-regression"); + let worker_executor = executor.clone(); + let worker = tokio::task::spawn_blocking(move || { + worker_executor.execute( + &context, + ToAgentMessage::ClientToolResult { + call_id: "cleanup-regression".to_string(), + content: Vec::new(), + is_error: false, + }, + ) + }); + let started = tokio::time::timeout(Duration::from_secs(1), async { + while !executor.started.load(Ordering::SeqCst) { + tokio::time::sleep(Duration::from_millis(5)).await; + } + }) + .await; + let failure = std::panic::catch_unwind(|| { + let _release_guard = BlockedPromptRuntimeExecutorReleaseGuard(executor.clone()); + panic!("intentional fixture assertion failure before explicit release"); + }); + let completion = tokio::time::timeout(Duration::from_secs(1), worker).await; + // Keep the regression itself safe when cleanup is broken. + executor.release(); + assert!(started.is_ok(), "blocked fixture should start"); + assert!( + failure.is_err(), + "the original fixture failure must remain visible" + ); + completion + .expect("panic cleanup must release the blocked fixture") + .expect("blocked fixture worker should not panic") + .expect("blocked fixture should retain its normal response"); +} diff --git a/packages/local-host-rs/src/hosted_runner/tests/deferred_rejection.rs b/packages/hosted-runner-rs/src/hosted_runner/tests/deferred_rejection.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/tests/deferred_rejection.rs rename to packages/hosted-runner-rs/src/hosted_runner/tests/deferred_rejection.rs diff --git a/packages/local-host-rs/src/hosted_runner/tests/env_aliases.rs b/packages/hosted-runner-rs/src/hosted_runner/tests/env_aliases.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/tests/env_aliases.rs rename to packages/hosted-runner-rs/src/hosted_runner/tests/env_aliases.rs diff --git a/packages/local-host-rs/src/hosted_runner/tests/replay_ownership.rs b/packages/hosted-runner-rs/src/hosted_runner/tests/replay_ownership.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/tests/replay_ownership.rs rename to packages/hosted-runner-rs/src/hosted_runner/tests/replay_ownership.rs diff --git a/packages/local-host-rs/src/hosted_runner/tests/request_routing.rs b/packages/hosted-runner-rs/src/hosted_runner/tests/request_routing.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/tests/request_routing.rs rename to packages/hosted-runner-rs/src/hosted_runner/tests/request_routing.rs diff --git a/packages/local-host-rs/src/hosted_runner/tests/transport_flush.rs b/packages/hosted-runner-rs/src/hosted_runner/tests/transport_flush.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/tests/transport_flush.rs rename to packages/hosted-runner-rs/src/hosted_runner/tests/transport_flush.rs diff --git a/packages/local-host-rs/src/hosted_runner/thread_protocol.rs b/packages/hosted-runner-rs/src/hosted_runner/thread_protocol.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner/thread_protocol.rs rename to packages/hosted-runner-rs/src/hosted_runner/thread_protocol.rs diff --git a/packages/local-host-rs/src/hosted_runner/workload_identity.rs b/packages/hosted-runner-rs/src/hosted_runner/workload_identity.rs similarity index 99% rename from packages/local-host-rs/src/hosted_runner/workload_identity.rs rename to packages/hosted-runner-rs/src/hosted_runner/workload_identity.rs index 53594bc0e..606c1b707 100644 --- a/packages/local-host-rs/src/hosted_runner/workload_identity.rs +++ b/packages/hosted-runner-rs/src/hosted_runner/workload_identity.rs @@ -337,8 +337,7 @@ impl WorkloadIdentityExchanger { .timeout(StdDuration::from_secs(10)) .redirect(reqwest::redirect::Policy::none()) .https_only(true) - .tls_built_in_root_certs(false) - .add_root_certificate(identity_ca) + .tls_certs_only([identity_ca]) .build() .map_err(|_| WorkloadIdentityError::Unavailable)?; let binding = IdentityBinding { diff --git a/packages/local-host-rs/src/hosted_runner_cli.rs b/packages/hosted-runner-rs/src/hosted_runner_cli.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner_cli.rs rename to packages/hosted-runner-rs/src/hosted_runner_cli.rs diff --git a/packages/local-host-rs/src/hosted_runner_conformance.rs b/packages/hosted-runner-rs/src/hosted_runner_conformance.rs similarity index 100% rename from packages/local-host-rs/src/hosted_runner_conformance.rs rename to packages/hosted-runner-rs/src/hosted_runner_conformance.rs diff --git a/packages/hosted-runner-rs/src/lib.rs b/packages/hosted-runner-rs/src/lib.rs new file mode 100644 index 000000000..ca33d1cae --- /dev/null +++ b/packages/hosted-runner-rs/src/lib.rs @@ -0,0 +1,22 @@ +//! Hosted runner: the single-session HTTP surface, rendezvous carrier and CLI +//! startup that Platform-managed Maestro runtimes execute. +//! +//! Split out of `maestro-local-host` so that edits here recompile this crate +//! instead of the whole local host. + +#[cfg(test)] +pub(crate) use maestro_ai as ai; +#[cfg(test)] +pub(crate) use maestro_local_host::credential_mode; +pub(crate) use maestro_local_host::{ + agent, fs_atomic, git, headless, headless_server, safety, transcript, +}; + +pub mod hosted_runner; +pub mod hosted_runner_cli; +pub mod hosted_runner_conformance; + +/// Environment switch that lets the spawned native child accept the loopback +/// Identity fixture, now that `maestro-local-host` is not compiled as a test build. +#[cfg(test)] +pub(crate) const ID_AUTH: &str = "MAESTRO_TEST_IDENTITY_AUTHORITY"; diff --git a/packages/local-host-rs/tests/fixtures/hosted-runner-startup-contract.json b/packages/hosted-runner-rs/tests/fixtures/hosted-runner-startup-contract.json similarity index 100% rename from packages/local-host-rs/tests/fixtures/hosted-runner-startup-contract.json rename to packages/hosted-runner-rs/tests/fixtures/hosted-runner-startup-contract.json diff --git a/packages/local-host-rs/tests/hosted_runner_startup_contract.rs b/packages/hosted-runner-rs/tests/hosted_runner_startup_contract.rs similarity index 99% rename from packages/local-host-rs/tests/hosted_runner_startup_contract.rs rename to packages/hosted-runner-rs/tests/hosted_runner_startup_contract.rs index 9c322859a..7fe8ea882 100644 --- a/packages/local-host-rs/tests/hosted_runner_startup_contract.rs +++ b/packages/hosted-runner-rs/tests/hosted_runner_startup_contract.rs @@ -13,7 +13,7 @@ use std::fs; use std::net::TcpListener; use std::path::{Path, PathBuf}; -use maestro_local_host::hosted_runner_cli::{ +use maestro_hosted_runner_lib::hosted_runner_cli::{ HostedRunnerLaunchConfig, resolve_hosted_runner_launch_config, }; use serde::Deserialize; @@ -234,7 +234,7 @@ fn every_exercised_coordinate_is_documented() { #[cfg(unix)] #[tokio::test] async fn cli_runtime_reports_identity_and_drains_a_manifest() { - use maestro_local_host::hosted_runner_cli::{ + use maestro_hosted_runner_lib::hosted_runner_cli::{ HostedRunnerShutdownSignal, start_hosted_runner_cli_runtime, }; diff --git a/packages/local-host-rs/Cargo.toml b/packages/local-host-rs/Cargo.toml index 2d8aa6e8e..2777fd4df 100644 --- a/packages/local-host-rs/Cargo.toml +++ b/packages/local-host-rs/Cargo.toml @@ -44,7 +44,6 @@ maestro-presentation.workspace = true # Async runtime async-trait = "0.1" tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "signal", "fs", "net", "process", "time"] } -tokio-rustls.workspace = true tokio-util = { workspace = true, features = ["rt"] } futures.workspace = true @@ -57,7 +56,7 @@ maestro-session-history.workspace = true maestro-session.workspace = true # HTTP client for AI APIs -reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls-tls"] } +reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls"] } rcgen.workspace = true rustls.workspace = true semver = "1" @@ -99,7 +98,6 @@ tree-sitter-bash.workspace = true pdf-extract.workspace = true urlencoding = "2" zeroize.workspace = true -x509-parser.workspace = true # Diff generation similar.workspace = true diff --git a/packages/local-host-rs/src/agent/mod.rs b/packages/local-host-rs/src/agent/mod.rs index 844f09ac1..86a689cbc 100644 --- a/packages/local-host-rs/src/agent/mod.rs +++ b/packages/local-host-rs/src/agent/mod.rs @@ -25,6 +25,7 @@ mod native_host; mod native_lifecycle_tests; #[cfg(test)] mod native_read_only_tests; +pub mod tool_host; // The runtime owns these modules. Re-exporting them here keeps existing TUI // callers source-compatible without putting a dependency from runtime back to @@ -252,14 +253,16 @@ impl NativeAgent { let telemetry_identity_scope = Arc::new(RwLock::new(initial_identity_scope)); let provider_name = resolved.provider_name.clone(); let telemetry_config = config.clone(); - let host = build_local_host( - &config, - credential_vault.clone(), - client_override, - subagent_parent_scope_id.clone(), - mailbox_identity.clone(), - Arc::clone(&telemetry_identity_scope), - )?; + let host = tool_host::tool_host().build_local_host(tool_host::LocalHostBuild { + config: &config, + credential_vault: credential_vault.clone(), + skip_identity_verification: client_override + .as_ref() + .is_some_and(ClientOverride::skips_identity_verification), + subagent_parent_scope_id: subagent_parent_scope_id.clone(), + mailbox_identity: mailbox_identity.clone(), + telemetry_identity_scope: Arc::clone(&telemetry_identity_scope), + })?; let runtime_config = config.into_runtime(); let telemetry_host = host.clone(); let (inner, events) = maestro_runtime::agent::NativeAgent::start_with_resolved_client( @@ -619,20 +622,20 @@ pub fn dex_loop_execution_host( config: &NativeAgentConfig, credential_vault: CredentialVault, ) -> Result { - build_local_host( + tool_host::tool_host().build_local_host(tool_host::LocalHostBuild { config, credential_vault, - None, - None, - None, - Arc::new(RwLock::new(None)), - ) + skip_identity_verification: false, + subagent_parent_scope_id: None, + mailbox_identity: None, + telemetry_identity_scope: Arc::new(RwLock::new(None)), + }) } fn build_local_host( config: &NativeAgentConfig, credential_vault: CredentialVault, - client_override: Option, + skip_identity_verification: bool, subagent_parent_scope_id: Option, mailbox_identity: Option, telemetry_identity_scope: Arc>>, @@ -653,10 +656,7 @@ fn build_local_host( let mut hooks = IntegratedHookSystem::load_from_config(&config.cwd); hooks.set_model(&config.model); - if client_override - .as_ref() - .is_none_or(|override_client| !override_client.skips_identity_verification()) - { + if !skip_identity_verification { if let Ok(session) = crate::credential_mode::verified_current_identity_session() { hooks.set_identity_context( Some(session.organization_id.clone()), diff --git a/packages/local-host-rs/src/agent/native_admission_tests.rs b/packages/local-host-rs/src/agent/native_admission_tests.rs index 5c7f05551..9cea31356 100644 --- a/packages/local-host-rs/src/agent/native_admission_tests.rs +++ b/packages/local-host-rs/src/agent/native_admission_tests.rs @@ -62,7 +62,7 @@ fn tui_host(config: NativeAgentConfig) -> NativeExecutionHostHandle { super::build_local_host( &config, CredentialVault::new(), - None, + false, None, None, Arc::new(RwLock::new(None)), diff --git a/packages/local-host-rs/src/agent/tool_host.rs b/packages/local-host-rs/src/agent/tool_host.rs new file mode 100644 index 000000000..1611e3377 --- /dev/null +++ b/packages/local-host-rs/src/agent/tool_host.rs @@ -0,0 +1,81 @@ +//! Seam between the local host and the concrete tool implementation. +//! +//! The host asks this trait for the three things it needs from the tool layer: +//! composing the native execution host, and listing tool definitions for +//! capability and subagent policy decisions. [`BuiltInToolHost`] is the +//! in-tree implementation; a different crate can supply its own through +//! [`install_tool_host`]. + +use std::path::Path; +use std::sync::{Arc, OnceLock, RwLock}; + +use anyhow::Result; +use maestro_runtime::agent::{NativeExecutionHostHandle, ToolDefinition}; + +use super::{NativeAgentConfig, build_local_host}; +use crate::agent::credential_store::CredentialVault; + +/// Inputs for composing one native execution host. +pub struct LocalHostBuild<'a> { + pub config: &'a NativeAgentConfig, + pub credential_vault: CredentialVault, + /// True for an injected test client that bypasses Identity verification. + pub skip_identity_verification: bool, + pub subagent_parent_scope_id: Option, + pub mailbox_identity: Option, + pub telemetry_identity_scope: Arc>>, +} + +pub trait ToolHost: Send + Sync { + /// Compose the tool executor, hooks, firewall and model resolver for one actor. + fn build_local_host(&self, build: LocalHostBuild<'_>) -> Result; + + /// Definitions of the built-in tool registry. + fn registry_tool_definitions(&self) -> Vec; + + /// Definitions an executor rooted at `cwd` exposes. + fn executor_tool_definitions(&self, cwd: &Path) -> Vec; +} + +/// The tool implementation that lives in this crate. +pub struct BuiltInToolHost; + +impl ToolHost for BuiltInToolHost { + fn build_local_host(&self, build: LocalHostBuild<'_>) -> Result { + build_local_host( + build.config, + build.credential_vault, + build.skip_identity_verification, + build.subagent_parent_scope_id, + build.mailbox_identity, + build.telemetry_identity_scope, + ) + } + + fn registry_tool_definitions(&self) -> Vec { + crate::tools::ToolRegistry::new().tools().cloned().collect() + } + + fn executor_tool_definitions(&self, cwd: &Path) -> Vec { + crate::tools::ToolExecutor::new(cwd.to_string_lossy().to_string()) + .tool_definitions() + .cloned() + .collect() + } +} + +static INSTALLED: OnceLock> = OnceLock::new(); +static BUILT_IN: BuiltInToolHost = BuiltInToolHost; + +/// Replace the in-tree tool implementation. Returns the rejected host when one +/// was already installed. +pub fn install_tool_host(host: Arc) -> Result<(), Arc> { + INSTALLED.set(host) +} + +pub(crate) fn tool_host() -> &'static dyn ToolHost { + match INSTALLED.get() { + Some(host) => host.as_ref(), + None => &BUILT_IN, + } +} diff --git a/packages/local-host-rs/src/codex_cli.rs b/packages/local-host-rs/src/codex_cli.rs index 01900a95b..e2723b88d 100644 --- a/packages/local-host-rs/src/codex_cli.rs +++ b/packages/local-host-rs/src/codex_cli.rs @@ -1302,10 +1302,10 @@ fn codex_schema_diagnostics_for_cwd(cwd: &Path) -> Result> { } fn runtime_codex_dynamic_tools(cwd: &Path) -> Vec { - let executor = crate::tools::ToolExecutor::new(cwd.to_string_lossy().to_string()); - let tools = executor - .tool_definitions() - .map(|definition| (definition.tool.name.clone(), definition.clone())) + let tools = crate::agent::tool_host::tool_host() + .executor_tool_definitions(cwd) + .into_iter() + .map(|definition| (definition.tool.name.clone(), definition)) .collect::>(); crate::agent::dynamic_tools_from_native(&tools) } diff --git a/packages/local-host-rs/src/credential_mode.rs b/packages/local-host-rs/src/credential_mode.rs index 3b23767fc..7872c7b98 100644 --- a/packages/local-host-rs/src/credential_mode.rs +++ b/packages/local-host-rs/src/credential_mode.rs @@ -637,10 +637,7 @@ fn identity_verification_client( if certificates.is_empty() { bail!("hosted EvalOps Identity CA contains no certificates"); } - builder = builder.use_rustls_tls().tls_built_in_root_certs(false); - for certificate in certificates { - builder = builder.add_root_certificate(certificate); - } + builder = builder.tls_backend_rustls().tls_certs_only(certificates); } builder .build() diff --git a/packages/local-host-rs/src/headless/async_transport.rs b/packages/local-host-rs/src/headless/async_transport.rs index 3e19928b7..9bfbadf93 100644 --- a/packages/local-host-rs/src/headless/async_transport.rs +++ b/packages/local-host-rs/src/headless/async_transport.rs @@ -458,7 +458,7 @@ impl AsyncAgentTransport { ); } - pub(crate) fn event_notification(&self) -> Arc { + pub fn event_notification(&self) -> Arc { Arc::clone(&self.event_notification) } diff --git a/packages/local-host-rs/src/headless/controller_binding.rs b/packages/local-host-rs/src/headless/controller_binding.rs index 83b083559..c4a96d312 100644 --- a/packages/local-host-rs/src/headless/controller_binding.rs +++ b/packages/local-host-rs/src/headless/controller_binding.rs @@ -14,15 +14,15 @@ use thiserror::Error; use super::messages::ControllerBindingHello; /// Version of the optional Platform-to-Maestro controller binding handshake. -pub(crate) const CONTROLLER_BINDING_VERSION: &str = "evalops.maestro.controller-binding.v1"; +pub const CONTROLLER_BINDING_VERSION: &str = "evalops.maestro.controller-binding.v1"; /// Schema version for the secret-free controller context inside the binding. -pub(crate) const CONTROLLER_CONTEXT_SCHEMA_VERSION: &str = "evalops.maestro.controller-context.v1"; +pub const CONTROLLER_CONTEXT_SCHEMA_VERSION: &str = "evalops.maestro.controller-context.v1"; const PLATFORM_CONTROLLER_ID: &str = "evalops.platform"; /// Lifetime selected by Platform before the Maestro process is admitted. #[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -pub(crate) enum ControllerLifetimeProfile { +pub enum ControllerLifetimeProfile { /// One bounded operating turn. Ephemeral, /// One durable hosted thread generation. @@ -32,35 +32,35 @@ pub(crate) enum ControllerLifetimeProfile { /// Secret-free Platform scope and request correlation for one headless child. #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] -pub(crate) struct ControllerContext { - pub(crate) schema_version: String, - pub(crate) controller_id: String, - pub(crate) organization_id: String, - pub(crate) workspace_id: String, - pub(crate) thread_id: String, +pub struct ControllerContext { + pub schema_version: String, + pub controller_id: String, + pub organization_id: String, + pub workspace_id: String, + pub thread_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) channel_id: Option, + pub channel_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) request_id: Option, - pub(crate) lifetime_profile: ControllerLifetimeProfile, + pub request_id: Option, + pub lifetime_profile: ControllerLifetimeProfile, #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) runtime_generation: Option, + pub runtime_generation: Option, } /// Runtime identity values that a managed process may require the hello to match. #[derive(Debug, Clone, Default, Eq, PartialEq)] -pub(crate) struct ControllerScopeExpectation { - pub(crate) organization_id: Option, - pub(crate) workspace_id: Option, - pub(crate) thread_id: Option, - pub(crate) channel_id: Option, - pub(crate) request_id: Option, +pub struct ControllerScopeExpectation { + pub organization_id: Option, + pub workspace_id: Option, + pub thread_id: Option, + pub channel_id: Option, + pub request_id: Option, } impl ControllerScopeExpectation { /// Reads only secret-free identity variables populated by the Platform adapter. #[must_use] - pub(crate) fn from_evalops_environment() -> Self { + pub fn from_evalops_environment() -> Self { Self { organization_id: std::env::var("MAESTRO_EVALOPS_ORG_ID").ok(), workspace_id: std::env::var("MAESTRO_EVALOPS_WORKSPACE_ID").ok(), @@ -73,15 +73,15 @@ impl ControllerScopeExpectation { /// Echoed proof that Maestro parsed and accepted the exact binding payload. #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) struct ControllerBindingReceipt { - pub(crate) binding_version: String, - pub(crate) binding_sha256: String, - pub(crate) controller_context: ControllerContext, +pub struct ControllerBindingReceipt { + pub binding_version: String, + pub binding_sha256: String, + pub controller_context: ControllerContext, } /// Strict controller-binding validation failures. #[derive(Debug, Error, Eq, PartialEq)] -pub(crate) enum ControllerBindingError { +pub enum ControllerBindingError { #[error("controller binding fields must be supplied together")] Incomplete, #[error("unsupported controller binding version")] @@ -119,7 +119,7 @@ struct ControllerHelloExtension { } /// Parses and validates the optional controller extension carried by `hello`. -pub(crate) fn controller_binding_from_hello_json( +pub fn controller_binding_from_hello_json( raw: &str, negotiated_protocol_version: &str, expected: &ControllerScopeExpectation, @@ -152,7 +152,7 @@ pub(crate) fn controller_binding_from_hello_json( })) } -pub(crate) fn controller_binding_from_hello_extension( +pub fn controller_binding_from_hello_extension( extension: Option<&ControllerBindingHello>, negotiated_protocol_version: &str, expected: &ControllerScopeExpectation, @@ -287,7 +287,7 @@ fn validate_manifest( } /// Computes the cross-repository canonical binding digest. -pub(crate) fn controller_binding_sha256( +pub fn controller_binding_sha256( binding_version: &str, context: &ControllerContext, capability_manifest: &Value, diff --git a/packages/local-host-rs/src/headless/messages.rs b/packages/local-host-rs/src/headless/messages.rs index 21000921f..cb62c7efb 100644 --- a/packages/local-host-rs/src/headless/messages.rs +++ b/packages/local-host-rs/src/headless/messages.rs @@ -98,9 +98,8 @@ use serde::{Deserialize, Serialize, de::Deserializer}; use serde_json::Value; use std::collections::HashMap; -pub(crate) const CODEX_SUBAGENT_TOOL_PREFIX: &str = "codex.subagent."; -pub(crate) const CODEX_SUBAGENT_WORK_GRAPH_SCHEMA: &str = - "evalops.maestro.codex.subagent-workgraph.v1"; +pub const CODEX_SUBAGENT_TOOL_PREFIX: &str = "codex.subagent."; +pub const CODEX_SUBAGENT_WORK_GRAPH_SCHEMA: &str = "evalops.maestro.codex.subagent-workgraph.v1"; pub(crate) const SEMANTIC_CONVERSATION_PROTOCOL: &str = "evalops.maestro.semantic-conversation.v1"; /// Current headless protocol version shared with the TypeScript runtime. @@ -417,7 +416,7 @@ pub enum ToAgentMessage { } impl ToAgentMessage { - pub(crate) fn validate_managed_inference_authorization(&self) -> Result<(), &'static str> { + pub fn validate_managed_inference_authorization(&self) -> Result<(), &'static str> { let authorization = match self { Self::ManagedAuthorizationResult { authorization, .. } => Some(authorization), Self::Prompt { @@ -1474,11 +1473,13 @@ pub use state::{ ActiveFileWatch, ActiveTool, ActiveUtilityCommand, AgentEvent, AgentState, CodexSubagentContinuityEdge, GovernedClientToolBinding, PendingApproval, StreamingResponse, }; +pub use state::{ + CodexSubagentChildRun, active_codex_subagent_status, codex_subagent_child_runs, + codex_subagent_edge_key, codex_subagent_operation, codex_subagent_status_is_terminal, +}; #[allow(unused_imports)] pub(crate) use state::{ - CodexSubagentChildRun, HEADLESS_OUTPUT_LIMIT, active_codex_subagent_status, - codex_subagent_child_runs, codex_subagent_edge_key, codex_subagent_operation, - codex_subagent_status_is_terminal, json_string_array_from_object, json_string_from_object, + HEADLESS_OUTPUT_LIMIT, json_string_array_from_object, json_string_from_object, }; #[cfg(test)] diff --git a/packages/local-host-rs/src/headless/messages/state.rs b/packages/local-host-rs/src/headless/messages/state.rs index bf75f7505..c54eeb519 100644 --- a/packages/local-host-rs/src/headless/messages/state.rs +++ b/packages/local-host-rs/src/headless/messages/state.rs @@ -241,7 +241,7 @@ pub struct ActiveFileWatch { pub owner_connection_id: Option, } -pub(crate) fn codex_subagent_operation(tool: &str) -> Option<&'static str> { +pub fn codex_subagent_operation(tool: &str) -> Option<&'static str> { let suffix = tool .strip_prefix(CODEX_SUBAGENT_TOOL_PREFIX) .unwrap_or(tool); @@ -255,7 +255,7 @@ pub(crate) fn codex_subagent_operation(tool: &str) -> Option<&'static str> { } } -pub(crate) fn active_codex_subagent_status(operation: &str) -> &'static str { +pub fn active_codex_subagent_status(operation: &str) -> &'static str { match operation { "send_input" => "waiting_for_input_ack", "wait_agent" => "wait_pending", @@ -278,7 +278,7 @@ fn terminal_codex_subagent_status(operation: &str, success: bool) -> &'static st } } -pub(crate) fn codex_subagent_status_is_terminal(status: &str) -> bool { +pub fn codex_subagent_status_is_terminal(status: &str) -> bool { let normalized = status .trim() .to_ascii_lowercase() @@ -351,15 +351,13 @@ fn has_codex_work_graph(args: Option<&serde_json::Value>) -> bool { } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct CodexSubagentChildRun { +pub struct CodexSubagentChildRun { pub child_run_id: Option, pub thread_id: Option, pub status: Option, } -pub(crate) fn codex_subagent_child_runs( - args: Option<&serde_json::Value>, -) -> Vec { +pub fn codex_subagent_child_runs(args: Option<&serde_json::Value>) -> Vec { let Some(args) = args.and_then(serde_json::Value::as_object) else { return Vec::new(); }; @@ -482,7 +480,7 @@ fn codex_subagent_tool_end_args( source_args.cloned() } -pub(crate) fn codex_subagent_edge_key(edge: &CodexSubagentContinuityEdge) -> String { +pub fn codex_subagent_edge_key(edge: &CodexSubagentContinuityEdge) -> String { [ edge.spawn_tool_call_id.as_deref().unwrap_or_default(), edge.wait_tool_call_id.as_deref().unwrap_or_default(), diff --git a/packages/local-host-rs/src/headless/mod.rs b/packages/local-host-rs/src/headless/mod.rs index 01174be82..76b3b15ac 100644 --- a/packages/local-host-rs/src/headless/mod.rs +++ b/packages/local-host-rs/src/headless/mod.rs @@ -201,7 +201,7 @@ use tokio::sync::{Notify, mpsc}; mod async_transport; -pub(crate) mod controller_binding; +pub mod controller_binding; #[cfg(test)] mod controller_binding_test; mod framing; @@ -236,9 +236,10 @@ fn local_controller_capabilities() -> messages::ClientCapabilities { /// approval metadata. A client must not recreate this list or infer a /// per-call decision from it; the native tool event remains authoritative for /// argument-aware approval and action-firewall outcomes. -pub(crate) fn native_server_capabilities() -> messages::ServerCapabilities { - let mut native_tools = crate::tools::ToolRegistry::new() - .tools() +pub fn native_server_capabilities() -> messages::ServerCapabilities { + let mut native_tools = crate::agent::tool_host::tool_host() + .registry_tool_definitions() + .into_iter() .map(|definition| { let name = definition.tool.name.clone(); messages::NativeToolCapability { @@ -336,7 +337,7 @@ pub(crate) fn send_transport_event( // shared with `tools::registry::execute` and `agent::native`, not part of // this crate's public surface. pub(crate) use supervisor::report_diagnostic_nonblocking; -pub(crate) use supervisor::{ResponseAcknowledgement, response_ack_request_id}; +pub use supervisor::{ResponseAcknowledgement, response_ack_request_id}; // Message framing pub use framing::{ diff --git a/packages/local-host-rs/src/headless/supervisor.rs b/packages/local-host-rs/src/headless/supervisor.rs index 1b965233b..1a7bcf667 100644 --- a/packages/local-host-rs/src/headless/supervisor.rs +++ b/packages/local-host-rs/src/headless/supervisor.rs @@ -50,7 +50,7 @@ fn workspace_capability_replay_cursor(request: &ApplyWorkspaceCapabilitySet) -> } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ResponseAcknowledgement { +pub enum ResponseAcknowledgement { NotExpected, Consumed, Queued, @@ -464,7 +464,7 @@ pub struct AgentSupervisor { } impl AgentSupervisor { - pub(crate) async fn wait_for_response_acknowledgement_async( + pub async fn wait_for_response_acknowledgement_async( supervisor: Arc>, request_id: String, timeout: Duration, @@ -788,7 +788,7 @@ impl AgentSupervisor { /// Bind the local child to the immutable resident controller identity. /// The configured extension is replayed on every later child spawn. - pub(crate) fn set_local_controller_binding( + pub fn set_local_controller_binding( &mut self, binding: ControllerBindingHello, ) -> Result<(), AsyncTransportError> { @@ -830,7 +830,7 @@ impl AgentSupervisor { /// Send a message and report whether the native response consumer /// explicitly acknowledged accepting a control response. - pub(crate) fn send_and_drain_agent_messages_with_ack( + pub fn send_and_drain_agent_messages_with_ack( &mut self, msg: ToAgentMessage, ) -> Result<(Vec, ResponseAcknowledgement), AsyncTransportError> { @@ -946,22 +946,22 @@ impl AgentSupervisor { messages } - pub(crate) fn wait_for_response_acknowledgement( + pub fn wait_for_response_acknowledgement( &mut self, request_id: &str, ) -> (Vec, ResponseAcknowledgement) { self.drain_agent_messages_until_ack(Some(request_id), RESPONSE_ACK_TIMEOUT) } - pub(crate) fn has_response_acknowledgement(&self, request_id: &str) -> bool { + pub fn has_response_acknowledgement(&self, request_id: &str) -> bool { self.pending_response_acknowledgements.contains(request_id) } - pub(crate) fn has_response_rejection(&self, request_id: &str) -> bool { + pub fn has_response_rejection(&self, request_id: &str) -> bool { self.pending_response_rejections.contains_key(request_id) } - pub(crate) fn take_response_rejection(&mut self, request_id: &str) -> Option { + pub fn take_response_rejection(&mut self, request_id: &str) -> Option { let rejection = self.pending_response_rejections.remove(request_id); if rejection.is_some() { self.expected_response_acknowledgements.remove(request_id); @@ -970,7 +970,7 @@ impl AgentSupervisor { rejection } - pub(crate) fn take_response_acknowledgement(&mut self, request_id: &str) -> bool { + pub fn take_response_acknowledgement(&mut self, request_id: &str) -> bool { let acknowledged = self.pending_response_acknowledgements.remove(request_id); if acknowledged { self.expected_response_acknowledgements.remove(request_id); @@ -978,7 +978,7 @@ impl AgentSupervisor { acknowledged } - pub(crate) fn discard_response_acknowledgement( + pub fn discard_response_acknowledgement( &mut self, request_id: &str, transport_generation: u64, @@ -1014,13 +1014,13 @@ impl AgentSupervisor { true } - #[cfg(test)] - pub(crate) fn response_acknowledgement_count(&self) -> usize { + #[cfg(any(test, feature = "test-support"))] + pub fn response_acknowledgement_count(&self) -> usize { self.expected_response_acknowledgements.len() } #[must_use] - pub(crate) fn transport_generation(&self) -> u64 { + pub fn transport_generation(&self) -> u64 { self.transport_generation } @@ -1561,7 +1561,7 @@ impl AgentSupervisor { self.transport.is_some() } - pub(crate) fn event_notification(&self) -> Option> { + pub fn event_notification(&self) -> Option> { self.transport .as_ref() .map(ManagedTransport::event_notification) @@ -1596,7 +1596,7 @@ impl AgentSupervisor { } /// Shutdown the supervisor and wait for its active transport to be reaped. - pub(crate) async fn shutdown_and_wait(&mut self) { + pub async fn shutdown_and_wait(&mut self) { self.shutdown(); self.wait_for_pending_transport_shutdown().await; } @@ -1616,7 +1616,7 @@ impl AgentSupervisor { } } -pub(crate) fn response_ack_request_id(message: &ToAgentMessage) -> Option<&str> { +pub fn response_ack_request_id(message: &ToAgentMessage) -> Option<&str> { match message { ToAgentMessage::ToolResponse { call_id, .. } | ToAgentMessage::ClientToolResult { call_id, .. } diff --git a/packages/local-host-rs/src/headless/workspace_capabilities.rs b/packages/local-host-rs/src/headless/workspace_capabilities.rs index f0a11f9fb..b01da8988 100644 --- a/packages/local-host-rs/src/headless/workspace_capabilities.rs +++ b/packages/local-host-rs/src/headless/workspace_capabilities.rs @@ -144,7 +144,7 @@ pub(crate) fn accept_workspace_capability_receipt( } #[derive(Debug, Error, Eq, PartialEq)] -pub(crate) enum WorkspaceCapabilityError { +pub enum WorkspaceCapabilityError { #[error( "workspace prompt capabilities require an accepted Platform resident controller binding" )] @@ -551,8 +551,8 @@ fn capability_item_sha256( Ok(sha256_prefixed(canonical.as_bytes())) } -#[cfg(test)] -pub(crate) fn recompute_request_digests( +#[cfg(any(test, feature = "test-support"))] +pub fn recompute_request_digests( request: &mut ApplyWorkspaceCapabilitySet, ) -> Result<(), WorkspaceCapabilityError> { for capability in &mut request.admitted_catalog { diff --git a/packages/local-host-rs/src/headless_server.rs b/packages/local-host-rs/src/headless_server.rs index 302228d4e..ce47625e9 100644 --- a/packages/local-host-rs/src/headless_server.rs +++ b/packages/local-host-rs/src/headless_server.rs @@ -1015,8 +1015,8 @@ fn validate_governed_grant_shape(grant: &GovernedToolGrant) -> Result<()> { pub(crate) const GOVERNED_GRANT_ISSUER: &str = "evalops.platform"; pub(crate) const GOVERNED_GRANT_AUDIENCE: &str = "evalops.maestro"; const GOVERNED_GRANT_PUBLIC_KEYS_ENV: &str = "MAESTRO_PLATFORM_TOOL_GRANT_ED25519_PUBLIC_KEYS"; -#[cfg(test)] -pub(crate) static GOVERNED_GRANT_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); +#[cfg(any(test, feature = "test-support"))] +pub static GOVERNED_GRANT_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); #[derive(Debug, Clone, serde::Deserialize)] #[serde(deny_unknown_fields)] @@ -1041,7 +1041,7 @@ enum GovernedGrantPublicKeyState { } #[derive(Debug, Clone)] -pub(crate) struct GovernedGrantVerificationContext<'a> { +pub struct GovernedGrantVerificationContext<'a> { pub organization_id: &'a str, pub workspace_id: &'a str, pub thread_id: &'a str, @@ -1092,10 +1092,8 @@ fn governed_grant_canonical_bytes(grant: &GovernedToolGrant) -> Result> .context("serialize governed tool grant canonical payload") } -#[cfg(test)] -pub(crate) fn governed_tool_grant_canonical_bytes_for_test( - grant: &GovernedToolGrant, -) -> Result> { +#[cfg(any(test, feature = "test-support"))] +pub fn governed_tool_grant_canonical_bytes_for_test(grant: &GovernedToolGrant) -> Result> { governed_grant_canonical_bytes(grant) } @@ -1154,7 +1152,7 @@ fn governed_grant_keys_from_json(raw: &str) -> Result, now_ms: i64, @@ -3362,7 +3360,7 @@ fn managed_provider_override() -> Option { .filter(|provider| !provider.is_empty()) } -pub(crate) fn resolve_headless_model( +pub fn resolve_headless_model( model_override: Option, env: &HashMap, ) -> String { @@ -5548,10 +5546,10 @@ mod tests { let script = root.path().join("app-server.js"); let log = root.path().join("app-server.log"); let output = root.path().join("headless.log"); - // This fixture uses only loopback HTTP. Do not enumerate the developer's - // macOS certificate Keychain while building its Identity client. + // Loopback HTTP only: one throwaway root, because the platform verifier rejects an empty file. let roots = root.path().join("roots.pem"); - std::fs::write(&roots, "").expect("isolated certificate roots"); + std::fs::write(&roots, include_str!("../tests/isolated_root.pem")) + .expect("isolated certificate roots"); let home = root.path().join("maestro-home"); std::fs::write( &script, diff --git a/packages/local-host-rs/src/lib.rs b/packages/local-host-rs/src/lib.rs index ed992ab7f..05639e471 100644 --- a/packages/local-host-rs/src/lib.rs +++ b/packages/local-host-rs/src/lib.rs @@ -77,9 +77,6 @@ pub mod video; pub mod workflow_runtime; pub use sandbox::SandboxPolicy; -pub mod hosted_runner; -pub mod hosted_runner_cli; -pub mod hosted_runner_conformance; pub mod subagents; /// Generated bindings for the independently owned public application contract. diff --git a/packages/local-host-rs/src/private_code_authority.rs b/packages/local-host-rs/src/private_code_authority.rs index 102069c9a..a122aaabf 100644 --- a/packages/local-host-rs/src/private_code_authority.rs +++ b/packages/local-host-rs/src/private_code_authority.rs @@ -41,10 +41,7 @@ pub(crate) fn load() -> Result> { .redirect(reqwest::redirect::Policy::none()) .timeout(TIMEOUT); if let Some(certificates) = certificates(&config)? { - builder = builder.use_rustls_tls().tls_built_in_root_certs(false); - for certificate in certificates { - builder = builder.add_root_certificate(certificate); - } + builder = builder.tls_backend_rustls().tls_certs_only(certificates); } Ok(Some(CustomerAuthority { http: builder.build()?, @@ -180,10 +177,7 @@ fn verify( .redirect(reqwest::redirect::Policy::none()) .timeout(TIMEOUT); if let Some(certificates) = certificates(&config)? { - builder = builder.use_rustls_tls().tls_built_in_root_certs(false); - for certificate in certificates { - builder = builder.add_root_certificate(certificate); - } + builder = builder.tls_backend_rustls().tls_certs_only(certificates); } let response = builder .build()? diff --git a/packages/local-host-rs/src/subagents/mod.rs b/packages/local-host-rs/src/subagents/mod.rs index d8bffabe3..27f532f70 100644 --- a/packages/local-host-rs/src/subagents/mod.rs +++ b/packages/local-host-rs/src/subagents/mod.rs @@ -48,7 +48,6 @@ use crate::orb_connection::HostedOrbOwnerBinding; use crate::sandbox::SandboxPolicy; use crate::session::{SessionLock, sanitize_path_for_dirname}; use crate::state::ApprovalMode; -use crate::tools::ToolRegistry; use crate::worktree::WorktreeSession; use crate::tools::orb_delegation::{ @@ -3059,7 +3058,7 @@ impl SubagentManager { Ok(timeout_ms) => timeout_ms, Err(error) => return ToolResult::failure(error), }; - let deadline = Instant::now() + Duration::from_millis(timeout_ms); + let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms); let steer_signal = self.steer_signal(); loop { @@ -3067,14 +3066,17 @@ impl SubagentManager { Ok(record) => record, Err(error) => return ToolResult::failure(error), }; - if record.status.is_terminal() || timeout_ms == 0 || Instant::now() >= deadline { + if record.status.is_terminal() + || timeout_ms == 0 + || tokio::time::Instant::now() >= deadline + { return tool_result_for_record(record); } if cancel.is_some_and(CancellationToken::is_cancelled) { return cancelled_result("wait_subagent cancelled"); } let (record, slice) = if record.backend == SubagentBackend::Orb { - let remaining = deadline.saturating_duration_since(Instant::now()); + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); let refresh_cancel = cancel.map_or_else(CancellationToken::new, CancellationToken::child_token); let refresh = self.refresh_orb_record(record.clone(), Some(&refresh_cancel)); @@ -3090,14 +3092,14 @@ impl SubagentManager { } else { (record, Duration::from_millis(50)) }; - if record.status.is_terminal() || Instant::now() >= deadline { + if record.status.is_terminal() || tokio::time::Instant::now() >= deadline { return tool_result_for_record(record); } if cancel.is_some_and(CancellationToken::is_cancelled) { return cancelled_result("wait_subagent cancelled"); } - let remaining = deadline.saturating_duration_since(Instant::now()); + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); match sleep_or_release(remaining.min(slice), cancel, steer_signal.as_deref()).await { WaitWake::Elapsed => {} WaitWake::Cancelled => return cancelled_result("wait_subagent cancelled"), @@ -4891,8 +4893,9 @@ fn child_allowed_tools_for_role( role: SubagentRole, profile_tools: Option<&[String]>, ) -> HashSet { - let mut tools = ToolRegistry::new() - .tools() + let mut tools = crate::agent::tool_host::tool_host() + .registry_tool_definitions() + .into_iter() .filter(|definition| { let name = definition.tool.name.to_ascii_lowercase(); let globally_allowed = !SUBAGENT_TOOL_NAMES.contains(&name.as_str()) @@ -5987,6 +5990,7 @@ fn convert_usage(usage: &crate::agent::TokenUsage) -> crate::headless::TokenUsag #[cfg(test)] mod tests { + mod wait_timeouts; use std::sync::Mutex; use futures::future::BoxFuture; @@ -6590,49 +6594,6 @@ mod tests { ); } - #[tokio::test] - async fn orb_wait_bounds_remote_refresh_by_remaining_timeout() { - let root = tempfile::tempdir().expect("temp root"); - let manager = - SubagentManager::with_root(root.path().to_path_buf(), root.path().join("records")); - let binding = owner_binding("org-a", "workspace-a", "connection-a", 1); - let caller = OwnerBindingCaller::with_status_delay("active", &[], Duration::from_secs(5)); - manager - .set_orb_adapter(OrbDelegationAdapter::from_caller_with_owner_binding( - caller.clone(), - binding.clone(), - )) - .await; - let record = running_orb_record(root.path(), &binding); - manager - .write_record(&record) - .expect("persist Computer record"); - - let result = tokio::time::timeout( - Duration::from_secs(1), - manager.wait( - &serde_json::json!({ - "subagent_id": record.id, - "timeout_ms": 25, - }), - None, - ), - ) - .await - .expect("wait deadline must bound a slow remote refresh"); - - assert!(result.success, "the cached running snapshot remains valid"); - assert_eq!( - caller - .calls - .lock() - .expect("owner caller calls lock") - .as_slice(), - ["orb_task_status"], - "a timed-out status refresh must not proceed to collect" - ); - } - #[tokio::test] async fn orb_resume_rejects_all_terminal_lifecycle_aliases_without_sending() { for lifecycle_state in [ diff --git a/packages/local-host-rs/src/subagents/tests/wait_timeouts.rs b/packages/local-host-rs/src/subagents/tests/wait_timeouts.rs new file mode 100644 index 000000000..77e7168b4 --- /dev/null +++ b/packages/local-host-rs/src/subagents/tests/wait_timeouts.rs @@ -0,0 +1,84 @@ +use super::*; + +#[tokio::test(start_paused = true)] +async fn orb_wait_bounds_remote_refresh_by_remaining_timeout() { + let root = tempfile::tempdir().expect("temp root"); + let manager = + SubagentManager::with_root(root.path().to_path_buf(), root.path().join("records")); + let binding = owner_binding("org-a", "workspace-a", "connection-a", 1); + let caller = OwnerBindingCaller::with_status_delay("active", &[], Duration::from_secs(5)); + manager + .set_orb_adapter(OrbDelegationAdapter::from_caller_with_owner_binding( + caller.clone(), + binding.clone(), + )) + .await; + let record = running_orb_record(root.path(), &binding); + manager + .write_record(&record) + .expect("persist Computer record"); + + let result = tokio::time::timeout( + Duration::from_secs(1), + manager.wait( + &serde_json::json!({ + "subagent_id": record.id, + "timeout_ms": 25, + }), + None, + ), + ) + .await + .expect("wait deadline must bound a slow remote refresh"); + + assert!(result.success, "the cached running snapshot remains valid"); + assert_eq!( + caller + .calls + .lock() + .expect("owner caller calls lock") + .as_slice(), + ["orb_task_status"], + "a timed-out status refresh must not proceed to collect" + ); +} + +#[tokio::test(start_paused = true)] +async fn orb_wait_counts_completed_refresh_and_sleep_on_the_same_clock() { + let root = tempfile::tempdir().expect("temp root"); + let manager = + SubagentManager::with_root(root.path().to_path_buf(), root.path().join("records")); + let binding = owner_binding("org-a", "workspace-a", "connection-a", 1); + let caller = OwnerBindingCaller::with_status_delay("active", &[], Duration::ZERO); + manager + .set_orb_adapter(OrbDelegationAdapter::from_caller_with_owner_binding( + caller.clone(), + binding.clone(), + )) + .await; + let record = running_orb_record(root.path(), &binding); + manager + .write_record(&record) + .expect("persist Computer record"); + + let started = tokio::time::Instant::now(); + let result = tokio::time::timeout( + Duration::from_secs(1), + manager.wait( + &serde_json::json!({ + "subagent_id": record.id, + "timeout_ms": 25, + }), + None, + ), + ) + .await + .expect("wait deadline must bound a slow remote refresh"); + + assert!(result.success, "the cached running snapshot remains valid"); + assert_eq!( + tokio::time::Instant::now().duration_since(started), + Duration::from_millis(25), + "completed refresh and sleep must share the original wait budget" + ); +} diff --git a/packages/local-host-rs/src/tools/process_utils.rs b/packages/local-host-rs/src/tools/process_utils.rs index cefbc7d5b..3546d5140 100644 --- a/packages/local-host-rs/src/tools/process_utils.rs +++ b/packages/local-host-rs/src/tools/process_utils.rs @@ -386,7 +386,8 @@ mod tests { let mut line = String::new(); reader.read_line(&mut line).await.expect("descendant pid"); let descendant: i32 = line.trim().parse().expect("pid"); - tokio::time::timeout(std::time::Duration::from_secs(2), async { + let cleanup_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2); + tokio::time::timeout_at(cleanup_deadline, async { loop { let stat = std::fs::read_to_string(format!("/proc/{root}/stat")).expect("direct child"); @@ -408,6 +409,21 @@ mod tests { Some(7) ); drop(guard); + // Late orphan adoption is handled by the owned asynchronous reaper. + // Await it within the original shared cleanup budget. + tokio::time::timeout_at(cleanup_deadline, async { + loop { + // SAFETY: signal 0 only probes this test's recorded descendant. + if unsafe { libc::kill(descendant, 0) } == -1 + && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) + { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + } + }) + .await + .expect("owned asynchronous reaper removes the descendant"); // Reaping, not merely SIGKILL: signal 0 must see no zombie either. // SAFETY: signal 0 only probes this test's recorded descendant. assert_eq!(unsafe { libc::kill(descendant, 0) }, -1); diff --git a/packages/local-host-rs/src/tools/registry/tests.rs b/packages/local-host-rs/src/tools/registry/tests.rs index 68772c2a3..b34f97128 100644 --- a/packages/local-host-rs/src/tools/registry/tests.rs +++ b/packages/local-host-rs/src/tools/registry/tests.rs @@ -1,7 +1,11 @@ use super::*; mod explore; +#[cfg(not(windows))] +mod mcp_counter_fixture; mod streaming_order; +#[cfg(not(windows))] +use mcp_counter_fixture::{failing_counter_server, read_counter}; use std::path::Path; use crate::agent::{ @@ -330,35 +334,6 @@ fn collect_openai_schema_issues(value: &serde_json::Value, path: &str, issues: & } } -#[cfg(not(windows))] -fn failing_counter_server(server_name: &str, counter_path: &Path) -> serde_json::Value { - serde_json::json!({ - "name": server_name, - "transport": "stdio", - "command": "sh", - // Leave enough headroom for a loaded parallel test process to - // schedule the child before the handshake timeout expires. - "timeout": 500, - // These tests exercise retry/reload mechanics, not the workspace - // trust gate, so opt the server out of the approval requirement. - "requiresProjectApproval": false, - "args": [ - "-c", - "count=$(cat \"$1\" 2>/dev/null || echo 0); echo $((count + 1)) > \"$1\"; exit 1", - "sh", - counter_path.display().to_string() - ] - }) -} - -#[cfg(not(windows))] -fn read_counter(counter_path: &Path) -> usize { - std::fs::read_to_string(counter_path) - .ok() - .and_then(|value| value.trim().parse::().ok()) - .unwrap_or(0) -} - struct TrustedWorkspaceGuard { _lock: tokio::sync::OwnedMutexGuard<()>, previous_home: Option, diff --git a/packages/local-host-rs/src/tools/registry/tests/mcp_counter_fixture.rs b/packages/local-host-rs/src/tools/registry/tests/mcp_counter_fixture.rs new file mode 100644 index 000000000..c85c8792e --- /dev/null +++ b/packages/local-host-rs/src/tools/registry/tests/mcp_counter_fixture.rs @@ -0,0 +1,59 @@ +use std::path::Path; + +pub(super) fn failing_counter_server(server_name: &str, counter_path: &Path) -> serde_json::Value { + serde_json::json!({ + "name": server_name, + "transport": "stdio", + "command": "sh", + // Leave enough headroom for a loaded parallel test process to + // schedule the child before the handshake timeout expires. + "timeout": 500, + // These tests exercise retry/reload mechanics, not the workspace + // trust gate, so opt the server out of the approval requirement. + "requiresProjectApproval": false, + "args": [ + "-c", + // Record the attempt using shell builtins before failing. A + // separate `cat` process can outlive the handshake deadline on + // a loaded host, leaving the attempt uncounted when the shell + // is killed. The retry assertions require the counter itself + // to be independent of external executable scheduling. + "count=0; if [ -f \"$1\" ]; then IFS= read -r count < \"$1\"; fi; printf '%s\\n' \"$((count + 1))\" > \"$1\"; exit 1", + "sh", + counter_path.display().to_string() + ] + }) +} + +pub(super) fn read_counter(counter_path: &Path) -> usize { + std::fs::read_to_string(counter_path) + .ok() + .and_then(|value| value.trim().parse::().ok()) + .unwrap_or(0) +} + +#[test] +fn failing_counter_server_does_not_require_external_path_utilities() { + let temp = tempfile::tempdir().expect("counter fixture directory"); + let counter_path = temp.path().join("counter.txt"); + let fixture = failing_counter_server("counter-fixture", &counter_path); + let shell = std::env::split_paths(&std::env::var_os("PATH").expect("shell search path")) + .map(|directory| directory.join("sh")) + .find(|candidate| candidate.is_file()) + .expect("test shell"); + let args: Vec<_> = fixture["args"] + .as_array() + .expect("fixture arguments") + .iter() + .map(|argument| argument.as_str().expect("string argument")) + .collect(); + for expected in 1..=2 { + let output = std::process::Command::new(&shell) + .args(&args) + .env("PATH", temp.path()) + .output() + .expect("run failing server fixture"); + assert_eq!(output.status.code(), Some(1), "fixture must still fail"); + assert_eq!(read_counter(&counter_path), expected); + } +} diff --git a/packages/local-host-rs/src/transcript.rs b/packages/local-host-rs/src/transcript.rs index d63a43d7a..50dd38ad0 100644 --- a/packages/local-host-rs/src/transcript.rs +++ b/packages/local-host-rs/src/transcript.rs @@ -151,7 +151,7 @@ fn is_credential_key(key: &str) -> bool { .any(|metric| normalized.contains(metric)) } -pub(crate) fn redact_agent_message( +pub fn redact_agent_message( message: crate::headless::messages::FromAgentMessage, ) -> crate::headless::messages::FromAgentMessage { let Ok(value) = serde_json::to_value(&message) else { @@ -178,7 +178,7 @@ pub(crate) fn redact_agent_message( redacted_message } -pub(crate) fn agent_message_for_controller( +pub fn agent_message_for_controller( message: crate::headless::messages::FromAgentMessage, ) -> crate::headless::messages::FromAgentMessage { if matches!( diff --git a/packages/local-host-rs/tests/isolated_root.pem b/packages/local-host-rs/tests/isolated_root.pem new file mode 100644 index 000000000..08d35ca89 --- /dev/null +++ b/packages/local-host-rs/tests/isolated_root.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqDCCAU+gAwIBAgIUf+lDYExCpbWssZTa5ylFcbqdX3MwCgYIKoZIzj0EAwIw +KTEnMCUGA1UEAwweaGVhZGxlc3MgZml4dHVyZSBpc29sYXRlZCByb290MCAXDTI2 +MTAxMjA4MTI0M1oYDzIxMjYwOTE4MDgxMjQzWjApMScwJQYDVQQDDB5oZWFkbGVz +cyBmaXh0dXJlIGlzb2xhdGVkIHJvb3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AARa8W3bTsjpCod0TYplXhxyaoG9QrVcI3AVRUrHEdBGPb+ZrDAd8UTJRXkZIXTr +wGYt6N7pENxXNp6+4WE/P6XCo1MwUTAdBgNVHQ4EFgQUvrTgW4+ylVyynLQCOJNs +8Aa6dqEwHwYDVR0jBBgwFoAUvrTgW4+ylVyynLQCOJNs8Aa6dqEwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiBTNoCUVy08D0e1LOowhYnIFoUpLn+0 +5Z86ZBfEUpSwRwIgNqh1rIZZo2shb10R/97kBqcIWcMDvKqtWwKVc9lqENE= +-----END CERTIFICATE----- diff --git a/packages/maestro-rs/Cargo.toml b/packages/maestro-rs/Cargo.toml index 37710dc45..31a5d042b 100644 --- a/packages/maestro-rs/Cargo.toml +++ b/packages/maestro-rs/Cargo.toml @@ -29,6 +29,7 @@ anyhow.workspace = true ctor.workspace = true maestro-runtime-gateway.workspace = true maestro-local-host.workspace = true +maestro-hosted-runner-lib.workspace = true maestro-runtime.workspace = true maestro-tui.workspace = true serde_json.workspace = true diff --git a/packages/maestro-rs/src/hosted_companion.rs b/packages/maestro-rs/src/hosted_companion.rs index b51177aa9..dc1a076dc 100644 --- a/packages/maestro-rs/src/hosted_companion.rs +++ b/packages/maestro-rs/src/hosted_companion.rs @@ -1,6 +1,6 @@ //! The opt-in coding companion shares the hosted resident's supervised lifetime. use anyhow::{Context, Result, bail}; -use maestro_local_host::hosted_runner_cli::resolve_hosted_runner_launch_config; +use maestro_hosted_runner_lib::hosted_runner_cli::resolve_hosted_runner_launch_config; use std::{collections::HashMap, ffi::OsString, future::Future, net::TcpListener, path::PathBuf}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, diff --git a/packages/maestro-rs/src/native_effect.rs b/packages/maestro-rs/src/native_effect.rs index 229c86e7b..2c86d722e 100644 --- a/packages/maestro-rs/src/native_effect.rs +++ b/packages/maestro-rs/src/native_effect.rs @@ -73,13 +73,10 @@ pub(crate) fn run(args: &[OsString]) -> Result { ensure_repository(&cwd)?; // The execution owner fixes production cwd to /workspace. Tests use their // own temporary worktree through the same snapshot and command path. - let root = std::env::temp_dir().join("maestro-native-effects"); - std::fs::create_dir_all(&root)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?; - } + let root = maestro_local_host::sandbox::prepare_private_temp_directory( + &std::env::temp_dir(), + "maestro-native-effects", + )?; let pending = begin_or_resume(&cwd, &root, key); let mut child = Command::new("/bin/sh") .args(["-c", command]) diff --git a/packages/maestro-rs/tests/native_effect.rs b/packages/maestro-rs/tests/native_effect.rs index 9446876eb..057ceb9bd 100644 --- a/packages/maestro-rs/tests/native_effect.rs +++ b/packages/maestro-rs/tests/native_effect.rs @@ -157,3 +157,119 @@ fn first_approved_command_in_blank_workspace_saves_changes_but_broken_git_is_pre ); fs::remove_dir_all(cwd).unwrap(); } + +#[test] +fn persisted_baselines_keep_preimages_when_legacy_roots_appear_or_persist() { + use maestro_local_host::checkpoints::{finalize_turn_snapshot, native_effect::begin_or_resume}; + use std::os::unix::fs::PermissionsExt as _; + + for legacy_first in [true, false] { + let temp = std::env::temp_dir().join(format!( + "maestro-native-legacy-{}-{legacy_first}", + std::process::id() + )); + fs::create_dir(&temp).unwrap(); + fs::set_permissions(&temp, fs::Permissions::from_mode(0o700)).unwrap(); + let cwd = temp.join("repo"); + fs::create_dir(&cwd).unwrap(); + git(&cwd, &["init", "-q"]); + fs::write(cwd.join("source.txt"), "before\n").unwrap(); + git(&cwd, &["add", "."]); + git( + &cwd, + &[ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-qm", + "initial", + ], + ); + let legacy = temp.join("maestro-native-effects"); + if legacy_first { + fs::create_dir(&legacy).unwrap(); + fs::set_permissions(&legacy, fs::Permissions::from_mode(0o700)).unwrap(); + } + let root = maestro_local_host::sandbox::prepare_private_temp_directory( + &temp, + "maestro-native-effects", + ) + .unwrap(); + let key = maestro_runtime_contracts::native_code::checkpoint_key( + "org", + "workspace", + &cwd.to_string_lossy(), + ); + let pending = begin_or_resume(&cwd, &root, &key).unwrap(); + fs::write(cwd.join("source.txt"), "first effect\n").unwrap(); + finalize_turn_snapshot(pending).unwrap().unwrap(); + if !legacy_first { + // An older binary may create its current-UID legacy directory after + // the new binary has already persisted the turn's first baseline. + fs::create_dir(&legacy).unwrap(); + fs::set_permissions(&legacy, fs::Permissions::from_mode(0o700)).unwrap(); + } + + let output = Command::new(maestro_binary()) + .args(["native-code-effect", &key, "printf 'after\\n' > source.txt"]) + .env("TMPDIR", &temp) + .current_dir(&cwd) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let receipt: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let file = receipt["changes"]["files"] + .as_array() + .unwrap() + .iter() + .find(|file| file["path"] == "source.txt") + .unwrap(); + assert_eq!(file["beforeContent"], "before\n"); + assert_eq!(file["afterContent"], "after\n"); + assert_eq!( + maestro_local_host::sandbox::prepare_private_temp_directory( + &temp, + "maestro-native-effects" + ) + .unwrap(), + root + ); + fs::remove_dir_all(temp).unwrap(); + } +} + +#[test] +fn native_checkpoint_root_symlink_is_rejected_before_the_command() { + use std::os::unix::fs::{PermissionsExt as _, symlink}; + + let temp = + std::env::temp_dir().join(format!("maestro-native-root-link-{}", std::process::id())); + fs::create_dir(&temp).unwrap(); + fs::set_permissions(&temp, fs::Permissions::from_mode(0o700)).unwrap(); + let cwd = temp.join("repo"); + fs::create_dir(&cwd).unwrap(); + let target = temp.join("other-root"); + fs::create_dir(&target).unwrap(); + symlink(&target, temp.join("maestro-native-effects")).unwrap(); + let key = maestro_runtime_contracts::native_code::checkpoint_key( + "org", + "workspace", + &cwd.to_string_lossy(), + ); + let output = Command::new(maestro_binary()) + .args(["native-code-effect", &key, "printf 'unexpected' > marker"]) + .env("TMPDIR", &temp) + .current_dir(&cwd) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!(!cwd.join("marker").exists()); + assert_eq!(fs::read_dir(target).unwrap().count(), 0); + fs::remove_dir_all(temp).unwrap(); +} diff --git a/packages/runtime-gateway-rs/Cargo.toml b/packages/runtime-gateway-rs/Cargo.toml index cd1dc4d9e..e8c175cf4 100644 --- a/packages/runtime-gateway-rs/Cargo.toml +++ b/packages/runtime-gateway-rs/Cargo.toml @@ -27,7 +27,7 @@ maestro-dex-host.workspace = true maestro-local-host = { workspace = true, features = ["runtime-gateway-bridge"] } pdf-extract.workspace = true prost.workspace = true -reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] } +reqwest = { workspace = true, features = ["blocking", "json", "rustls"] } serde.workspace = true serde_json.workspace = true sha1 = "0.11" diff --git a/packages/runtime-gateway-rs/src/tests.rs b/packages/runtime-gateway-rs/src/tests.rs index 04b88957a..dd35926ff 100644 --- a/packages/runtime-gateway-rs/src/tests.rs +++ b/packages/runtime-gateway-rs/src/tests.rs @@ -2481,82 +2481,6 @@ rl.on("line", (line) => { ); } -#[tokio::test] -async fn codex_headless_bridge_bounds_shutdown_wait_after_valid_output() { - let _guard = ENV_LOCK.lock().await; - let root = TestDir::new("codex-headless-hung-shutdown"); - let cli_path = root.path().join("cli.js"); - fs::write( - &cli_path, - r#"const readline = require("readline"); -const rl = readline.createInterface({ input: process.stdin, terminal: false }); -function send(value) { - process.stdout.write(`${JSON.stringify(value)}\n`); -} -rl.on("line", (line) => { - const msg = JSON.parse(line); - if (msg.type === "prompt") { - send({ type: "response_start", response_id: "response-1" }); - send({ type: "response_chunk", response_id: "response-1", content: "bounded output", is_thinking: false }); - send({ type: "response_end", response_id: "response-1", duration_ms: 1 }); - send({ type: "turn_completed", response_id: "response-1" }); - } else if (msg.type === "shutdown") { - setInterval(() => {}, 1000); - } -}); -"#, - ) - .expect("cli fixture should be written"); - let previous_cli = env::var_os("MAESTRO_CODEX_APP_SERVER_CLI"); - let previous_timeout = env::var_os("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS"); - let previous_shutdown_timeout = env::var_os("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS"); - env::set_var("MAESTRO_CODEX_APP_SERVER_CLI", &cli_path); - env::set_var("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS", "5000"); - env::set_var("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS", "50"); - - let state = test_app_state_with_sessions(HashMap::new()); - let (_client, mut server) = tcp_stream_pair().await; - let started = Instant::now(); - let output = run_codex_app_server_headless_cli( - &mut server, - CodexBridgeTransport::Sse, - &state, - None, - root.path(), - "gpt-5.5", - "hello", - &[], - ) - .await; - - if let Some(previous) = previous_cli { - env::set_var("MAESTRO_CODEX_APP_SERVER_CLI", previous); - } else { - env::remove_var("MAESTRO_CODEX_APP_SERVER_CLI"); - } - if let Some(previous) = previous_timeout { - env::set_var("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS", previous); - } else { - env::remove_var("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS"); - } - if let Some(previous) = previous_shutdown_timeout { - env::set_var("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS", previous); - } else { - env::remove_var("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS"); - } - - assert_eq!( - output - .expect("valid output should survive a hung shutdown") - .text, - "bounded output" - ); - assert!( - started.elapsed() < Duration::from_secs(2), - "hung shutdown should be bounded" - ); -} - #[tokio::test] async fn codex_app_server_timeout_kills_child_process() { let _guard = ENV_LOCK.lock().await; @@ -14298,3 +14222,6 @@ mod native_headless_contract; #[path = "tests/session_persistence.rs"] mod session_persistence_tests; + +#[path = "tests/codex_bridge_shutdown.rs"] +mod codex_bridge_shutdown; diff --git a/packages/runtime-gateway-rs/src/tests/codex_bridge_shutdown.rs b/packages/runtime-gateway-rs/src/tests/codex_bridge_shutdown.rs new file mode 100644 index 000000000..10191139a --- /dev/null +++ b/packages/runtime-gateway-rs/src/tests/codex_bridge_shutdown.rs @@ -0,0 +1,100 @@ +use super::*; + +#[tokio::test] +async fn codex_headless_bridge_bounds_shutdown_wait_after_valid_output() { + assert_bounded_shutdown_after_valid_output(0).await; +} + +#[tokio::test] +async fn codex_headless_bridge_shutdown_budget_excludes_allowed_response_delay() { + assert_bounded_shutdown_after_valid_output(3000).await; +} + +async fn assert_bounded_shutdown_after_valid_output(response_delay_ms: u64) { + let _guard = ENV_LOCK.lock().await; + let root = TestDir::new("codex-headless-hung-shutdown"); + let cli_path = root.path().join("cli.js"); + let completion_marker = root.path().join("turn-completed"); + fs::write( + &cli_path, + r#"const fs = require("fs"); +const readline = require("readline"); +const rl = readline.createInterface({ input: process.stdin, terminal: false }); +function send(value) { + process.stdout.write(`${JSON.stringify(value)}\n`); +} +rl.on("line", (line) => { + const msg = JSON.parse(line); + if (msg.type === "prompt") { + const respond = () => { + send({ type: "response_start", response_id: "response-1" }); + send({ type: "response_chunk", response_id: "response-1", content: "bounded output", is_thinking: false }); + send({ type: "response_end", response_id: "response-1", duration_ms: 1 }); + fs.writeFileSync(COMPLETION_MARKER, "completed"); + send({ type: "turn_completed", response_id: "response-1" }); + }; + if (RESPONSE_DELAY_MS === 0) respond(); + else setTimeout(respond, RESPONSE_DELAY_MS); + } else if (msg.type === "shutdown") { + setInterval(() => {}, 1000); + } +}); +"# + .replace("COMPLETION_MARKER", &serde_json::to_string(completion_marker.to_str().expect("completion marker path")).expect("serialize completion marker path")) + .replace("RESPONSE_DELAY_MS", &response_delay_ms.to_string()), + ) + .expect("cli fixture should be written"); + let previous_cli = env::var_os("MAESTRO_CODEX_APP_SERVER_CLI"); + let previous_timeout = env::var_os("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS"); + let previous_shutdown_timeout = env::var_os("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS"); + env::set_var("MAESTRO_CODEX_APP_SERVER_CLI", &cli_path); + env::set_var("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS", "5000"); + env::set_var("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS", "50"); + + let state = test_app_state_with_sessions(HashMap::new()); + let (_client, mut server) = tcp_stream_pair().await; + let output = run_codex_app_server_headless_cli( + &mut server, + CodexBridgeTransport::Sse, + &state, + None, + root.path(), + "gpt-5.5", + "hello", + &[], + ) + .await; + + if let Some(previous) = previous_cli { + env::set_var("MAESTRO_CODEX_APP_SERVER_CLI", previous); + } else { + env::remove_var("MAESTRO_CODEX_APP_SERVER_CLI"); + } + if let Some(previous) = previous_timeout { + env::set_var("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS", previous); + } else { + env::remove_var("MAESTRO_CODEX_APP_SERVER_TIMEOUT_MS"); + } + if let Some(previous) = previous_shutdown_timeout { + env::set_var("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS", previous); + } else { + env::remove_var("MAESTRO_CODEX_APP_SERVER_SHUTDOWN_TIMEOUT_MS"); + } + + assert_eq!( + output + .expect("valid output should survive a hung shutdown") + .text, + "bounded output" + ); + assert!( + fs::metadata(&completion_marker) + .expect("valid turn must record its completion boundary") + .modified() + .expect("completion marker must have a timestamp") + .elapsed() + .expect("completion boundary must not be in the future") + < Duration::from_secs(2), + "hung shutdown should be bounded" + ); +} diff --git a/packages/sandbox-rs/src/lib.rs b/packages/sandbox-rs/src/lib.rs index 6c4265f55..cff42606f 100644 --- a/packages/sandbox-rs/src/lib.rs +++ b/packages/sandbox-rs/src/lib.rs @@ -162,6 +162,9 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use thiserror::Error; +mod private_temp; +pub use private_temp::prepare_private_temp_directory; + // ───────────────────────────────────────────────────────────── // Sandbox Policy Types // ───────────────────────────────────────────────────────────── @@ -383,10 +386,8 @@ pub fn sandbox_cache_session_id() -> &'static str { #[must_use] pub fn sandbox_cache_root(session_id: &str) -> PathBuf { let temp_dir = std::env::temp_dir(); - dunce::canonicalize(&temp_dir) - .unwrap_or(temp_dir) - .join(SANDBOX_CACHE_DIR) - .join(session_id) + let temp_dir = dunce::canonicalize(&temp_dir).unwrap_or(temp_dir); + private_temp::private_temp_directory_path(&temp_dir, SANDBOX_CACHE_DIR).join(session_id) } fn prepare_sandbox_cache_root(session_id: &str) -> std::io::Result { @@ -406,10 +407,7 @@ fn prepare_sandbox_cache_root_in(temp_dir: &Path, session_id: &str) -> std::io:: )); } - let temp_dir = dunce::canonicalize(temp_dir)?; - ensure_trusted_temp_directory(&temp_dir)?; - let parent = temp_dir.join(SANDBOX_CACHE_DIR); - ensure_private_directory(&parent)?; + let parent = prepare_private_temp_directory(temp_dir, SANDBOX_CACHE_DIR)?; reclaim_inactive_sandbox_caches(&parent, session_id)?; let root = parent.join(session_id); diff --git a/packages/sandbox-rs/src/private_temp.rs b/packages/sandbox-rs/src/private_temp.rs new file mode 100644 index 000000000..cb66e5ac6 --- /dev/null +++ b/packages/sandbox-rs/src/private_temp.rs @@ -0,0 +1,121 @@ +//! Private temp roots shared by caches and native checkpoint storage. +use std::path::{Path, PathBuf}; + +/// Prepare an owner-only root without taking over another user's legacy root. +/// Existing roots owned by this user retain their path so persisted native +/// checkpoints keep their original first-command baseline. +pub fn prepare_private_temp_directory(temp_dir: &Path, name: &str) -> std::io::Result { + if Path::new(name).components().count() != 1 + || !matches!( + Path::new(name).components().next(), + Some(std::path::Component::Normal(_)) + ) + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "private temp directory name must be one path component", + )); + } + let temp_dir = dunce::canonicalize(temp_dir)?; + super::ensure_trusted_temp_directory(&temp_dir)?; + let root = private_temp_directory_path(&temp_dir, name); + super::ensure_private_directory(&root)?; + Ok(root) +} + +pub(super) fn private_temp_directory_path(temp_dir: &Path, name: &str) -> PathBuf { + #[cfg(unix)] + { + directory_path_for_uid(temp_dir, name, unsafe { libc::geteuid() }) + } + #[cfg(not(unix))] + { + temp_dir.join(name) + } +} + +#[cfg(unix)] +fn directory_path_for_uid(temp_dir: &Path, name: &str, uid: u32) -> PathBuf { + use std::os::unix::fs::MetadataExt as _; + let current = temp_dir.join(format!("{name}-{uid}")); + // Once selected, this root owns the persisted first-command baseline. + // A legacy root appearing or changing ownership must not replace it. + match std::fs::symlink_metadata(¤t) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + _ => return current, + } + let legacy = temp_dir.join(name); + match std::fs::symlink_metadata(&legacy) { + // Keep malformed and current-owner paths subject to the existing + // no-follow, owner and mode checks; only a foreign directory is ignored. + Ok(metadata) if !metadata.is_dir() || metadata.uid() == uid => legacy, + Ok(_) => current, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => current, + Err(_) => legacy, + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _, symlink}; + + #[test] + fn foreign_legacy_owner_selects_a_separate_stable_root() { + let temp = tempfile::tempdir().unwrap(); + let legacy = temp.path().join("effects"); + std::fs::create_dir(&legacy).unwrap(); + std::fs::set_permissions(&legacy, std::fs::Permissions::from_mode(0o700)).unwrap(); + std::fs::write(legacy.join("baseline"), b"original").unwrap(); + let owner = std::fs::metadata(&legacy).unwrap().uid(); + let other_uid = owner.checked_add(1).unwrap(); + let selected = directory_path_for_uid(temp.path(), "effects", other_uid); + assert_eq!(selected, temp.path().join(format!("effects-{other_uid}"))); + assert_eq!( + selected, + directory_path_for_uid(temp.path(), "effects", other_uid) + ); + assert_eq!(std::fs::read(legacy.join("baseline")).unwrap(), b"original"); + assert_eq!(std::fs::metadata(&legacy).unwrap().mode() & 0o777, 0o700); + } + + #[test] + fn current_owner_keeps_the_legacy_checkpoint_root() { + let temp = tempfile::tempdir().unwrap(); + std::fs::set_permissions(temp.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); + let legacy = temp.path().join("effects"); + std::fs::create_dir(&legacy).unwrap(); + std::fs::write(legacy.join("baseline"), b"original").unwrap(); + assert_eq!( + prepare_private_temp_directory(temp.path(), "effects").unwrap(), + legacy + ); + assert_eq!(std::fs::read(legacy.join("baseline")).unwrap(), b"original"); + assert_eq!(std::fs::metadata(&legacy).unwrap().mode() & 0o777, 0o700); + } + + #[test] + fn selected_uid_root_rejects_symlinks_and_shared_permissions() { + let temp = tempfile::tempdir().unwrap(); + std::fs::set_permissions(temp.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); + let uid = unsafe { libc::geteuid() }; + let root = temp.path().join(format!("effects-{uid}")); + let target = tempfile::tempdir().unwrap(); + symlink(target.path(), &root).unwrap(); + assert_eq!( + prepare_private_temp_directory(temp.path(), "effects") + .unwrap_err() + .kind(), + std::io::ErrorKind::PermissionDenied + ); + std::fs::remove_file(&root).unwrap(); + std::fs::create_dir(&root).unwrap(); + std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o777)).unwrap(); + assert_eq!( + prepare_private_temp_directory(temp.path(), "effects") + .unwrap_err() + .kind(), + std::io::ErrorKind::PermissionDenied + ); + } +} diff --git a/packages/tui-kit-rs/Cargo.toml b/packages/tui-kit-rs/Cargo.toml new file mode 100644 index 000000000..80483379e --- /dev/null +++ b/packages/tui-kit-rs/Cargo.toml @@ -0,0 +1,131 @@ +[package] +name = "maestro-tui-kit" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Terminal, theme, markdown, history and usage building blocks of the Deixic Code terminal application" + +[lib] +name = "maestro_tui_kit" +path = "src/lib.rs" + +[dependencies] +maestro-local-host.workspace = true +maestro-ui.workspace = true +maestro-presentation.workspace = true +maestro-interaction.workspace = true +maestro-codex.workspace = true +maestro-sandbox.workspace = true +maestro-swarm.workspace = true +maestro-ai = { workspace = true, features = ["bedrock"] } + +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-std", "io-util", "sync", "signal", "fs", "net", "process", "time"] } +tokio-stream.workspace = true +reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls"] } +ratatui = { workspace = true, features = [ + "crossterm_0_29", + "layout-cache", + "macros", + "underline-color", + "unstable-widget-ref", +] } +crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] } +uncurses = "0.0.1" +syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "parsing", "regex-fancy"] } +pulldown-cmark = "0.13" +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp"] } +supports-color = "3" +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +thiserror.workspace = true +tracing.workspace = true +regex.workspace = true +dirs.workspace = true +dunce.workspace = true +rand.workspace = true +chrono.workspace = true +base64.workspace = true +tempfile.workspace = true +libc.workspace = true +url = "2" +arboard = { version = "3", optional = true } +notify = { version = "8", optional = true } + +[features] +default = [] +clipboard = ["arboard"] +hot-reload = ["notify"] +# Test-only helpers the terminal application's tests call. +test-support = [] + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util"] } +maestro-local-host = { workspace = true, features = ["test-support"] } +pretty_assertions = "1" +vt100 = "0.16.2" + +[lints.clippy] +# Enable pedantic lints for code quality +pedantic = { level = "warn", priority = -1 } + +# Allow common patterns that are intentional in this codebase +unused_async = "allow" # Async functions for interface consistency +missing_errors_doc = "allow" # Not all error conditions need docs +missing_panics_doc = "allow" # Not all panics need docs +cast_possible_truncation = "allow" # Often unavoidable in systems code +cast_precision_loss = "allow" # Acceptable for display/UI calculations +cast_sign_loss = "allow" # Used intentionally in many places +cast_lossless = "allow" # Allow explicit casts for clarity +must_use_candidate = "allow" # Too noisy for all builder patterns +doc_markdown = "allow" # Too picky about formatting +module_name_repetitions = "allow" # Common Rust naming pattern +similar_names = "allow" # Often intentional (e.g., tx/rx) +too_many_lines = "allow" # Subjective limit +struct_excessive_bools = "allow" # Sometimes needed for state machines +items_after_statements = "allow" # Useful for local helpers +wildcard_imports = "allow" # Used for re-exports +match_same_arms = "allow" # Sometimes clearer than combining +needless_pass_by_value = "allow" # Interface consistency +redundant_closure_for_method_calls = "allow" # Sometimes clearer +uninlined_format_args = "allow" # Older Rust style is still readable +if_not_else = "allow" # Sometimes clearer to negate +single_match_else = "allow" # Sometimes clearer than if-let +return_self_not_must_use = "allow" # Builder patterns returning Self +format_push_string = "allow" # push_str(&format!()) is readable +unnecessary_wraps = "allow" # Sometimes for API consistency +option_if_let_else = "allow" # match is often clearer +match_wildcard_for_single_variants = "allow" # Future-proofing matches +ref_option_ref = "allow" # &Option is sometimes cleaner +trivially_copy_pass_by_ref = "allow" # Sometimes clearer to pass by ref +fn_params_excessive_bools = "allow" # UI functions often have many flags +large_futures = "allow" # TUI app naturally has large futures +iter_without_into_iter = "allow" # Not always needed +unused_self = "allow" # Sometimes for method consistency +option_option = "allow" # Sometimes needed for optional Option +manual_let_else = "allow" # match is often clearer +significant_drop_tightening = "allow" # Sometimes clearer to drop earlier +map_unwrap_or = "allow" # Sometimes clearer than map+unwrap_or_else +implicit_hasher = "allow" # Not generalizing over hashers is fine +struct_field_names = "allow" # prefix/postfix patterns are OK +many_single_char_names = "allow" # Short names in math/UI code +assigning_clones = "allow" # Sometimes clearer than clone_from +case_sensitive_file_extension_comparisons = "allow" # Platform-specific code +cast_possible_wrap = "allow" # Necessary for libc interop +needless_continue = "allow" # Sometimes improves readability +if_then_some_else_none = "allow" # Sometimes clearer than bool::then +ref_option = "allow" # &Option is sometimes appropriate +default_trait_access = "allow" # Type::default() is clear enough +used_underscore_binding = "allow" # Sometimes used for side effects +match_bool = "allow" # if-else is sometimes clearer than match +missing_fields_in_debug = "allow" # Some fields can't be debugged (closures) +naive_bytecount = "allow" # bytecount crate not worth adding +doc_link_with_quotes = "allow" # Quotes in doc examples are OK +comparison_chain = "allow" # if-else chains are often clearer +large_stack_arrays = "allow" # Test fixtures may use large arrays + +[lints.rust] +# Unsafe code is allowed only for low-level system operations +# (libc process management, Darwin APIs, pointer arithmetic) +# Each unsafe block should have a SAFETY comment explaining why it's safe +unexpected_cfgs = { level = "allow", check-cfg = ["cfg(kani)"] } diff --git a/packages/tui-rs/src/ansi_commands.rs b/packages/tui-kit-rs/src/ansi_commands.rs similarity index 99% rename from packages/tui-rs/src/ansi_commands.rs rename to packages/tui-kit-rs/src/ansi_commands.rs index bf8a4b65b..62db7a84a 100644 --- a/packages/tui-rs/src/ansi_commands.rs +++ b/packages/tui-kit-rs/src/ansi_commands.rs @@ -34,7 +34,7 @@ use crossterm::Command; /// /// ```rust,ignore /// use crossterm::execute; -/// use maestro_tui::ansi_commands::SetScrollRegion; +/// use maestro_tui_kit::ansi_commands::SetScrollRegion; /// /// // Set scroll region to rows 5-20 (1-based) /// execute!(stdout(), SetScrollRegion(5..20))?; @@ -409,7 +409,7 @@ impl Command for ScrollDown { /// # Example /// /// ```rust,ignore -/// use maestro_tui::ansi_commands::{SetScrollRegion, ResetScrollRegion, scroll_region_down}; +/// use maestro_tui_kit::ansi_commands::{SetScrollRegion, ResetScrollRegion, scroll_region_down}; /// use crossterm::{execute, cursor::MoveTo}; /// /// let mut stdout = std::io::stdout(); diff --git a/packages/tui-rs/src/bridge.rs b/packages/tui-kit-rs/src/bridge.rs similarity index 100% rename from packages/tui-rs/src/bridge.rs rename to packages/tui-kit-rs/src/bridge.rs diff --git a/packages/tui-rs/src/clipboard.rs b/packages/tui-kit-rs/src/clipboard.rs similarity index 99% rename from packages/tui-rs/src/clipboard.rs rename to packages/tui-kit-rs/src/clipboard.rs index 4ba1f27d4..2cfa6d864 100644 --- a/packages/tui-rs/src/clipboard.rs +++ b/packages/tui-kit-rs/src/clipboard.rs @@ -62,7 +62,7 @@ pub enum ClipboardError { /// # Example /// /// ```no_run -/// use maestro_tui::clipboard::ClipboardManager; +/// use maestro_tui_kit::clipboard::ClipboardManager; /// /// let mut clipboard = ClipboardManager::new(); /// if clipboard.is_available() { diff --git a/packages/tui-rs/src/config_watcher.rs b/packages/tui-kit-rs/src/config_watcher.rs similarity index 99% rename from packages/tui-rs/src/config_watcher.rs rename to packages/tui-kit-rs/src/config_watcher.rs index 8d693bb56..d28f8d5a9 100644 --- a/packages/tui-rs/src/config_watcher.rs +++ b/packages/tui-kit-rs/src/config_watcher.rs @@ -13,7 +13,7 @@ //! # Example //! //! ```rust,ignore -//! use maestro_tui::config_watcher::{ConfigWatcher, ConfigWatcherBuilder}; +//! use maestro_tui_kit::config_watcher::{ConfigWatcher, ConfigWatcherBuilder}; //! //! let mut watcher = ConfigWatcherBuilder::new() //! .watch_composer_configs() diff --git a/packages/tui-rs/src/crash_handler.rs b/packages/tui-kit-rs/src/crash_handler.rs similarity index 100% rename from packages/tui-rs/src/crash_handler.rs rename to packages/tui-kit-rs/src/crash_handler.rs diff --git a/packages/tui-rs/src/dex_actions.rs b/packages/tui-kit-rs/src/dex_actions.rs similarity index 84% rename from packages/tui-rs/src/dex_actions.rs rename to packages/tui-kit-rs/src/dex_actions.rs index 0edb628cb..dab44dba0 100644 --- a/packages/tui-rs/src/dex_actions.rs +++ b/packages/tui-kit-rs/src/dex_actions.rs @@ -2,10 +2,10 @@ use maestro_interaction::{Action, ActionCatalog}; use std::sync::LazyLock; -pub(crate) use maestro_presentation::appearance::{Appearance, LOOKS}; +pub use maestro_presentation::appearance::{Appearance, LOOKS}; #[derive(Clone, Copy)] -pub(crate) enum Setting { +pub enum Setting { Personality(&'static str), Motion(bool), Tips(bool), @@ -14,7 +14,7 @@ pub(crate) enum Setting { Recap(bool), } -pub(crate) const SETTINGS: [Action; 13] = [ +pub const SETTINGS: [Action; 13] = [ Action::new("quiet", "quiet", Setting::Personality("quiet")), Action::new("standard", "standard", Setting::Personality("standard")), Action::new( @@ -51,7 +51,7 @@ pub(crate) const SETTINGS: [Action; 13] = [ ]; #[derive(Clone, Copy)] -pub(crate) enum Control { +pub enum Control { Help, Pet, Appearance, @@ -59,7 +59,7 @@ pub(crate) enum Control { Next, } -pub(crate) const CONTROLS: [Action; 5] = [ +pub const CONTROLS: [Action; 5] = [ Action::new("", "Dex help", Control::Help), Action::new("pet", "Pet Dex", Control::Pet).description("Give Dex a brief reaction"), Action::new("appearance", "Choose appearance", Control::Appearance), @@ -68,22 +68,22 @@ pub(crate) const CONTROLS: [Action; 5] = [ .description("Fill the next suggested prompt without submitting it"), ]; -pub(crate) fn controls() -> &'static ActionCatalog<'static, Control> { +pub fn controls() -> &'static ActionCatalog<'static, Control> { static CATALOG: LazyLock> = LazyLock::new(|| ActionCatalog::new(&CONTROLS).expect("Dex controls must be unique")); &CATALOG } -pub(crate) fn looks() -> &'static ActionCatalog<'static, Appearance> { +pub fn looks() -> &'static ActionCatalog<'static, Appearance> { static CATALOG: LazyLock> = LazyLock::new(|| ActionCatalog::new(&LOOKS).expect("Dex appearances must be unique")); &CATALOG } -pub(crate) fn settings() -> &'static ActionCatalog<'static, Setting> { +pub fn settings() -> &'static ActionCatalog<'static, Setting> { static CATALOG: LazyLock> = LazyLock::new(|| ActionCatalog::new(&SETTINGS).expect("Dex settings must be unique")); &CATALOG } -pub(crate) fn command_ids() -> Vec<&'static str> { +pub fn command_ids() -> Vec<&'static str> { controls() .actions() .iter() @@ -93,10 +93,10 @@ pub(crate) fn command_ids() -> Vec<&'static str> { .filter(|id| !id.is_empty()) .collect() } -pub(crate) fn contains(id: &str) -> bool { +pub fn contains(id: &str) -> bool { controls().find(id).is_some() || settings().find(id).is_some() || looks().find(id).is_some() } -pub(crate) fn help() -> String { +pub fn help() -> String { [controls().help(), settings().help(), looks().help()].join("\n") } diff --git a/packages/tui-rs/src/effects/mod.rs b/packages/tui-kit-rs/src/effects/mod.rs similarity index 95% rename from packages/tui-rs/src/effects/mod.rs rename to packages/tui-kit-rs/src/effects/mod.rs index b4be720f6..cd14b8f02 100644 --- a/packages/tui-rs/src/effects/mod.rs +++ b/packages/tui-kit-rs/src/effects/mod.rs @@ -26,7 +26,7 @@ //! ## Basic Spinner //! //! ```rust,ignore -//! use maestro_tui::effects::{spinner, braille_spinner}; +//! use maestro_tui_kit::effects::{spinner, braille_spinner}; //! //! // In your render loop, increment frame each iteration //! let frame = 42; @@ -39,7 +39,7 @@ //! ## Shimmer Effect //! //! ```rust,ignore -//! use maestro_tui::effects::shimmer_spans; +//! use maestro_tui_kit::effects::shimmer_spans; //! //! // Deixic violet sheen (rest between sweeps, soft pulse) //! let spans = shimmer_spans("Working"); diff --git a/packages/tui-rs/src/effects/shimmer.rs b/packages/tui-kit-rs/src/effects/shimmer.rs similarity index 100% rename from packages/tui-rs/src/effects/shimmer.rs rename to packages/tui-kit-rs/src/effects/shimmer.rs diff --git a/packages/tui-rs/src/effects/spinner.rs b/packages/tui-kit-rs/src/effects/spinner.rs similarity index 100% rename from packages/tui-rs/src/effects/spinner.rs rename to packages/tui-kit-rs/src/effects/spinner.rs diff --git a/packages/tui-rs/src/file_mentions.rs b/packages/tui-kit-rs/src/file_mentions.rs similarity index 100% rename from packages/tui-rs/src/file_mentions.rs rename to packages/tui-kit-rs/src/file_mentions.rs diff --git a/packages/tui-rs/src/history/mod.rs b/packages/tui-kit-rs/src/history/mod.rs similarity index 94% rename from packages/tui-rs/src/history/mod.rs rename to packages/tui-kit-rs/src/history/mod.rs index c701a33d7..6910bcb9b 100644 --- a/packages/tui-rs/src/history/mod.rs +++ b/packages/tui-kit-rs/src/history/mod.rs @@ -13,7 +13,7 @@ //! # Example //! //! ```rust,ignore -//! use maestro_tui::history::PromptHistory; +//! use maestro_tui_kit::history::PromptHistory; //! //! let mut history = PromptHistory::load_or_create()?; //! history.add("git status"); diff --git a/packages/tui-rs/src/history/store.rs b/packages/tui-kit-rs/src/history/store.rs similarity index 100% rename from packages/tui-rs/src/history/store.rs rename to packages/tui-kit-rs/src/history/store.rs diff --git a/packages/tui-rs/src/hyperlink.rs b/packages/tui-kit-rs/src/hyperlink.rs similarity index 99% rename from packages/tui-rs/src/hyperlink.rs rename to packages/tui-kit-rs/src/hyperlink.rs index 0d2678647..14b9f00a2 100644 --- a/packages/tui-rs/src/hyperlink.rs +++ b/packages/tui-kit-rs/src/hyperlink.rs @@ -40,7 +40,7 @@ const OSC8_END: &str = "\x1b]8;;\x07"; /// /// # Example /// ``` -/// use maestro_tui::hyperlink::format_link; +/// use maestro_tui_kit::hyperlink::format_link; /// /// // With custom label /// let link = format_link("https://example.com", Some("Example")); diff --git a/packages/tui-rs/src/inline_scroll.rs b/packages/tui-kit-rs/src/inline_scroll.rs similarity index 100% rename from packages/tui-rs/src/inline_scroll.rs rename to packages/tui-kit-rs/src/inline_scroll.rs diff --git a/packages/tui-rs/src/keybindings.rs b/packages/tui-kit-rs/src/keybindings.rs similarity index 99% rename from packages/tui-rs/src/keybindings.rs rename to packages/tui-kit-rs/src/keybindings.rs index 23a068b33..3a4043a53 100644 --- a/packages/tui-rs/src/keybindings.rs +++ b/packages/tui-kit-rs/src/keybindings.rs @@ -1,13 +1,13 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] use std::sync::OnceLock; use crossterm::event::KeyCode; use serde::Deserialize; use serde_json::{Value, json}; -#[cfg(test)] +#[cfg(any(test, feature = "test-support"))] use tokio::sync::Mutex; use crate::key_hints::{KeyBinding, alt, ctrl, shift}; @@ -199,8 +199,8 @@ pub fn is_keybindings_config_path(path: &Path) -> bool { || path.file_name() == canonical_expected.file_name() } -#[cfg(test)] -pub(crate) fn keybindings_test_env_lock() -> &'static Mutex<()> { +#[cfg(any(test, feature = "test-support"))] +pub fn keybindings_test_env_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())) } diff --git a/packages/tui-kit-rs/src/lib.rs b/packages/tui-kit-rs/src/lib.rs new file mode 100644 index 000000000..2550d188e --- /dev/null +++ b/packages/tui-kit-rs/src/lib.rs @@ -0,0 +1,53 @@ +//! Terminal, theme, markdown, history, usage and other self-contained pieces of the +//! terminal application, split out of `maestro-tui` so that edits to the +//! application recompile the application rather than these modules. + +pub(crate) use maestro_ai as ai; +pub(crate) use maestro_codex::codex_app_server; +pub(crate) use maestro_local_host::{ + codex_auth, color_utils, config, credential_mode, doctor, headless, init_cli, localization, + managed_setup, model_catalog, output_sanitize, path_utils, safety, service_connections, + session, telemetry, tools, +}; +pub(crate) use maestro_sandbox as sandbox; +pub(crate) use maestro_swarm as swarm; +pub(crate) use maestro_ui::{key_hints, render_utils, sync_output, wrapping}; + +pub mod ansi_commands; +pub mod bridge; +pub mod clipboard; +pub mod config_watcher; +pub mod crash_handler; +pub mod dex_actions; +pub mod effects; +pub mod file_mentions; +pub mod history; +pub mod hyperlink; +pub mod inline_scroll; +pub mod keybindings; +pub mod loop_wake; +pub mod magic_trace; +pub mod markdown; +pub mod markdown_stream; +pub mod mode_cli; +pub mod model_monitor; +pub mod notifications; +pub mod onboarding_checks; +pub mod orb_cli; +pub mod painter_cli; +pub mod palette; +pub mod palette_resource; +pub mod platform_provider_refs; +pub mod process_hardening; +pub mod prompts; +pub mod protocol; +pub mod shimmer; +pub mod subscription_usage; +pub mod syntax; +pub mod terminal; +pub mod themes; +pub mod tool_summary; +pub mod tooltips; +pub mod turn_summary; +pub mod ui_prefs; +pub mod usage; diff --git a/packages/tui-rs/src/loop_wake.rs b/packages/tui-kit-rs/src/loop_wake.rs similarity index 92% rename from packages/tui-rs/src/loop_wake.rs rename to packages/tui-kit-rs/src/loop_wake.rs index 0368c4ae6..5c98f06b1 100644 --- a/packages/tui-rs/src/loop_wake.rs +++ b/packages/tui-kit-rs/src/loop_wake.rs @@ -12,23 +12,23 @@ use tokio::sync::{Notify, mpsc, oneshot}; /// Safety drain when no terminal input and no producer signal arrives. /// Matches the hosted runner's notified maintenance interval. -pub(crate) const IDLE_SAFETY_TICK: Duration = Duration::from_secs(5); -pub(crate) const SHORT_MAINTENANCE_POLL: Duration = Duration::from_millis(100); -pub(crate) const BUSY_FRAME_POLL: Duration = Duration::from_millis(33); +pub const IDLE_SAFETY_TICK: Duration = Duration::from_secs(5); +pub const SHORT_MAINTENANCE_POLL: Duration = Duration::from_millis(100); +pub const BUSY_FRAME_POLL: Duration = Duration::from_millis(33); #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct TerminalPollInput { - pub(crate) agent_activity: bool, - pub(crate) busy: bool, - pub(crate) pending_redraw: bool, +pub struct TerminalPollInput { + pub agent_activity: bool, + pub busy: bool, + pub pending_redraw: bool, /// Animation, an outstanding theme query, a debounce, or a live source /// that cannot signal the loop itself. - pub(crate) short_cadence: bool, + pub short_cadence: bool, } impl TerminalPollInput { - #[cfg(test)] - pub(crate) fn idle() -> Self { + #[cfg(any(test, feature = "test-support"))] + pub fn idle() -> Self { Self { agent_activity: false, busy: false, @@ -38,7 +38,7 @@ impl TerminalPollInput { } } -pub(crate) fn terminal_poll_timeout(input: TerminalPollInput) -> Duration { +pub fn terminal_poll_timeout(input: TerminalPollInput) -> Duration { // Busy frames stay at 33 ms even when a redraw is already queued. // Agent output still skips the wait, matching the previous contract. if input.agent_activity { @@ -59,7 +59,7 @@ pub(crate) fn terminal_poll_timeout(input: TerminalPollInput) -> Duration { /// `notify` wakes the crossterm `select`. `terminal_waker` interrupts a /// blocking uncurses `EventSource::poll` from another thread. #[derive(Clone)] -pub(crate) struct LoopWake { +pub struct LoopWake { notify: Arc, terminal_waker: Arc>>, } @@ -71,14 +71,15 @@ impl std::fmt::Debug for LoopWake { } impl LoopWake { - pub(crate) fn new() -> Self { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { Self { notify: Arc::new(Notify::new()), terminal_waker: Arc::new(Mutex::new(None)), } } - pub(crate) fn signal(&self) { + pub fn signal(&self) { self.notify.notify_one(); if let Ok(guard) = self.terminal_waker.lock() { if let Some(waker) = guard.as_ref() { @@ -87,7 +88,7 @@ impl LoopWake { } } - pub(crate) fn install_terminal_waker(&self, waker: uncurses::event::Waker) { + pub fn install_terminal_waker(&self, waker: uncurses::event::Waker) { if let Ok(mut guard) = self.terminal_waker.lock() { *guard = Some(waker); } @@ -99,7 +100,7 @@ impl LoopWake { } #[derive(Debug, PartialEq, Eq)] -pub(crate) enum LoopWakeCause { +pub enum LoopWakeCause { Terminal(T), Producer, Tick, @@ -110,7 +111,7 @@ pub(crate) enum LoopWakeCause { /// A zero timeout still prefers terminal input that is already queued, then /// returns. That is the non-blocking poll the busy and agent-activity paths /// already used. -pub(crate) async fn await_loop_wake( +pub async fn await_loop_wake( timeout: Duration, wake: &LoopWake, tty: impl Future, @@ -135,7 +136,7 @@ pub(crate) async fn await_loop_wake( /// Forward a Tokio channel onto a receiver the loop can `try_recv`, and signal /// the loop after each item. Runtime agent sends already wake this task. -pub(crate) fn forward_unbounded( +pub fn forward_unbounded( mut rx: mpsc::UnboundedReceiver, wake: LoopWake, ) -> mpsc::UnboundedReceiver { @@ -152,7 +153,7 @@ pub(crate) fn forward_unbounded( forwarded } -pub(crate) fn forward_oneshot( +pub fn forward_oneshot( rx: oneshot::Receiver, wake: LoopWake, ) -> oneshot::Receiver { @@ -170,7 +171,7 @@ pub(crate) fn forward_oneshot( /// `tokio::time::pause`. The crossterm path uses [`await_loop_wake`] with the /// same timeout decision. #[cfg(test)] -pub(crate) async fn run_notified_terminal_pump( +pub async fn run_notified_terminal_pump( stop: tokio::sync::watch::Receiver, wake: &LoopWake, wakes: &std::sync::atomic::AtomicUsize, diff --git a/packages/tui-rs/src/magic_trace.rs b/packages/tui-kit-rs/src/magic_trace.rs similarity index 100% rename from packages/tui-rs/src/magic_trace.rs rename to packages/tui-kit-rs/src/magic_trace.rs diff --git a/packages/tui-rs/src/markdown.rs b/packages/tui-kit-rs/src/markdown.rs similarity index 99% rename from packages/tui-rs/src/markdown.rs rename to packages/tui-kit-rs/src/markdown.rs index 9abe99a15..0f60bc9f2 100644 --- a/packages/tui-rs/src/markdown.rs +++ b/packages/tui-kit-rs/src/markdown.rs @@ -34,7 +34,7 @@ //! # Example //! //! ``` -//! use maestro_tui::markdown::render_markdown; +//! use maestro_tui_kit::markdown::render_markdown; //! //! let markdown = "# Hello\n\nThis is **bold** and *italic*."; //! let text = render_markdown(markdown); @@ -120,7 +120,7 @@ impl MarkdownStyles { /// # Example /// /// ``` -/// use maestro_tui::markdown::render_markdown; +/// use maestro_tui_kit::markdown::render_markdown; /// /// let text = render_markdown("**Bold** and *italic*"); /// ``` diff --git a/packages/tui-rs/src/markdown_stream.rs b/packages/tui-kit-rs/src/markdown_stream.rs similarity index 99% rename from packages/tui-rs/src/markdown_stream.rs rename to packages/tui-kit-rs/src/markdown_stream.rs index d33c3acfa..ed7ec34b9 100644 --- a/packages/tui-rs/src/markdown_stream.rs +++ b/packages/tui-kit-rs/src/markdown_stream.rs @@ -15,7 +15,7 @@ //! # Example //! //! ```rust,ignore -//! use maestro_tui::markdown_stream::MarkdownStreamCollector; +//! use maestro_tui_kit::markdown_stream::MarkdownStreamCollector; //! //! let mut collector = MarkdownStreamCollector::new(Some(80)); //! diff --git a/packages/tui-rs/src/mode_cli.rs b/packages/tui-kit-rs/src/mode_cli.rs similarity index 100% rename from packages/tui-rs/src/mode_cli.rs rename to packages/tui-kit-rs/src/mode_cli.rs diff --git a/packages/tui-rs/src/model_monitor.rs b/packages/tui-kit-rs/src/model_monitor.rs similarity index 100% rename from packages/tui-rs/src/model_monitor.rs rename to packages/tui-kit-rs/src/model_monitor.rs diff --git a/packages/tui-rs/src/notifications.rs b/packages/tui-kit-rs/src/notifications.rs similarity index 99% rename from packages/tui-rs/src/notifications.rs rename to packages/tui-kit-rs/src/notifications.rs index 6e7c63b13..92ab3ddc3 100644 --- a/packages/tui-rs/src/notifications.rs +++ b/packages/tui-kit-rs/src/notifications.rs @@ -249,7 +249,7 @@ pub fn osc_progress_clear() -> String { /// [`crate::ansi_commands::PostNotification`] reuse it for the same reason: /// any text interpolated into an OSC payload needs this, not just the /// title-setting sequence it was first written for. -pub(crate) fn sanitize_osc_text(text: &str) -> String { +pub fn sanitize_osc_text(text: &str) -> String { text.chars().filter(|c| !c.is_control()).collect() } diff --git a/packages/tui-rs/src/onboarding_checks.rs b/packages/tui-kit-rs/src/onboarding_checks.rs similarity index 100% rename from packages/tui-rs/src/onboarding_checks.rs rename to packages/tui-kit-rs/src/onboarding_checks.rs diff --git a/packages/tui-rs/src/orb_cli.rs b/packages/tui-kit-rs/src/orb_cli.rs similarity index 100% rename from packages/tui-rs/src/orb_cli.rs rename to packages/tui-kit-rs/src/orb_cli.rs diff --git a/packages/tui-rs/src/painter_cli.rs b/packages/tui-kit-rs/src/painter_cli.rs similarity index 100% rename from packages/tui-rs/src/painter_cli.rs rename to packages/tui-kit-rs/src/painter_cli.rs diff --git a/packages/tui-rs/src/palette.rs b/packages/tui-kit-rs/src/palette.rs similarity index 100% rename from packages/tui-rs/src/palette.rs rename to packages/tui-kit-rs/src/palette.rs diff --git a/packages/tui-rs/src/palette_resource.rs b/packages/tui-kit-rs/src/palette_resource.rs similarity index 100% rename from packages/tui-rs/src/palette_resource.rs rename to packages/tui-kit-rs/src/palette_resource.rs diff --git a/packages/tui-rs/src/platform_provider_refs.rs b/packages/tui-kit-rs/src/platform_provider_refs.rs similarity index 99% rename from packages/tui-rs/src/platform_provider_refs.rs rename to packages/tui-kit-rs/src/platform_provider_refs.rs index 9e2f12404..16a53e636 100644 --- a/packages/tui-rs/src/platform_provider_refs.rs +++ b/packages/tui-kit-rs/src/platform_provider_refs.rs @@ -263,7 +263,7 @@ pub fn select_default(id: &str) -> Result { Ok(selected) } -pub(crate) fn keys_request( +pub fn keys_request( session: &PlatformSession, method: reqwest::Method, path: &str, diff --git a/packages/tui-rs/src/process_hardening.rs b/packages/tui-kit-rs/src/process_hardening.rs similarity index 100% rename from packages/tui-rs/src/process_hardening.rs rename to packages/tui-kit-rs/src/process_hardening.rs diff --git a/packages/tui-rs/src/prompts.rs b/packages/tui-kit-rs/src/prompts.rs similarity index 100% rename from packages/tui-rs/src/prompts.rs rename to packages/tui-kit-rs/src/prompts.rs diff --git a/packages/tui-rs/src/protocol/mod.rs b/packages/tui-kit-rs/src/protocol/mod.rs similarity index 100% rename from packages/tui-rs/src/protocol/mod.rs rename to packages/tui-kit-rs/src/protocol/mod.rs diff --git a/packages/tui-rs/src/protocol/styles.rs b/packages/tui-kit-rs/src/protocol/styles.rs similarity index 100% rename from packages/tui-rs/src/protocol/styles.rs rename to packages/tui-kit-rs/src/protocol/styles.rs diff --git a/packages/tui-rs/src/shimmer.rs b/packages/tui-kit-rs/src/shimmer.rs similarity index 100% rename from packages/tui-rs/src/shimmer.rs rename to packages/tui-kit-rs/src/shimmer.rs diff --git a/packages/tui-rs/src/subscription_usage.rs b/packages/tui-kit-rs/src/subscription_usage.rs similarity index 99% rename from packages/tui-rs/src/subscription_usage.rs rename to packages/tui-kit-rs/src/subscription_usage.rs index d409b6731..890bea972 100644 --- a/packages/tui-rs/src/subscription_usage.rs +++ b/packages/tui-kit-rs/src/subscription_usage.rs @@ -46,7 +46,7 @@ enum LinkState { } /// Render immediately from recorded activity, with one bounded official Codex read. -pub(crate) async fn report(cwd: &str) -> String { +pub async fn report(cwd: &str) -> String { let today = Local::now().date_naive(); let root = crate::session::sessions_dir(cwd) .parent() diff --git a/packages/tui-rs/src/syntax.rs b/packages/tui-kit-rs/src/syntax.rs similarity index 99% rename from packages/tui-rs/src/syntax.rs rename to packages/tui-kit-rs/src/syntax.rs index a22f263a8..8782e7d40 100644 --- a/packages/tui-rs/src/syntax.rs +++ b/packages/tui-kit-rs/src/syntax.rs @@ -32,7 +32,7 @@ //! # Example //! //! ``` -//! use maestro_tui::syntax::highlight_code; +//! use maestro_tui_kit::syntax::highlight_code; //! //! let code = "fn main() {\n println!(\"Hello\");\n}"; //! let lines = highlight_code(code, Some("rust")); @@ -182,7 +182,7 @@ fn syntax_theme_name(theme: &crate::themes::Theme) -> &'static str { /// # Example /// /// ``` -/// use maestro_tui::syntax::has_syntax; +/// use maestro_tui_kit::syntax::has_syntax; /// /// assert!(has_syntax("rust")); /// assert!(has_syntax("python")); diff --git a/packages/tui-rs/src/terminal/events.rs b/packages/tui-kit-rs/src/terminal/events.rs similarity index 98% rename from packages/tui-rs/src/terminal/events.rs rename to packages/tui-kit-rs/src/terminal/events.rs index 57517e183..c0d128df7 100644 --- a/packages/tui-rs/src/terminal/events.rs +++ b/packages/tui-kit-rs/src/terminal/events.rs @@ -104,13 +104,13 @@ pub enum TerminalEvent { /// # Example /// /// ```no_run -/// use maestro_tui::terminal::TerminalEventStream; +/// use maestro_tui_kit::terminal::TerminalEventStream; /// /// # async fn example() { /// let mut events = TerminalEventStream::new(); /// while let Some(event) = events.next().await { /// match event { -/// maestro_tui::terminal::TerminalEvent::Key { key, .. } => { +/// maestro_tui_kit::terminal::TerminalEvent::Key { key, .. } => { /// println!("Key pressed: {}", key); /// } /// _ => {} @@ -160,7 +160,7 @@ impl Default for TerminalEventStream { /// events. That lets the app query color-scheme capabilities without leaking /// reply bytes into the composer. #[derive(Debug)] -pub(crate) enum AppTerminalEvent { +pub enum AppTerminalEvent { Key(KeyEvent), Mouse(MouseEvent), Paste(String), @@ -175,7 +175,7 @@ pub(crate) enum AppTerminalEvent { impl AppTerminalEvent { /// Preserve the legacy crossterm path as a fallback when the controlling /// terminal cannot be opened by uncurses. - pub(crate) fn from_crossterm(event: Event) -> Option { + pub fn from_crossterm(event: Event) -> Option { match event { Event::Key(key) => Some(Self::Key(key)), Event::Mouse(mouse) => Some(Self::Mouse(mouse)), @@ -188,7 +188,7 @@ impl AppTerminalEvent { } /// Synchronous uncurses event reader for the main render loop. -pub(crate) struct TerminalEventReader { +pub struct TerminalEventReader { source: EventSource, state: EventConversionState, } @@ -201,7 +201,7 @@ struct EventConversionState { impl TerminalEventReader { /// Open the controlling terminal and create a protocol-aware event source. - pub(crate) fn open() -> io::Result { + pub fn open() -> io::Result { let terminal = uncurses::terminal::Terminal::open()?; Ok(Self { source: EventSource::new(terminal.input())?, @@ -210,7 +210,7 @@ impl TerminalEventReader { } /// Cloneable handle that interrupts a blocking [`Self::poll`]. - pub(crate) fn waker(&self) -> uncurses::event::Waker { + pub fn waker(&self) -> uncurses::event::Waker { self.source.waker() } @@ -218,7 +218,7 @@ impl TerminalEventReader { /// /// Query replies unsupported by the application are deliberately consumed /// here rather than reinterpreted as user input. - pub(crate) fn poll(&mut self, timeout: Duration) -> io::Result> { + pub fn poll(&mut self, timeout: Duration) -> io::Result> { if self.state.pending.is_empty() && !self.source.poll(Some(timeout))? { return Ok(None); } diff --git a/packages/tui-rs/src/terminal/history.rs b/packages/tui-kit-rs/src/terminal/history.rs similarity index 99% rename from packages/tui-rs/src/terminal/history.rs rename to packages/tui-kit-rs/src/terminal/history.rs index 9d3f1da39..8f8843f7a 100644 --- a/packages/tui-rs/src/terminal/history.rs +++ b/packages/tui-kit-rs/src/terminal/history.rs @@ -76,8 +76,8 @@ use crate::protocol::HistoryLine; /// /// ```no_run /// use std::fs::File; -/// use maestro_tui::terminal::push_history_lines; -/// use maestro_tui::protocol::HistoryLine; +/// use maestro_tui_kit::terminal::push_history_lines; +/// use maestro_tui_kit::protocol::HistoryLine; /// /// # fn example(mut tty: File) -> std::io::Result<()> { /// let lines = vec![/* history lines */]; diff --git a/packages/tui-rs/src/terminal/mod.rs b/packages/tui-kit-rs/src/terminal/mod.rs similarity index 92% rename from packages/tui-rs/src/terminal/mod.rs rename to packages/tui-kit-rs/src/terminal/mod.rs index 648d31491..4bed26022 100644 --- a/packages/tui-rs/src/terminal/mod.rs +++ b/packages/tui-kit-rs/src/terminal/mod.rs @@ -30,7 +30,7 @@ //! # Example Usage //! //! ```no_run -//! use maestro_tui::terminal::{init, restore, TerminalEventStream}; +//! use maestro_tui_kit::terminal::{init, restore, TerminalEventStream}; //! //! # async fn example() -> std::io::Result<()> { //! // Initialize the terminal @@ -52,13 +52,13 @@ mod events; mod history; mod setup; -pub(crate) use events::{AppTerminalEvent, TerminalEventReader}; +pub use events::{AppTerminalEvent, TerminalEventReader}; pub use events::{TerminalEvent, TerminalEventStream}; pub use history::push_history_lines; pub use setup::{ Terminal, TerminalCapabilities, calculate_viewport, check_tty, init, init_fallback, is_tty_available, recreate_with_viewport, restore, size, write_raw, }; -pub(crate) use setup::{ +pub use setup::{ disable_theme_reporting, enable_theme_reporting, initialize_theme_reporting, query_theme, }; diff --git a/packages/tui-rs/src/terminal/setup.rs b/packages/tui-kit-rs/src/terminal/setup.rs similarity index 98% rename from packages/tui-rs/src/terminal/setup.rs rename to packages/tui-kit-rs/src/terminal/setup.rs index 9a19e33cb..65100cd9d 100644 --- a/packages/tui-rs/src/terminal/setup.rs +++ b/packages/tui-kit-rs/src/terminal/setup.rs @@ -167,7 +167,7 @@ pub fn is_tty_available() -> bool { /// # Example /// /// ```no_run -/// # use maestro_tui::terminal::check_tty; +/// # use maestro_tui_kit::terminal::check_tty; /// if let Err(e) = check_tty() { /// eprintln!("Cannot access terminal: {}", e); /// std::process::exit(1); @@ -364,24 +364,24 @@ pub fn write_raw(data: &str) -> io::Result<()> { } /// Discover the existing light/dark reporting mode and current terminal theme. -pub(crate) fn initialize_theme_reporting() -> io::Result<()> { +pub fn initialize_theme_reporting() -> io::Result<()> { write_raw_bytes(&theme_reporting_initialization_sequence()?) } /// Enable live light/dark reports after the terminal reports the mode as reset. -pub(crate) fn enable_theme_reporting() -> io::Result<()> { +pub fn enable_theme_reporting() -> io::Result<()> { write_raw_bytes(&theme_reporting_enable_sequence()?)?; THEME_REPORTING_ENABLED_BY_MAESTRO.store(true, Ordering::Release); Ok(()) } /// Request the terminal's current light/dark preference and background color. -pub(crate) fn query_theme() -> io::Result<()> { +pub fn query_theme() -> io::Result<()> { write_raw_bytes(&theme_query_sequence()) } /// Stop live light/dark reports. -pub(crate) fn disable_theme_reporting() -> io::Result<()> { +pub fn disable_theme_reporting() -> io::Result<()> { if !THEME_REPORTING_ENABLED_BY_MAESTRO.swap(false, Ordering::AcqRel) { return Ok(()); } @@ -480,7 +480,7 @@ fn restore_impl() -> io::Result<()> { /// # Example /// /// ```no_run -/// # use maestro_tui::terminal::size; +/// # use maestro_tui_kit::terminal::size; /// let (width, height) = size()?; /// println!("Terminal is {}x{} characters", width, height); /// # Ok::<(), std::io::Error>(()) diff --git a/packages/tui-rs/src/themes/mod.rs b/packages/tui-kit-rs/src/themes/mod.rs similarity index 99% rename from packages/tui-rs/src/themes/mod.rs rename to packages/tui-kit-rs/src/themes/mod.rs index debe6dc38..f31b70d36 100644 --- a/packages/tui-rs/src/themes/mod.rs +++ b/packages/tui-kit-rs/src/themes/mod.rs @@ -59,7 +59,7 @@ //! # Usage Example //! //! ```rust,ignore -//! use maestro_tui::themes::{set_theme_by_name, current_theme, available_themes}; +//! use maestro_tui_kit::themes::{set_theme_by_name, current_theme, available_themes}; //! //! // List available themes //! for name in available_themes() { diff --git a/packages/tui-rs/src/themes/osc11.rs b/packages/tui-kit-rs/src/themes/osc11.rs similarity index 100% rename from packages/tui-rs/src/themes/osc11.rs rename to packages/tui-kit-rs/src/themes/osc11.rs diff --git a/packages/tui-rs/src/themes/vscode/LICENSE.txt b/packages/tui-kit-rs/src/themes/vscode/LICENSE.txt similarity index 100% rename from packages/tui-rs/src/themes/vscode/LICENSE.txt rename to packages/tui-kit-rs/src/themes/vscode/LICENSE.txt diff --git a/packages/tui-rs/src/themes/vscode/README.md b/packages/tui-kit-rs/src/themes/vscode/README.md similarity index 100% rename from packages/tui-rs/src/themes/vscode/README.md rename to packages/tui-kit-rs/src/themes/vscode/README.md diff --git a/packages/tui-rs/src/themes/vscode/source.json b/packages/tui-kit-rs/src/themes/vscode/source.json similarity index 100% rename from packages/tui-rs/src/themes/vscode/source.json rename to packages/tui-kit-rs/src/themes/vscode/source.json diff --git a/packages/tui-rs/src/themes/vscode/themes.json b/packages/tui-kit-rs/src/themes/vscode/themes.json similarity index 100% rename from packages/tui-rs/src/themes/vscode/themes.json rename to packages/tui-kit-rs/src/themes/vscode/themes.json diff --git a/packages/tui-rs/src/tool_summary.rs b/packages/tui-kit-rs/src/tool_summary.rs similarity index 100% rename from packages/tui-rs/src/tool_summary.rs rename to packages/tui-kit-rs/src/tool_summary.rs diff --git a/packages/tui-rs/src/tooltips.rs b/packages/tui-kit-rs/src/tooltips.rs similarity index 100% rename from packages/tui-rs/src/tooltips.rs rename to packages/tui-kit-rs/src/tooltips.rs diff --git a/packages/tui-rs/src/turn_summary.rs b/packages/tui-kit-rs/src/turn_summary.rs similarity index 94% rename from packages/tui-rs/src/turn_summary.rs rename to packages/tui-kit-rs/src/turn_summary.rs index ce7b303fa..7b1eadf9a 100644 --- a/packages/tui-rs/src/turn_summary.rs +++ b/packages/tui-kit-rs/src/turn_summary.rs @@ -17,7 +17,7 @@ pub struct TurnSummary { impl TurnSummary { /// Add one provider response's usage to the aggregate turn summary. - pub(crate) fn add_usage(&mut self, usage: &crate::headless::TokenUsage) { + pub fn add_usage(&mut self, usage: &crate::headless::TokenUsage) { self.input_tokens = self.input_tokens.saturating_add(usage.input_tokens); self.output_tokens = self.output_tokens.saturating_add(usage.output_tokens); self.cache_read_tokens = self @@ -29,7 +29,7 @@ impl TurnSummary { } /// Set the tool counts collected from the completed transcript slice. - pub(crate) fn set_tool_counts( + pub fn set_tool_counts( &mut self, tool_calls: usize, successful_tools: usize, @@ -41,13 +41,13 @@ impl TurnSummary { } /// Set the wall-clock duration of the completed turn. - pub(crate) fn set_duration(&mut self, duration: Duration) { + pub fn set_duration(&mut self, duration: Duration) { self.duration = duration; } /// Render the concise status-bar recap shown after the app becomes idle. #[must_use] - pub(crate) fn status_line(&self) -> String { + pub fn status_line(&self) -> String { let tools = match self.tool_calls { 0 => "no tools".to_string(), 1 => format!( diff --git a/packages/tui-rs/src/ui_prefs.rs b/packages/tui-kit-rs/src/ui_prefs.rs similarity index 100% rename from packages/tui-rs/src/ui_prefs.rs rename to packages/tui-kit-rs/src/ui_prefs.rs diff --git a/packages/tui-rs/src/usage/mod.rs b/packages/tui-kit-rs/src/usage/mod.rs similarity index 93% rename from packages/tui-rs/src/usage/mod.rs rename to packages/tui-kit-rs/src/usage/mod.rs index 8bb9c27e4..5373fea56 100644 --- a/packages/tui-rs/src/usage/mod.rs +++ b/packages/tui-kit-rs/src/usage/mod.rs @@ -14,7 +14,7 @@ //! # Example //! //! ```rust,ignore -//! use maestro_tui::usage::{UsageTracker, ModelPricing}; +//! use maestro_tui_kit::usage::{UsageTracker, ModelPricing}; //! //! let mut tracker = UsageTracker::new(); //! tracker.add_turn(usage); diff --git a/packages/tui-rs/src/usage/pricing.rs b/packages/tui-kit-rs/src/usage/pricing.rs similarity index 100% rename from packages/tui-rs/src/usage/pricing.rs rename to packages/tui-kit-rs/src/usage/pricing.rs diff --git a/packages/tui-rs/src/usage/tracker.rs b/packages/tui-kit-rs/src/usage/tracker.rs similarity index 100% rename from packages/tui-rs/src/usage/tracker.rs rename to packages/tui-kit-rs/src/usage/tracker.rs diff --git a/packages/tui-rs/tooltips.txt b/packages/tui-kit-rs/tooltips.txt similarity index 100% rename from packages/tui-rs/tooltips.txt rename to packages/tui-kit-rs/tooltips.txt diff --git a/packages/tui-rs/Cargo.toml b/packages/tui-rs/Cargo.toml index c3a8c688a..9156f89cd 100644 --- a/packages/tui-rs/Cargo.toml +++ b/packages/tui-rs/Cargo.toml @@ -27,6 +27,10 @@ path = "src/lib.rs" [dependencies] maestro-local-host.workspace = true +maestro-tui-kit.workspace = true +maestro-cli-commands.workspace = true +maestro-hosted-runner-lib.workspace = true +maestro-a2a-cli.workspace = true maestro-a2a-ledger.workspace = true # Terminal handling crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] } @@ -66,7 +70,7 @@ maestro-session-history.workspace = true maestro-session.workspace = true # HTTP client for AI APIs -reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls-tls"] } +reqwest = { workspace = true, features = ["blocking", "json", "stream", "rustls"] } rcgen.workspace = true rustls.workspace = true semver = "1" @@ -177,19 +181,20 @@ windows-sys = { workspace = true, features = [ default = [] # Unverified NativeAgent constructor for in-crate tests plus the local # perf/bench binaries. Shipped maestro / deixic-code builds must not enable it. -test-support = ["maestro-local-host/test-support", "maestro-runtime/test-support"] +test-support = ["maestro-local-host/test-support", "maestro-runtime/test-support", "maestro-tui-kit/test-support"] thin-scenario = ["maestro-local-host/thin-scenario"] -clipboard = ["arboard"] +clipboard = ["arboard", "maestro-tui-kit/clipboard"] # Hook backends lua = ["mlua", "maestro-local-host/lua"] wasm = ["wasmtime", "maestro-local-host/wasm"] -hot-reload = ["notify", "maestro-local-host/hot-reload"] +hot-reload = ["notify", "maestro-local-host/hot-reload", "maestro-tui-kit/hot-reload"] # Enable all hook features hooks-full = ["lua", "wasm", "hot-reload"] [dev-dependencies] maestro-ui-preview = { path = "../ui-preview-rs" } maestro-local-host = { workspace = true, features = ["test-support"] } +maestro-tui-kit = { workspace = true, features = ["test-support"] } maestro-runtime = { workspace = true, features = ["test-support"] } maestro-session = { workspace = true, features = ["test-support"] } # Reconstruct real terminal cells in PTY assertions, including cursor-only spaces. diff --git a/packages/tui-rs/src/lib.rs b/packages/tui-rs/src/lib.rs index 2921a31db..61b7685c3 100644 --- a/packages/tui-rs/src/lib.rs +++ b/packages/tui-rs/src/lib.rs @@ -14,13 +14,13 @@ /// Runs the native agent and exchanges typed commands and events. pub mod agent; -pub mod acp_cli; /// AI provider clients (Anthropic, OpenAI, etc.). /// Provides unified interfaces for different AI APIs with streaming support. /// Lives in the `maestro-ai` crate (packages/ai-rs); re-exported here under /// its historical module path so existing `crate::ai::...` call sites are /// unaffected. pub use maestro_ai as ai; +pub use maestro_cli_commands::acp_cli; pub use maestro_local_host::agents_cli; pub use maestro_local_host::model_dynamics; /// File-level checkpoints for `/rewind files` (restore files an agent turn modified). @@ -31,14 +31,14 @@ pub use maestro_local_host::doctor; pub use maestro_local_host::local_models; pub use maestro_local_host::managed_setup; pub use maestro_local_host::model_catalog; +pub use maestro_local_host::orb_connection; /// Shared atomic-write helper (temp file + fsync + rename + parent-dir fsync) /// for persisted JSON/text state. See module docs for crash-safety vs. /// power-loss-safety guarantees. pub use maestro_session::fs_atomic; -pub mod model_monitor; -pub use maestro_local_host::orb_connection; -pub mod palette_resource; -pub mod platform_provider_refs; +pub use maestro_tui_kit::model_monitor; +pub use maestro_tui_kit::palette_resource; +pub use maestro_tui_kit::platform_provider_refs; /// Slash command system. /// Parses and executes commands like /help, /clear, /model, etc. @@ -54,11 +54,11 @@ pub use maestro_local_host::config; /// Async-signal-safe crash handler for SIGSEGV/SIGBUS. /// Complements the panic hook: records hard crashes and restores the terminal. -pub mod crash_handler; +pub use maestro_tui_kit::crash_handler; /// Visual effects (spinners, shimmers, animations). /// Terminal-based animations for loading states and visual feedback. -pub mod effects; +pub use maestro_tui_kit::effects; /// Native CLI entrypoint shared by the `maestro-tui` and canonical `maestro` binaries. pub mod entrypoint; @@ -70,7 +70,7 @@ pub(crate) use maestro_local_host::path_utils; pub use maestro_workspace::files; /// Inline `@file` mention expansion for the composer. -pub mod file_mentions; +pub use maestro_tui_kit::file_mentions; /// Headless mode communication protocol. /// JSON-based IPC protocol for communicating with the native agent. @@ -78,22 +78,22 @@ pub use maestro_local_host::headless; pub use maestro_local_host::headless_server; /// Hosted runner contract primitives for Platform-managed Maestro runtimes. -pub use maestro_local_host::hosted_runner; -pub use maestro_local_host::hosted_runner_conformance; +pub use maestro_hosted_runner_lib::hosted_runner; +pub use maestro_hosted_runner_lib::hosted_runner_conformance; /// Rust hosted runner command-line entrypoints. -pub use maestro_local_host::hosted_runner_cli; +pub use maestro_hosted_runner_lib::hosted_runner_cli; /// Message protocol definitions. /// Type definitions shared by native terminal consumers. -pub mod protocol; +pub use maestro_tui_kit::protocol; /// Maestro <-> Conductor bridge status types and helpers. -pub mod bridge; +pub use maestro_tui_kit::bridge; -/// Live, bounded checks used by guided setup. -pub mod onboarding_checks; pub use maestro_local_host::service_connections; +/// Live, bounded checks used by guided setup. +pub use maestro_tui_kit::onboarding_checks; /// Session persistence (save/load conversations). /// JSONL-based session storage for resuming previous conversations. pub mod session { @@ -104,9 +104,9 @@ pub mod session { } pub mod session_transfer; pub mod setup_cli; -mod subscription_usage; pub use maestro_local_host::skill_cli; pub use maestro_local_host::skill_package_cli; +pub use maestro_tui_kit::subscription_usage; /// Application state management. /// Central state struct that holds all mutable application data. @@ -114,7 +114,7 @@ pub mod state; /// Terminal setup and event handling. /// Raw terminal mode, event polling, and cleanup on exit. -pub mod terminal; +pub use maestro_tui_kit::terminal; /// Tool execution (bash, file operations). /// Executes tools requested by the AI agent and returns results. @@ -129,7 +129,7 @@ pub use maestro_local_host::tools; /// Clipboard integration (copy/paste). /// Platform-specific clipboard access for copying code blocks. -pub mod clipboard; +pub use maestro_tui_kit::clipboard; /// Interactive goal mode (create / pause / block / complete + auto-continue). pub use maestro_local_host::goal; @@ -143,10 +143,10 @@ pub use maestro_local_host::rlm; /// Durable local messages for parent and delegated agent sessions. pub use maestro_local_host::mailbox; -mod dex_actions; +pub use maestro_tui_kit::dex_actions; pub mod dex_delight; /// Lightweight UI prefs (`~/.maestro/ui.json`), e.g. footer density. -pub mod ui_prefs; +pub use maestro_tui_kit::ui_prefs; /// Diff generation and rendering. /// Shows file changes with colored additions/deletions. @@ -173,15 +173,15 @@ pub use maestro_workspace::git; pub use maestro_ui::key_hints; /// User-configurable Rust TUI keybinding loading and resolution. -pub mod keybindings; +pub use maestro_tui_kit::keybindings; /// Markdown rendering for terminal. /// Converts markdown to styled terminal output with syntax highlighting. -pub mod markdown; +pub use maestro_tui_kit::markdown; /// Desktop notifications. /// Sends system notifications when tasks complete (optional). -pub mod notifications; +pub use maestro_tui_kit::notifications; /// Scrollable text pager. /// Like `less` - allows scrolling through long content. @@ -189,57 +189,57 @@ pub use maestro_ui::pager; /// Terminal color palette management. /// Handles different color capability levels (16, 256, true color). -pub mod palette; +pub use maestro_tui_kit::palette; -pub mod a2a_cli; +pub use maestro_a2a_cli::a2a_cli; pub mod cli_commands; -pub mod cloud_cli; -pub mod deixic_operating_client; +pub use maestro_cli_commands::cloud_cli; +pub use maestro_cli_commands::deixic_operating_client; /// The one-loop kernel consumer for local (non-cloud) turns, behind /// `MAESTRO_DEX_LOOP=1`. See `docs/design/maestro-on-dex-loop.md`. pub mod dex_loop_local; pub mod mission_run; pub mod workflow_cli; +pub use maestro_cli_commands::config_cli; +pub use maestro_cli_commands::context_cli; +pub use maestro_cli_commands::evidence; pub use maestro_codex::codex_app_server; pub use maestro_codex::codex_session; pub use maestro_local_host::codex_auth; pub use maestro_local_host::codex_cli; pub use maestro_local_host::codex_identity; -pub mod config_cli; -pub mod context_cli; pub use maestro_local_host::evalops_cli; -mod evidence; /// Droid-style executable slash commands from `.composer/commands/`. pub mod exec_commands; -pub mod import_claude_cli; +pub use maestro_a2a_cli::operating_plane_cli; +pub use maestro_a2a_cli::operating_plane_client; +pub use maestro_a2a_cli::operating_plane_summary; +pub use maestro_cli_commands::import_claude_cli; +pub use maestro_cli_commands::memory_cli; +pub use maestro_cli_commands::plugins_cli; pub use maestro_local_host::init_cli; -pub mod memory_cli; pub use maestro_local_host::mission_cli; pub use maestro_local_host::mission_readiness; -pub mod mode_cli; pub use maestro_local_host::openai_cli; -pub mod operating_plane_cli; -pub mod operating_plane_client; -pub mod operating_plane_summary; -pub mod orb_cli; -pub mod painter_cli; -pub mod plugins_cli; +pub use maestro_tui_kit::mode_cli; +pub use maestro_tui_kit::orb_cli; +pub use maestro_tui_kit::painter_cli; pub mod print_mode; +pub use maestro_cli_commands::remote_attach; +pub use maestro_cli_commands::remote_cli; /// Pre-main process hardening (core dumps, ptrace, loader-injection env vars). -pub mod process_hardening; +pub use maestro_tui_kit::process_hardening; /// Custom prompt templates. /// User-defined prompts with argument substitution. -pub mod prompts; -pub mod remote_attach; -pub mod remote_cli; +pub use maestro_tui_kit::prompts; /// Second-opinion review of uncommitted changes by a different model (`/rubber-duck`). pub mod rubber_duck; pub mod run_cli; pub mod scenario_cli; -pub mod search_cli; -pub mod thread_cli; -pub mod update_cli; -pub mod value_cli; +pub use maestro_a2a_cli::value_cli; +pub use maestro_cli_commands::search_cli; +pub use maestro_cli_commands::thread_cli; +pub use maestro_cli_commands::update_cli; pub use maestro_local_host::video; /// Droid-style session worktrees (`-w` / `--worktree`). @@ -247,7 +247,7 @@ pub use maestro_workspace::worktree; /// Jane Street magic-trace stop indicator + slow-frame hooks. /// Linux/Intel PT only; see module docs and `scripts/magic-trace-tui.sh`. -pub mod magic_trace; +pub use maestro_tui_kit::magic_trace; /// Command sandboxing (macOS Seatbelt, Linux Landlock). /// Restricts file system access for executed commands. @@ -264,24 +264,24 @@ pub use maestro_local_host::safety; /// Syntax highlighting for code blocks. /// Uses syntect for highlighting in various languages. -pub mod syntax; +pub use maestro_tui_kit::syntax; /// Color themes. /// UI color schemes (dark, light, custom). -pub mod themes; +pub use maestro_tui_kit::themes; /// Loading tooltips. /// Random tips shown while waiting for AI responses. -pub mod tooltips; +pub use maestro_tui_kit::tooltips; /// Text wrapping utilities. /// Word-wraps text for terminal display, respecting ANSI codes. pub use maestro_ui::wrapping; +pub use maestro_cli_commands::mcp_config_cli; /// Model Context Protocol (MCP) client. /// Connects to external MCP servers for additional tools and capabilities. pub use maestro_local_host::mcp; -pub mod mcp_config_cli; /// LSP diagnostics bridge (optional Node-based CLI integration). pub use maestro_local_host::lsp; @@ -295,11 +295,11 @@ pub use maestro_local_host::workflow_runtime; /// Usage and cost tracking. /// Tracks token consumption and estimates costs across sessions. -pub mod usage; +pub use maestro_tui_kit::usage; /// Prompt history with persistence and search. /// Stores and retrieves previous prompts for easy recall. -pub mod history; +pub use maestro_tui_kit::history; pub mod runtime_badges; /// Persistent UI state (queue modes, preferences). @@ -307,14 +307,14 @@ pub mod ui_state; /// Configuration file watcher for hot-reload. /// Watches config files and emits events on changes. -pub mod config_watcher; +pub use maestro_tui_kit::config_watcher; pub use maestro_local_host::tool_output; +pub use maestro_tui_kit::tool_summary; +pub use maestro_tui_kit::turn_summary; /// Text formatting utilities (truncation, JSON compacting). /// Ported from OpenAI Codex CLI (MIT licensed). pub use maestro_ui::text_format; -pub mod tool_summary; -pub mod turn_summary; /// Live/incremental text wrapping for streaming content. /// Allows text to be pushed in fragments and wrapped correctly. @@ -330,7 +330,7 @@ pub use maestro_local_host::color_utils; /// Streaming markdown collector for incremental rendering. /// Buffers text and commits only complete lines. -pub mod markdown_stream; +pub use maestro_tui_kit::markdown_stream; /// Terminal information and detection utilities. /// SSH, WSL, and terminal emulator detection. @@ -350,7 +350,7 @@ pub use maestro_ui::key_binding; /// ANSI terminal commands for scroll regions and terminal control. /// Essential for proper scrolling over SSH. -pub mod ansi_commands; +pub use maestro_tui_kit::ansi_commands; /// Control-character sanitization for content that reaches a real terminal /// outside ratatui's `Buffer` (which filters it for the TUI chat pane). @@ -367,7 +367,7 @@ pub use maestro_ui::viewport; /// Inline scrolling with scroll regions. /// For inline TUI mode with terminal scrollback integration. -pub mod inline_scroll; +pub use maestro_tui_kit::inline_scroll; /// Selection list rendering for popups and menus. /// Fuzzy match highlighting, aligned descriptions, smart wrapping. @@ -391,7 +391,7 @@ pub use maestro_ui::field_format; /// Shimmer animation effect. /// Animated text highlights for loading indicators. -pub mod shimmer; +pub use maestro_tui_kit::shimmer; /// ANSI code tracker with surgical resets. /// Stateful tracking of ANSI SGR codes for preventing visual artifacts. @@ -443,7 +443,7 @@ pub use maestro_ui::confirm_dialog; /// OSC-8 terminal hyperlinks. /// Clickable links in modern terminals. -pub mod hyperlink; +pub use maestro_tui_kit::hyperlink; /// ASCII animation system. /// Frame-based animations with built-in presets. @@ -472,7 +472,7 @@ pub use maestro_swarm as swarm; /// Main application struct and event loop. /// This is the top-level coordinator that ties everything together. mod app; -mod loop_wake; +pub use maestro_tui_kit::loop_wake; // ───────────────────────────────────────────────────────────────────────────── // RE-EXPORTS @@ -1422,6 +1422,6 @@ pub use maestro_local_host::code_authority; /// Session-backed product issue drafts and the product feedback client. pub(crate) use maestro_local_host::bug_report; +pub use maestro_cli_commands::mcp_catalog; pub use maestro_local_host::localization; -pub mod mcp_catalog; pub mod transcript_copy; diff --git a/proto/maestro/v1/protocol-compatibility-manifest.json b/proto/maestro/v1/protocol-compatibility-manifest.json index ae3899ccd..e659b46e0 100644 --- a/proto/maestro/v1/protocol-compatibility-manifest.json +++ b/proto/maestro/v1/protocol-compatibility-manifest.json @@ -5,7 +5,7 @@ "canonicalization": "rfc8785", "encoding": "utf-8" }, - "compatibilityDigest": "sha256:ead85aae8385c4b3aefbe70ba595ab30319041c16e2be22e6129c3c1796cd322", + "compatibilityDigest": "sha256:1683cb2ec7703bb3af52a0935a2726cadf7391c732a011c3a77ea831bbb87eda", "buildIdentity": { "sourceSha": null, "buildDigest": null, @@ -25,11 +25,11 @@ "packages/runtime-contracts-rs/fixtures/runtime-receipt-v1.json", "packages/runtime-contracts-rs/fixtures/runtime-receipt-contract-v1.json", "packages/local-host-rs/src/transcript.rs", - "packages/local-host-rs/src/hosted_runner/thread_protocol.rs", + "packages/hosted-runner-rs/src/hosted_runner/thread_protocol.rs", "proto/maestro/v1/hosted-thread-compatibility-matrix.json", - "packages/local-host-rs/src/hosted_runner_cli.rs", - "packages/local-host-rs/src/hosted_runner.rs", - "packages/local-host-rs/src/hosted_runner/rendezvous_protocol.rs" + "packages/hosted-runner-rs/src/hosted_runner_cli.rs", + "packages/hosted-runner-rs/src/hosted_runner.rs", + "packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol.rs" ], "compatibility": { "headless": { @@ -38,7 +38,7 @@ ], "schemaDeclaredVersion": "2026-08-08", "schemaContractDigest": "sha256:5888b53b9771d2f67376c61386c0071b8d28ab928617bd7c51fe38f63d5b838e", - "runtimeContractDigest": "sha256:c2483714ba8ad6b5f4b8bf19dc7670df5a784300c99a8216c023299d822046e8", + "runtimeContractDigest": "sha256:b0124f38d2a1616ee04c9514462eb12b9049cd6de36f5f7e70c5512afd1dc9a6", "messages": { "toRuntime": [ "managed_authorization_result", diff --git a/scripts/build-profile-contract.test.mjs b/scripts/build-profile-contract.test.mjs index 58eaa74d9..914051b94 100644 --- a/scripts/build-profile-contract.test.mjs +++ b/scripts/build-profile-contract.test.mjs @@ -19,7 +19,7 @@ const cargo = read("Cargo.toml"); const materialize = read("scripts/materialize-native-package.mjs"); const releaseBuilder = read("scripts/build-release-binary.mjs"); const dockerfile = read("Dockerfile"); -const updateCli = read("packages/tui-rs/src/update_cli.rs"); +const updateCli = read("packages/cli-commands-rs/src/update_cli.rs"); const ci = read(".github/workflows/maestro-ci.yml"); const release = read(".github/workflows/release.yml"); const ghcr = read(".github/workflows/ghcr-publish.yml"); diff --git a/scripts/check-deixic-code-naming.mjs b/scripts/check-deixic-code-naming.mjs index 92507afd9..865196159 100644 --- a/scripts/check-deixic-code-naming.mjs +++ b/scripts/check-deixic-code-naming.mjs @@ -13,7 +13,7 @@ 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/tui-rs/src/crash_handler.rs", ["=== Deixic Code Crash Report ==="]], + ["packages/tui-kit-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\""]], ["packages/tui-rs/Cargo.toml", ["description = \"Native terminal UI renderer for Deixic Code\""]], @@ -49,18 +49,18 @@ const FORBIDDEN_DISPLAY_TEXT = new Map([ ["packages/local-host-rs/src/init_cli.rs", ["Registering Maestro with", "\"client_name\": \"Maestro CLI\"", "return to Maestro"]], ["packages/local-host-rs/src/evalops_cli/platform_tools/config.rs", ["Restart Maestro to load"]], ["packages/local-host-rs/src/evalops_cli/platform_tools/provision.rs", ["\"client_name\": \"Maestro Platform Tools\"", "return to Maestro"]], - ["packages/tui-rs/src/crash_handler.rs", ["=== Maestro Crash Report ==="]], - ["packages/tui-rs/src/acp_cli.rs", ["\"title\": \"Maestro\"", "failed to launch Maestro agent"]], + ["packages/tui-kit-rs/src/crash_handler.rs", ["=== Maestro Crash Report ==="]], + ["packages/cli-commands-rs/src/acp_cli.rs", ["\"title\": \"Maestro\"", "failed to launch Maestro agent"]], ["packages/tui-rs/src/app.rs", ["Maestro TUI - Keyboard Shortcuts"]], - ["packages/tui-rs/src/a2a_cli/mod.rs", ["Maestro A2A Peer"]], - ["packages/tui-rs/src/a2a_cli/pairing.rs", ["Maestro A2A Peer"]], + ["packages/a2a-cli-rs/src/a2a_cli/mod.rs", ["Maestro A2A Peer"]], + ["packages/a2a-cli-rs/src/a2a_cli/pairing.rs", ["Maestro A2A Peer"]], ["packages/local-host-rs/src/agents_cli.rs", ["Existing Maestro agent instructions"]], ["packages/local-host-rs/src/evalops_cli/platform_tools.rs", ["from Maestro Platform tools CLI"]], - ["packages/tui-rs/src/keybindings.rs", ["inside Maestro"]], + ["packages/tui-kit-rs/src/keybindings.rs", ["inside Maestro"]], ["packages/local-host-rs/src/plugins/manager.rs", ["Maestro will not load this plugin"]], ["packages/local-host-rs/src/hooks/context.rs", ["Maestro dropped this hook"]], ["packages/local-host-rs/src/tools/bash/mod.rs", ["Blocked by Maestro's native sandbox", "inside Maestro's native OS sandbox"]], - ["packages/tui-rs/src/update_cli.rs", ["signed Maestro installer", "Another Maestro update"]], + ["packages/cli-commands-rs/src/update_cli.rs", ["signed Maestro installer", "Another Maestro update"]], ["packages/local-host-rs/src/mcp/client.rs", ["Maestro turn cancelled"]], ["packages/local-host-rs/src/mcp/http.rs", ["Maestro turn cancelled"]], ["packages/local-host-rs/src/mailbox.rs", ["Pending Maestro mailbox messages", "another Maestro process"]], diff --git a/scripts/check-localization.py b/scripts/check-localization.py index 7961e2818..99482755e 100644 --- a/scripts/check-localization.py +++ b/scripts/check-localization.py @@ -96,7 +96,7 @@ def check(root: Path) -> list[str]: if key not in known: line = text.count('\n', 0, match.start()) + 1 errors.append(f'{path.relative_to(root)}:{line}: missing catalog key {key!r}') - metadata = root / 'packages/tui-rs/src/mcp_catalog.rs' + metadata = root / 'packages/cli-commands-rs/src/mcp_catalog.rs' for match in re.finditer(r'(?:description|category):\s*(' + STRING + r')', metadata.read_text()): key = json.loads(match[1]) if key not in known: diff --git a/scripts/generate-protocol-compatibility-manifest.mjs b/scripts/generate-protocol-compatibility-manifest.mjs index d691a6d3b..092e0c851 100644 --- a/scripts/generate-protocol-compatibility-manifest.mjs +++ b/scripts/generate-protocol-compatibility-manifest.mjs @@ -23,11 +23,11 @@ const SOURCE_PATHS = { runtimeReceiptContractFixture: "packages/runtime-contracts-rs/fixtures/runtime-receipt-contract-v1.json", transcript: "packages/local-host-rs/src/transcript.rs", - thread: "packages/local-host-rs/src/hosted_runner/thread_protocol.rs", + thread: "packages/hosted-runner-rs/src/hosted_runner/thread_protocol.rs", threadCompatibilityMatrix: "proto/maestro/v1/hosted-thread-compatibility-matrix.json", - resident: "packages/local-host-rs/src/hosted_runner_cli.rs", - hostedRunner: "packages/local-host-rs/src/hosted_runner.rs", - rendezvous: "packages/local-host-rs/src/hosted_runner/rendezvous_protocol.rs", + resident: "packages/hosted-runner-rs/src/hosted_runner_cli.rs", + hostedRunner: "packages/hosted-runner-rs/src/hosted_runner.rs", + rendezvous: "packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol.rs", }; const ENUM_EXPORTS = { diff --git a/scripts/generate-protocol-compatibility-manifest.test.mjs b/scripts/generate-protocol-compatibility-manifest.test.mjs index 8d3c678e3..6c3926f48 100644 --- a/scripts/generate-protocol-compatibility-manifest.test.mjs +++ b/scripts/generate-protocol-compatibility-manifest.test.mjs @@ -441,10 +441,10 @@ test("source-root receipt generation supports the legacy v1-only recovery source runtimeReceipts: "packages/runtime-contracts-rs/src/receipts.rs", runtimeReceiptFixture: "packages/runtime-contracts-rs/fixtures/runtime-receipt-v1.json", transcript: "packages/local-host-rs/src/transcript.rs", - thread: "packages/local-host-rs/src/hosted_runner/thread_protocol.rs", - resident: "packages/local-host-rs/src/hosted_runner_cli.rs", - hostedRunner: "packages/local-host-rs/src/hosted_runner.rs", - rendezvous: "packages/local-host-rs/src/hosted_runner/rendezvous_protocol.rs", + thread: "packages/hosted-runner-rs/src/hosted_runner/thread_protocol.rs", + resident: "packages/hosted-runner-rs/src/hosted_runner_cli.rs", + hostedRunner: "packages/hosted-runner-rs/src/hosted_runner.rs", + rendezvous: "packages/hosted-runner-rs/src/hosted_runner/rendezvous_protocol.rs", }; const legacyCodeMode = `/// Runtime execution mode negotiated by a governing controller. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] diff --git a/scripts/generate-runtime-passport.mjs b/scripts/generate-runtime-passport.mjs index cb6d01284..875a7138e 100644 --- a/scripts/generate-runtime-passport.mjs +++ b/scripts/generate-runtime-passport.mjs @@ -12,7 +12,7 @@ const CONFORMANCE_VERSION = "evalops.maestro.runtime-conformance.v1"; const FIXTURE = resolve(ROOT, "packages/runtime-contracts-rs/fixtures/runtime-conformance-v1.json"); const PASSPORT_CONTRACT = resolve(ROOT, "packages/runtime-contracts-rs/fixtures/runtime-passport-contract-v1.json"); const DRIVER = resolve(ROOT, "scripts/run-runtime-conformance.mjs"); -const NATIVE_FIXTURE = resolve(ROOT, "packages/local-host-rs/src/hosted_runner_conformance.rs"); +const NATIVE_FIXTURE = resolve(ROOT, "packages/hosted-runner-rs/src/hosted_runner_conformance.rs"); function canonicalize(value) { if (value === null || typeof value === "boolean" || typeof value === "string") { diff --git a/scripts/generate-runtime-passport.test.mjs b/scripts/generate-runtime-passport.test.mjs index dac26645e..6bc31f555 100644 --- a/scripts/generate-runtime-passport.test.mjs +++ b/scripts/generate-runtime-passport.test.mjs @@ -69,7 +69,7 @@ test("canonical conformance fixture is present and versioned", () => { test("passport suite digest includes the executable native fixture source", () => { const identity = buildRuntimeConformanceIdentity(); const nativeFixtureDigest = `sha256:${createHash("sha256") - .update(readFileSync("packages/local-host-rs/src/hosted_runner_conformance.rs")) + .update(readFileSync("packages/hosted-runner-rs/src/hosted_runner_conformance.rs")) .digest("hex")}`; assert.equal(identity.nativeFixtureDigest, nativeFixtureDigest); assert.equal(buildRuntimePassport(options).conformance.suiteDigest, identity.suiteDigest); diff --git a/scripts/map-vscode-themes.py b/scripts/map-vscode-themes.py index c0dcca551..4a8fd19b1 100644 --- a/scripts/map-vscode-themes.py +++ b/scripts/map-vscode-themes.py @@ -14,7 +14,7 @@ from pathlib import Path COMMIT = "d9637b3f2faa8f8ce0636e556291fdb1ba714c0b" -DEST = Path(__file__).resolve().parents[1] / "packages/tui-rs/src/themes/vscode" +DEST = Path(__file__).resolve().parents[1] / "packages/tui-kit-rs/src/themes/vscode" def rgba(value): diff --git a/scripts/test-check-ui-consistency.py b/scripts/test-check-ui-consistency.py index 1681cac24..6e9c9ed71 100644 --- a/scripts/test-check-ui-consistency.py +++ b/scripts/test-check-ui-consistency.py @@ -39,7 +39,7 @@ def test_inline_test_method_does_not_hide_later_methods(self): def test_scope_excludes_palette_owner_and_unmigrated_components(self): self.assertEqual(len(check.FILES), 10) - self.assertNotIn('packages/tui-rs/src/themes.rs', check.FILES) + self.assertNotIn('packages/tui-kit-rs/src/themes.rs', check.FILES) self.assertNotIn('packages/tui-rs/src/app.rs', check.FILES) self.assertTrue(all((check.ROOT / p).is_file() for p in check.FILES)) diff --git a/scripts/update-lifecycle-contract.test.mjs b/scripts/update-lifecycle-contract.test.mjs index 2100cee49..49e514cf4 100644 --- a/scripts/update-lifecycle-contract.test.mjs +++ b/scripts/update-lifecycle-contract.test.mjs @@ -28,7 +28,7 @@ test("update lifecycle contract names every machine-readable surface", () => { }); test("startup checks use the public release source with a workable bound", () => { - const updater = read("packages/tui-rs/src/update_cli.rs"); + const updater = read("packages/cli-commands-rs/src/update_cli.rs"); assert.match(updater, /https:\/\/api\.github\.com\/repos\/dx-corp\/code\/releases/); assert.match(updater, /https:\/\/github\.com\/dx-corp\/code\/releases\/latest\/download\/channel-manifest\.json/); assert.match(updater, /DEFAULT_STARTUP_CHECK_TIMEOUT: Duration = Duration::from_secs\(2\)/); @@ -57,7 +57,7 @@ test("release metadata carries changelog notes and exact runtime passports", asy test("installer and release workflow preserve receipt metadata", () => { const installer = read("scripts/install.sh"); const release = readReleaseWorkflow(); - const updater = read("packages/tui-rs/src/update_cli.rs"); + const updater = read("packages/cli-commands-rs/src/update_cli.rs"); const channelManifest = read("scripts/create-release-channel-manifest.mjs"); const channelResolver = read("scripts/resolve-release-channel.mjs"); assert.match(installer, /release-metadata\.json/); diff --git a/vendor/dex-loop/README.md b/vendor/dex-loop/README.md index 4325e40ed..96c5b8d91 100644 --- a/vendor/dex-loop/README.md +++ b/vendor/dex-loop/README.md @@ -79,7 +79,7 @@ outcome. `testing::tenant_isolation` uses matching thread/call IDs in separate organization and workspace scopes over shared storage. The filesystem host (`maestro-dex-host --test recovery_contract`) and hosted -PostgreSQL host (`dex-runtime --test postgres recovery_contract`) run the same +PostgreSQL host (`dex-runtime --test suite postgres::recovery_contract`) run the same assertions. The latter uses real `PgLog` and `DexEffects` over `PgLedger`, requires PostgreSQL, and refuses all external calls. The model and mutation are deterministic probes. These tests prove port recovery and persisted replay; diff --git a/vendor/managed-inference-contract/src/lib.rs b/vendor/managed-inference-contract/src/lib.rs index 0a672a92c..d3d27b1e7 100644 --- a/vendor/managed-inference-contract/src/lib.rs +++ b/vendor/managed-inference-contract/src/lib.rs @@ -273,6 +273,104 @@ pub struct ManagedInferenceScope { pub output_token_budget: ManagedInferenceOutputTokenBudget, } +/// Tenant-independent model route for one dispatched turn: the part of a +/// [`ManagedInferenceScope`] the server chooses from a provider binding before +/// any organization, workspace or turn identifiers exist. Routing is always +/// ordered over the single selected candidate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ManagedInferenceRoute { + pub endpoint: String, + pub model: String, + pub provider_candidates: Vec, + pub output_token_budget: ManagedInferenceOutputTokenBudget, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum ManagedInferenceRouteError { + #[error("The server-selected model binding is unavailable")] + InvalidBinding, + #[error("The Process model has no priced Work route")] + UnpricedWorkRoute, +} + +const ROUTE_FIELD_MAX_LEN: usize = 512; +const PREPAID_OUTPUT_TOKEN_CEILING: u64 = 32_768; + +impl ManagedInferenceProviderBinding { + /// Build the server-selected route used by interactive turns. Prepaid + /// coordinates get the chat-completions route ceiling; every other binding + /// keeps the provider default budget. + pub fn dispatch_route(&self) -> Result { + let valid_required = |value: &str| { + !value.is_empty() + && value.len() <= ROUTE_FIELD_MAX_LEN + && !value.chars().any(char::is_control) + }; + let valid_optional = |value: &str| { + value.len() <= ROUTE_FIELD_MAX_LEN && !value.chars().any(char::is_control) + }; + if !valid_required(&self.model) + || !valid_required(&self.provider) + || !valid_required(&self.provider_environment) + || !valid_optional(&self.credential_name) + || !valid_optional(&self.team_id) + { + return Err(ManagedInferenceRouteError::InvalidBinding); + } + + let funded = self.is_prepaid(); + let output_token_budget = if funded { + ManagedInferenceOutputTokenBudget { + value: Some(PREPAID_OUTPUT_TOKEN_CEILING), + origin: ManagedInferenceBudgetOrigin::RoutePolicy, + origin_reference: "managed-prepaid-v1".into(), + } + } else { + ManagedInferenceOutputTokenBudget { + value: None, + origin: ManagedInferenceBudgetOrigin::ProviderDefault, + origin_reference: String::new(), + } + }; + Ok(ManagedInferenceRoute { + endpoint: if funded { + "chat_completions" + } else { + "responses" + } + .into(), + model: self.model.clone(), + provider_candidates: vec![ManagedInferenceProviderCandidate { + provider: self.provider.clone(), + environment: self.provider_environment.clone(), + credential_name: self.credential_name.clone(), + team_id: self.team_id.clone(), + model: self.model.clone(), + }], + output_token_budget, + }) + } + + /// Build the priced Work route used by managed Process turns. + /// + /// The route-policy value is a ceiling: a resident may request fewer + /// tokens, while Gateway verifies the unchanged signed scope before + /// reserving money. + pub fn work_route(&self) -> Result { + if prepaid_model_pricing(&self.provider, &self.model).is_none() { + return Err(ManagedInferenceRouteError::UnpricedWorkRoute); + } + let mut route = self.dispatch_route()?; + route.endpoint = "chat_completions".into(); + route.output_token_budget = ManagedInferenceOutputTokenBudget { + value: Some(PREPAID_OUTPUT_TOKEN_CEILING), + origin: ManagedInferenceBudgetOrigin::RoutePolicy, + origin_reference: "process-work-v1".into(), + }; + Ok(route) + } +} + /// Reference to Billing's immutable Work allowance. The signature binds the /// reference to the same tenant and turn as model execution. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]